Compare commits
6 commits
02ffd4a013
...
9a6b8094da
Author | SHA1 | Date | |
---|---|---|---|
9a6b8094da | |||
9b7e6fdbd4 | |||
c459450abc | |||
96e55c52a5 | |||
b9b8f55c28 | |||
5afc7da40d |
21 changed files with 690 additions and 649 deletions
|
@ -14,10 +14,16 @@ module.exports = {
|
|||
parser: "svelte-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
svelteFeatures: {
|
||||
experimentalGenerics: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {},
|
||||
globals: {
|
||||
$$Generic: "readonly",
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,5 +8,3 @@ node_modules
|
|||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
sveltekit-zero-api.d.ts
|
||||
/.vscode
|
||||
|
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"]
|
||||
}
|
52
package.json
52
package.json
|
@ -17,13 +17,13 @@
|
|||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.27.0",
|
||||
"@auth/core": "^0.28.2",
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@prisma/client": "^5.10.2",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-dompurify": "^2.4.0",
|
||||
"marked": "^12.0.0",
|
||||
"isomorphic-dompurify": "^2.6.0",
|
||||
"marked": "^12.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"zod": "^3.22.4",
|
||||
|
@ -31,40 +31,40 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@playwright/test": "^1.42.0",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@trpc/client": "^10.45.1",
|
||||
"@trpc/server": "^10.45.1",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/node": "^20.11.21",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/set-cookie-parser": "^2.4.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"daisyui": "^4.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.9.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.5.3",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-nesting": "^12.0.4",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nesting": "^12.1.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"prisma": "^5.10.2",
|
||||
"prisma": "^5.12.1",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.5",
|
||||
"sveltekit-superforms": "^2.6.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"trpc-sveltekit": "^3.5.27",
|
||||
"svelte-check": "^3.6.9",
|
||||
"sveltekit-superforms": "^2.12.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"trpc-sveltekit": "^3.6.1",
|
||||
"tslib": "^2.6.2",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.4",
|
||||
"vitest": "^1.3.1"
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "^1.4.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
1109
pnpm-lock.yaml
1109
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -10,33 +10,46 @@
|
|||
import { createFloatingActions } from "svelte-floating-ui";
|
||||
import { shift } from "svelte-floating-ui/dom";
|
||||
import outclick from "$lib/actions/outclick";
|
||||
import type { BaseItem, SelectionOrText } from "./types";
|
||||
import type { BaseItem } from "./types";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type T = $$Generic<BaseItem>;
|
||||
type OnSelectResult = { newValue: string; close: boolean };
|
||||
|
||||
export let items: BaseItem[] | (() => Promise<BaseItem[]>);
|
||||
export let selection: SelectionOrText | null = null;
|
||||
/** List of items to choose from (or an async function fetching them) */
|
||||
export let items: T[] | (() => Promise<T[]>);
|
||||
/** Current selection of the autocomplete field */
|
||||
export let selection: T | null = null;
|
||||
/** Set of item IDs that should be hidden from the list */
|
||||
export let hiddenIds: Set<string | number> = new Set();
|
||||
export let cache: { [key: string]: BaseItem[] } = {};
|
||||
/** Object to cache fetched items in */
|
||||
export let cache: { [key: string]: T[] } = {};
|
||||
/** Key in cache object under which fetched items are stored */
|
||||
export let cacheKey: string | undefined = undefined;
|
||||
/** Input field placeholder */
|
||||
export let placeholder: string | undefined = undefined;
|
||||
/** Add horizontal padding to the input element */
|
||||
export let padding = true;
|
||||
/** CSS classes of the container */
|
||||
export let cls = "";
|
||||
/** CSS classes of the input element */
|
||||
export let inputCls = "w-full bg-transparent outline-none";
|
||||
export let asTextInput = false;
|
||||
/**
|
||||
* Enable filter bar optimizations:
|
||||
* - Clear input on mount
|
||||
*/
|
||||
export let partOfFilterbar = false;
|
||||
/** Dont select item automatically on close if only 1 item is shown */
|
||||
export let noAutoselect1 = false;
|
||||
/** Name of the form input. This sets a hidden field to the ID of the selected element
|
||||
* (to allow Svelte Superforms to work) */
|
||||
export let idInputName: string | undefined = undefined;
|
||||
export let filterFn: (item: BaseItem) => boolean = () => true;
|
||||
/** Function to filter shown items */
|
||||
export let filterFn: (item: T) => boolean = () => true;
|
||||
|
||||
/** Selection callback. Returns the new input value after selection */
|
||||
export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = (
|
||||
item,
|
||||
kb
|
||||
) => {
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
};
|
||||
export let onSelect: (item: T, kb: boolean) => OnSelectResult | void = () => {};
|
||||
export let onUnselect = () => {};
|
||||
export let onClose = (kb: boolean) => {};
|
||||
export let onBackspace = () => {};
|
||||
|
@ -44,10 +57,11 @@
|
|||
let opened = false;
|
||||
let highlightIndex = 0;
|
||||
let isLoading = false;
|
||||
let srcItems: BaseItem[];
|
||||
let filteredItems: BaseItem[] = [];
|
||||
let srcItems: T[];
|
||||
let filteredItems: T[] = [];
|
||||
|
||||
$: if (hiddenIds || selection) updateSearch();
|
||||
// Reload search items if hidden ids are changed
|
||||
$: if (srcItems && hiddenIds) updateSearch();
|
||||
|
||||
// HTML elements
|
||||
let inputElm: HTMLInputElement | undefined;
|
||||
|
@ -92,7 +106,7 @@
|
|||
if (i !== -1) {
|
||||
highlightIndex = i;
|
||||
}
|
||||
if (asTextInput) setInputValue(selection.name ?? "");
|
||||
if (!partOfFilterbar) setInputValue(selection.name ?? "");
|
||||
} else {
|
||||
highlightIndex = 0;
|
||||
setInputValue(selection.name ?? "");
|
||||
|
@ -142,30 +156,34 @@
|
|||
if (inputElm) inputElm.focus();
|
||||
}
|
||||
|
||||
export function close(kb: boolean) {
|
||||
export function close(kb = true) {
|
||||
if (opened) {
|
||||
onClose(kb);
|
||||
}
|
||||
opened = false;
|
||||
}
|
||||
|
||||
function selectListItem(item: BaseItem | undefined, kb: boolean) {
|
||||
selection = { id: item?.id, name: item?.name };
|
||||
const selRes = onSelect(item ?? { name: inputValue() }, kb);
|
||||
setInputValue(selRes.newValue);
|
||||
if (selRes.close) {
|
||||
function selectListItem(item: T | undefined, kb: boolean) {
|
||||
if (item) {
|
||||
selection = item;
|
||||
const selRes = onSelect(item, kb);
|
||||
setInputValue(selRes ? selRes.newValue : item.name);
|
||||
if (!selRes || selRes.close) {
|
||||
close(kb);
|
||||
} else {
|
||||
updateSearch();
|
||||
}
|
||||
} else {
|
||||
selection = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
let key = e.key;
|
||||
if (key === "Tab" && e.shiftKey) key = "ShiftTab";
|
||||
const fnmap = {
|
||||
Tab: close,
|
||||
ShiftTab: close,
|
||||
const fnmap: { [key: string]: () => void } = {
|
||||
Tab: () => close,
|
||||
ShiftTab: () => close,
|
||||
ArrowDown: () => {
|
||||
open();
|
||||
if (highlightIndex < filteredItems.length - 1) {
|
||||
|
@ -184,7 +202,7 @@
|
|||
e.stopPropagation();
|
||||
if (opened) {
|
||||
if (inputElm) inputElm.focus();
|
||||
close(true);
|
||||
close();
|
||||
}
|
||||
},
|
||||
Backspace: () => {
|
||||
|
@ -195,10 +213,9 @@
|
|||
}
|
||||
},
|
||||
};
|
||||
// @ts-expect-error unknown map type
|
||||
const fn = fnmap[key];
|
||||
if (typeof fn === "function") {
|
||||
fn(e);
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,6 +267,14 @@
|
|||
placement: "bottom-start",
|
||||
middleware: [shift()],
|
||||
});
|
||||
|
||||
if (!partOfFilterbar) {
|
||||
onMount(() => {
|
||||
if (selection?.name) {
|
||||
setInputValue(selection.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-grow {cls}" use:outclick on:outclick={close}>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
/** True if a separate search field should be displayed */
|
||||
export let search = false;
|
||||
|
||||
let autocomplete: Autocomplete | undefined;
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
let activeFilters: FilterData[] = [];
|
||||
let cache: { [key: string]: BaseItem[] } = {};
|
||||
let searchVal = "";
|
||||
|
@ -216,6 +216,7 @@
|
|||
activeFilters = activeFilters;
|
||||
updateFilter();
|
||||
}}
|
||||
partOfFilterbar
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
export let onRemove = () => {};
|
||||
export let onSelection = (selection: SelectionOrText, kb: boolean) => {};
|
||||
|
||||
let autocomplete: Autocomplete | undefined;
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
|
||||
function startEditing() {
|
||||
fdata.editing = true;
|
||||
|
@ -76,7 +76,9 @@ gap-1 pl-1"
|
|||
hiddenIds={hids}
|
||||
{cache}
|
||||
cacheKey={filter.id}
|
||||
selection={fdata.selection}
|
||||
selection={fdata.selection?.id
|
||||
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
|
||||
: null}
|
||||
padding={false}
|
||||
onSelect={(item) => {
|
||||
// Accept the selection if this is a free text field or the user selected a variant
|
||||
|
@ -89,6 +91,7 @@ gap-1 pl-1"
|
|||
}}
|
||||
{onClose}
|
||||
onBackspace={onRemove}
|
||||
partOfFilterbar
|
||||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
|
|
|
@ -68,16 +68,12 @@
|
|||
}}
|
||||
selection={patient?.room}
|
||||
onSelect={(item) => {
|
||||
// @ts-expect-error room items have station attr
|
||||
station = item.station;
|
||||
// @ts-expect-error ids are always numeric
|
||||
$form.room_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
}}
|
||||
onUnselect={() => {
|
||||
$form.room_id = null;
|
||||
}}
|
||||
asTextInput
|
||||
idInputName="room_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import FormField from "./FormField.svelte";
|
||||
import Markdown from "./Markdown.svelte";
|
||||
import type { InputConstraint } from "sveltekit-superforms";
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast } from "@mdi/js";
|
||||
|
||||
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||
|
|
|
@ -117,10 +117,10 @@ export async function newEntryVersion(
|
|||
|
||||
// Check if there are any updates
|
||||
if (
|
||||
cver.text === updatedVersion.text &&
|
||||
cver.date.getTime() === updatedVersion.date.getTime() &&
|
||||
cver.category_id === updatedVersion.category_id &&
|
||||
cver.priority === updatedVersion.priority
|
||||
cver?.text === updatedVersion.text &&
|
||||
cver?.date.getTime() === updatedVersion.date.getTime() &&
|
||||
cver?.category_id === updatedVersion.category_id &&
|
||||
cver?.priority === updatedVersion.priority
|
||||
) {
|
||||
return cver.id;
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ export async function newEntryExecution(
|
|||
}
|
||||
|
||||
// Check if there are any updates
|
||||
if (execution.text === cex.text) {
|
||||
if (execution.text === cex?.text) {
|
||||
return cex.id;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,9 @@ class SearchQueryComponents {
|
|||
*/
|
||||
export function parseSearchQuery(q: string): SearchQueryComponents {
|
||||
const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g;
|
||||
const components = Array.from(q.replaceAll("'", '"').matchAll(regexpParts), (m) => {
|
||||
const components = Array.from(
|
||||
q.replaceAll("'", '"').replaceAll("\\", "\\\\").matchAll(regexpParts),
|
||||
(m) => {
|
||||
const negative = m[1] === "-";
|
||||
// Exact
|
||||
if (m[2]) {
|
||||
|
@ -95,7 +97,8 @@ export function parseSearchQuery(q: string): SearchQueryComponents {
|
|||
} else {
|
||||
return new SearchQueryComponent(m[3], QueryComponentType.Normal, negative);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
components.length > 0 &&
|
||||
|
|
|
@ -74,7 +74,7 @@ export function formatBool(val: boolean): string {
|
|||
|
||||
export function getQueryUrl(q: EntityQuery, basePath: string): string {
|
||||
if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath;
|
||||
return basePath + "/" + JSON.stringify(q);
|
||||
return encodeURI(basePath + "/" + encodeURIComponent(JSON.stringify(q)));
|
||||
}
|
||||
|
||||
export function gotoEntityQuery(q: EntityQuery, basePath: string) {
|
||||
|
|
|
@ -51,14 +51,12 @@
|
|||
}}
|
||||
selection={data.entry.current_version.category}
|
||||
onSelect={(item) => {
|
||||
// @ts-expect-error ids are always numeric
|
||||
$form.category_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
}}
|
||||
onUnselect={() => {
|
||||
$form.category_id = null;
|
||||
}}
|
||||
asTextInput
|
||||
idInputName="category_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import { formatBool, formatDate } from "$lib/shared/util";
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
import type { PageData } from "./$types";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
|
|
@ -27,14 +27,12 @@
|
|||
return await trpc().room.list.query();
|
||||
}}
|
||||
onSelect={(item) => {
|
||||
// @ts-expect-error ids are always numeric
|
||||
$form.room_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
}}
|
||||
onUnselect={() => {
|
||||
$form.room_id = null;
|
||||
}}
|
||||
asTextInput
|
||||
idInputName="room_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
@ -46,10 +44,8 @@
|
|||
return await trpc().patient.getNames.query();
|
||||
}}
|
||||
onSelect={(item) => {
|
||||
// @ts-expect-error patient id
|
||||
$form.patient_id = item.id;
|
||||
trpc()
|
||||
// @ts-expect-error patient id
|
||||
.patient.get.query(item.id)
|
||||
.then((p) => {
|
||||
$form.patient_first_name = p.first_name;
|
||||
|
@ -64,11 +60,9 @@
|
|||
$form.patient_last_name = null;
|
||||
$form.patient_age = null;
|
||||
}}
|
||||
asTextInput
|
||||
noAutoselect1
|
||||
idInputName="patient_id"
|
||||
filterFn={(itm) => {
|
||||
// @ts-expect-error patient items have room attr
|
||||
return $form.room_id === null || itm.room_id === $form.room_id;
|
||||
}}
|
||||
placeholder="Neuer Patient"
|
||||
|
@ -119,14 +113,12 @@
|
|||
return await trpc().category.list.query();
|
||||
}}
|
||||
onSelect={(item) => {
|
||||
// @ts-expect-error ids are always numeric
|
||||
$form.category_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
}}
|
||||
onUnselect={() => {
|
||||
$form.category_id = null;
|
||||
}}
|
||||
asTextInput
|
||||
idInputName="category_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { ZPatientNew, ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZEntriesQuery, ZPatientNew, ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
@ -8,10 +10,9 @@ import type { PageLoad } from "./$types";
|
|||
export const load: PageLoad = async (event) => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let query: any = {};
|
||||
let query: z.infer<typeof ZEntriesQuery> = {};
|
||||
if (event.params.query) {
|
||||
query = JSON.parse(event.params.query);
|
||||
query = ZEntriesQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
|
||||
}
|
||||
if (!query.filter) query.filter = {};
|
||||
query.filter.patient = [{ id }];
|
||||
|
|
|
@ -9,7 +9,7 @@ export const load: PageLoad = async (event) => {
|
|||
let query: z.infer<typeof ZPatientsQuery> = {};
|
||||
|
||||
if (event.params.query) {
|
||||
query = ZPatientsQuery.parse(JSON.parse(event.params.query));
|
||||
query = ZPatientsQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
|
||||
}
|
||||
|
||||
const patients = await loadWrap(async () => trpc(event).patient.list.query(query));
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
let query = {};
|
||||
let query: z.infer<typeof ZEntriesQuery> = {};
|
||||
|
||||
if (event.params.query) {
|
||||
query = JSON.parse(event.params.query);
|
||||
query = ZEntriesQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
|
||||
}
|
||||
|
||||
const entries = await loadWrap(() => trpc(event).entry.list.query(query));
|
||||
|
|
|
@ -9,9 +9,7 @@ const config = {
|
|||
|
||||
kit: {
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({
|
||||
precompress: true,
|
||||
}),
|
||||
adapter: adapter(),
|
||||
|
||||
alias: {
|
||||
$api: "./src/api",
|
||||
|
|
Loading…
Reference in a new issue