From f485db2968dff97dd03d6ec34f2f71441c322256 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 7 May 2024 02:39:56 +0200 Subject: [PATCH 1/4] chore(release): release v0.2.1 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595addb..3d7c51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. +## [v0.2.1](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.2.0..v0.2.1) - 2024-05-07 + +### 🐛 Bug Fixes + +- Carta editor mobile context menu transparent - ([6bd7e15](https://code.thetadev.de/HSA/Visitenbuch/commit/6bd7e157eea6589be82692499cb956455386f7e5)) +- Dont output null age - ([c2c21d1](https://code.thetadev.de/HSA/Visitenbuch/commit/c2c21d1296c399324cdfa661c490cee79f7e16e1)) + + ## [v0.2.0](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.1.0..v0.2.0) - 2024-05-06 ### 🚀 Features diff --git a/package.json b/package.json index fbb40e9..2df97c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "visitenbuch", - "version": "0.2.0", + "version": "0.2.1", "private": true, "license": "AGPL-3.0", "scripts": { From f630ee08b888cc098e8dfb41a2e6f87c3e2f264f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 10 May 2024 13:51:55 +0200 Subject: [PATCH 2/4] feat: create executions without marking entry as done ('notes') --- .../migration.sql | 2 ++ prisma/schema.prisma | 1 + src/lib/components/entry/EntryBody.svelte | 20 ++++++++++--- src/lib/components/table/EntryTable.svelte | 4 +-- src/lib/components/ui/EntryTodoButton.svelte | 11 +++++++ .../components/ui/markdown/Markdown.svelte | 9 ++++-- src/lib/server/query/entry.ts | 10 +++++-- src/lib/server/query/mapping.ts | 1 + src/lib/shared/model/model.ts | 2 ++ src/lib/shared/model/validation.ts | 1 + src/lib/shared/util/diff.ts | 2 ++ src/lib/shared/util/util.ts | 21 ++++++++++++-- src/routes/(app)/entry/[id]/+page.server.ts | 28 +++++++++++++----- src/routes/(app)/entry/[id]/+page.svelte | 16 +++++----- src/routes/(app)/entry/[id]/+page.ts | 14 ++++++++- .../entry/[id]/editExecution/+page.server.ts | 15 ++++++++-- .../entry/[id]/editExecution/+page.svelte | 10 ++++--- .../(app)/entry/[id]/executions/+page.svelte | 3 ++ tests/integration/query/entry.ts | 29 ++++++++++++++----- 19 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 prisma/migrations/20240509111910_execution_done/migration.sql create mode 100644 src/lib/components/ui/EntryTodoButton.svelte diff --git a/prisma/migrations/20240509111910_execution_done/migration.sql b/prisma/migrations/20240509111910_execution_done/migration.sql new file mode 100644 index 0000000..fc40831 --- /dev/null +++ b/prisma/migrations/20240509111910_execution_done/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "entry_executions" ADD COLUMN "done" BOOLEAN NOT NULL DEFAULT true; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b626e89..633705d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -142,6 +142,7 @@ model EntryExecution { entry_id Int text String + done Boolean @default(true) author User @relation(fields: [author_id], references: [id]) author_id Int diff --git a/src/lib/components/entry/EntryBody.svelte b/src/lib/components/entry/EntryBody.svelte index 2dd3afc..48b5383 100644 --- a/src/lib/components/entry/EntryBody.svelte +++ b/src/lib/components/entry/EntryBody.svelte @@ -39,7 +39,7 @@

Erstellt {humanDate(entry.created_at, true)} · - {#if entry.execution} + {#if entry.execution?.done} Erledigt {humanDate(entry.execution.created_at)} {:else} Zu erledigen {humanDate(entry.current_version.date)} @@ -53,7 +53,7 @@ Beschreibung

- +
@@ -69,9 +69,12 @@ {#if withExecution && entry.execution}
-
+

- Erledigt am {formatDate(entry.execution.created_at, true)} von + {entry.execution.done ? "Erledigt am" : "Notiz von"} {formatDate(entry.execution.created_at, true)} von

@@ -88,3 +91,12 @@ {/if}
{/if} + + diff --git a/src/lib/components/table/EntryTable.svelte b/src/lib/components/table/EntryTable.svelte index 7143f49..92305a7 100644 --- a/src/lib/components/table/EntryTable.svelte +++ b/src/lib/components/table/EntryTable.svelte @@ -38,7 +38,7 @@ {#each entries.items as entry (entry.id)} {entry.current_version.text} - {#if entry.execution} + {#if entry.execution?.done} {formatDate(entry.execution.created_at, true)} + {#each { length: 4 } as _, i} + + {/each} +
diff --git a/src/lib/components/ui/markdown/Markdown.svelte b/src/lib/components/ui/markdown/Markdown.svelte index c43ffe0..6562f05 100644 --- a/src/lib/components/ui/markdown/Markdown.svelte +++ b/src/lib/components/ui/markdown/Markdown.svelte @@ -1,10 +1,15 @@ - +
+ + {@html rendered} +
diff --git a/src/lib/server/query/entry.ts b/src/lib/server/query/entry.ts index 8fa402b..cfe8323 100644 --- a/src/lib/server/query/entry.ts +++ b/src/lib/server/query/entry.ts @@ -162,7 +162,7 @@ export async function newEntryExecution( } // Check if there are any updates - if (execution.text === cex?.text) { + if (execution.text === cex?.text && execution.done === cex?.done) { return cex.id; } @@ -171,6 +171,7 @@ export async function newEntryExecution( entry_id, author_id, text: execution.text, + done: execution.done, }, select: { id: true }, }); @@ -199,6 +200,7 @@ export async function getEntries( c.color as category_color, ex.id as execution_id, ex.text as execution_text, + ex.done as execution_done, ex.created_at as execution_created_at, xau.id as execution_author_id, xau.name as execution_author_name, @@ -254,9 +256,9 @@ left join stations s on s.id = r.station_id`, } if (filter?.done === true) { - qb.addFilterClause("ex.id is not null"); + qb.addFilterClause("ex.done"); } else if (filter?.done === false) { - qb.addFilterClause("ex.id is null"); + qb.addFilterClause("(ex.id is null or not ex.done)"); } qb.addFilterList("xau.id", filter?.executor); @@ -323,6 +325,7 @@ left join stations s on s.id = r.station_id`, category_color: string; execution_id: number; execution_text: string; + execution_done: boolean; execution_created_at: Date; execution_author_id: number; execution_author_name: string; @@ -383,6 +386,7 @@ left join stations s on s.id = r.station_id`, id: item.execution_id, author: { id: item.execution_author_id, name: item.execution_author_name }, text: item.execution_text, + done: item.execution_done, created_at: item.execution_created_at, } : null, diff --git a/src/lib/server/query/mapping.ts b/src/lib/server/query/mapping.ts index ad7e041..5ef6852 100644 --- a/src/lib/server/query/mapping.ts +++ b/src/lib/server/query/mapping.ts @@ -82,6 +82,7 @@ export function mapExecution(execution: DbEntryExecutionLn): EntryExecution { author: execution.author, created_at: execution.created_at, text: execution.text, + done: execution.done, }; } diff --git a/src/lib/shared/model/model.ts b/src/lib/shared/model/model.ts index bbecac8..47a25eb 100644 --- a/src/lib/shared/model/model.ts +++ b/src/lib/shared/model/model.ts @@ -130,11 +130,13 @@ export type EntryExecution = { id: number; author: UserTag; text: string; + done: boolean; created_at: Date; }; export type EntryExecutionNew = { text: string; + done: boolean; }; export type SavedFilter = { diff --git a/src/lib/shared/model/validation.ts b/src/lib/shared/model/validation.ts index d353444..7a402c9 100644 --- a/src/lib/shared/model/validation.ts +++ b/src/lib/shared/model/validation.ts @@ -115,6 +115,7 @@ export const ZEntryNew = implement().with({ export const ZEntryExecutionNew = implement().with({ text: fields.TextString(), + done: z.boolean(), }); export const ZPagination = implement().with({ diff --git a/src/lib/shared/util/diff.ts b/src/lib/shared/util/diff.ts index 2db944d..44a10cd 100644 --- a/src/lib/shared/util/diff.ts +++ b/src/lib/shared/util/diff.ts @@ -25,6 +25,7 @@ export type EntryExecutionChange = { created_at: Date; text: Diff.Change[]; + done?: boolean; }; function newOrUndef(o: T, n: T): T | undefined { @@ -83,6 +84,7 @@ export function executionsDiff(executions: EntryExecution[]): EntryExecutionChan author: v.author, created_at: v.created_at, text, + done: v.done, }); prev = v; diff --git a/src/lib/shared/util/util.ts b/src/lib/shared/util/util.ts index 3f7f672..afe25b4 100644 --- a/src/lib/shared/util/util.ts +++ b/src/lib/shared/util/util.ts @@ -5,13 +5,14 @@ import { TRPCClientError } from "@trpc/client"; import DOMPurify from "isomorphic-dompurify"; import qs from "qs"; import type { FormOptions } from "sveltekit-superforms"; +import type { TRPCClientInit } from "trpc-sveltekit"; import { ZodError } from "zod"; import { DEFAULT_FILTER_NAME, URL_VISIT } from "$lib/shared/constants"; import type { EntityQuery, SavedFilter } from "$lib/shared/model"; -import { type RouterOutput } from "$lib/shared/trpc"; +import { trpc, type RouterOutput } from "$lib/shared/trpc"; -import { DateRange } from "./date"; +import { DateRange, utcDateToYMD } from "./date"; import { toastError, toastInfo } from "./toast"; export function formatBool(val: boolean): string { @@ -156,3 +157,19 @@ export function defaultVisitUrl(): string { }, }, URL_VISIT); } + +export async function moveEntryTodoDate(id: number, nTodoDays: number, init?: TRPCClientInit) { + if (nTodoDays > 0) { + const entry = await trpc(init).entry.get.query(id); + const newDate = new Date(entry.current_version.date); + newDate.setDate(newDate.getDate() + nTodoDays); + + await trpc(init).entry.newVersion.mutate({ + id, + old_version_id: entry.current_version.id, + version: { + date: utcDateToYMD(newDate), + }, + }); + } +} diff --git a/src/routes/(app)/entry/[id]/+page.server.ts b/src/routes/(app)/entry/[id]/+page.server.ts index 693fb9c..bfd54cc 100644 --- a/src/routes/(app)/entry/[id]/+page.server.ts +++ b/src/routes/(app)/entry/[id]/+page.server.ts @@ -5,23 +5,35 @@ import { superValidate, message } from "sveltekit-superforms"; import { ZUrlEntityId } from "$lib/shared/model/validation"; import { trpc } from "$lib/shared/trpc"; -import { loadWrap } from "$lib/shared/util"; +import { loadWrap, moveEntryTodoDate } from "$lib/shared/util"; -import { SchemaEntryExecution } from "./schema"; +import { SchemaNewExecution } from "./editExecution/schema"; export const actions: Actions = { default: async (event) => loadWrap(async () => { - const form = await superValidate(event.request, SchemaEntryExecution); + const formData = await event.request.formData(); + const form = await superValidate(formData, SchemaNewExecution); if (!form.valid) { return fail(400, { form }); } const id = ZUrlEntityId.parse(event.params.id); - await trpc(event).entry.newExecution.mutate({ - id, - old_execution_id: null, - execution: { text: form.data.text }, + const todoDays = formData.get("todo"); + const done = todoDays === null; + const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0; + + await loadWrap(async () => { + await trpc(event).entry.newExecution.mutate({ + id, + old_execution_id: form.data.old_execution_id, + execution: { text: form.data.text, done: todoDays === null }, + }); + await moveEntryTodoDate(id, nTodoDays, event); }); - return message(form, "Eintrag erledigt"); + + if (nTodoDays > 0) { + return message(form, `Eintrag um ${nTodoDays} Tage in die Zukunft verschoben`); + } + return message(form, done ? "Eintrag erledigt" : "Eintrag mit Notiz versehen"); }), }; diff --git a/src/routes/(app)/entry/[id]/+page.svelte b/src/routes/(app)/entry/[id]/+page.svelte index 7d0464d..8da72e7 100644 --- a/src/routes/(app)/entry/[id]/+page.svelte +++ b/src/routes/(app)/entry/[id]/+page.svelte @@ -2,29 +2,29 @@ import type { PageData } from "./$types"; - import { defaults, superForm } from "sveltekit-superforms"; + import { superForm } from "sveltekit-superforms"; import { superformConfig } from "$lib/shared/util"; import EntryBody from "$lib/components/entry/EntryBody.svelte"; + import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; - import { SchemaEntryExecution } from "./schema"; + import { SchemaNewExecution } from "./editExecution/schema"; export let data: PageData; - const formData = defaults(SchemaEntryExecution); const { form, errors, enhance, - } = superForm(formData, { - validators: SchemaEntryExecution, + } = superForm(data.form, { + validators: SchemaNewExecution, ...superformConfig("Eintrag"), }); -{#if !data.entry.execution} +{#if !data.entry.execution?.done}
-
+
+
+ {/if} diff --git a/src/routes/(app)/entry/[id]/+page.ts b/src/routes/(app)/entry/[id]/+page.ts index e99bd7b..3e48be4 100644 --- a/src/routes/(app)/entry/[id]/+page.ts +++ b/src/routes/(app)/entry/[id]/+page.ts @@ -1,14 +1,26 @@ import type { PageLoad } from "./$types"; +import { superValidate } from "sveltekit-superforms"; + import { ZUrlEntityId } from "$lib/shared/model/validation"; import { trpc } from "$lib/shared/trpc"; import { loadWrap } from "$lib/shared/util"; +import { SchemaNewExecution } from "./editExecution/schema"; + export const load: PageLoad = async (event) => { const entry = await loadWrap(async () => { const id = ZUrlEntityId.parse(event.params.id); return trpc(event).entry.get.query(id); }); - return { entry }; + const form = await superValidate( + { + old_execution_id: entry.execution?.id, + ...entry.execution, + }, + SchemaNewExecution, + ); + + return { entry, form }; }; diff --git a/src/routes/(app)/entry/[id]/editExecution/+page.server.ts b/src/routes/(app)/entry/[id]/editExecution/+page.server.ts index 8a0cb57..2fd0dbe 100644 --- a/src/routes/(app)/entry/[id]/editExecution/+page.server.ts +++ b/src/routes/(app)/entry/[id]/editExecution/+page.server.ts @@ -5,24 +5,33 @@ import { superValidate } from "sveltekit-superforms"; import { ZUrlEntityId } from "$lib/shared/model/validation"; import { trpc } from "$lib/shared/trpc"; -import { loadWrap } from "$lib/shared/util"; +import { loadWrap, moveEntryTodoDate } from "$lib/shared/util"; import { SchemaNewExecution } from "./schema"; export const actions: Actions = { default: async (event) => loadWrap(async () => { - const form = await superValidate(event.request, SchemaNewExecution); + const formData = await event.request.formData(); + const form = await superValidate(formData, SchemaNewExecution); if (!form.valid) { return fail(400, { form }); } const id = ZUrlEntityId.parse(event.params.id); + const todoDays = formData.get("todo"); + await trpc(event).entry.newExecution.mutate({ id, - execution: form.data, + execution: { + text: form.data.text, + done: todoDays === null, + }, old_execution_id: form.data.old_execution_id, }); + const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0; + await moveEntryTodoDate(id, nTodoDays, event); + redirect(302, `/entry/${id}`); }), }; diff --git a/src/routes/(app)/entry/[id]/editExecution/+page.svelte b/src/routes/(app)/entry/[id]/editExecution/+page.svelte index 4e94ca4..6815372 100644 --- a/src/routes/(app)/entry/[id]/editExecution/+page.svelte +++ b/src/routes/(app)/entry/[id]/editExecution/+page.svelte @@ -2,19 +2,19 @@ import type { PageData } from "./$types"; - import { defaults, superForm } from "sveltekit-superforms"; + import { superForm } from "sveltekit-superforms"; import { superformConfig } from "$lib/shared/util"; import EntryBody from "$lib/components/entry/EntryBody.svelte"; + import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; import { SchemaNewExecution } from "./schema"; export let data: PageData; - const formData = defaults(SchemaNewExecution); - const { form, errors, enhance } = superForm(formData, { + const { form, errors, enhance } = superForm(data.form, { validators: SchemaNewExecution, ...superformConfig("Eintrag"), }); @@ -30,8 +30,10 @@ label="Ausführungstext bearbeiten" bind:value={$form.text} > -
+
+
+ diff --git a/src/routes/(app)/entry/[id]/executions/+page.svelte b/src/routes/(app)/entry/[id]/executions/+page.svelte index 694c7f2..9f39031 100644 --- a/src/routes/(app)/entry/[id]/executions/+page.svelte +++ b/src/routes/(app)/entry/[id]/executions/+page.svelte @@ -22,6 +22,9 @@
#{i + 1}  , {formatDate(version.created_at, true)} + {#if !version.done} +
Notiz
+ {/if}
{#if version.text.length > 0}
diff --git a/tests/integration/query/entry.ts b/tests/integration/query/entry.ts index 226075d..d5932ce 100644 --- a/tests/integration/query/entry.ts +++ b/tests/integration/query/entry.ts @@ -57,8 +57,8 @@ async function insertTestEntries() { }); // Execute entries - await newEntryExecution(1, eId2, { text: "Some execution txt" }); - await newEntryExecution(2, eId3, { text: "More execution txt" }); + await newEntryExecution(1, eId2, { text: "Some execution txt", done: true }); + await newEntryExecution(2, eId3, { text: "More execution txt", done: true }); return { eId1, eId2, eId3 }; } @@ -166,7 +166,7 @@ test("create entry execution", async () => { }); const text = "Blutabnahme erledigt."; - const xId = await newEntryExecution(1, eId, { text }, null); + const xId = await newEntryExecution(1, eId, { text, done: true }, null); const entry = await getEntry(eId); expect(entry.execution?.id).toBe(xId); @@ -183,8 +183,8 @@ test("create entry execution (update)", async () => { version: TEST_VERSION, }); - const x1 = await newEntryExecution(1, eId, { text: "x1" }, null); - const x2 = await newEntryExecution(2, eId, { text: "x2" }, x1); + const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null); + const x2 = await newEntryExecution(2, eId, { text: "x2", done: true }, x1); const entry = await getEntry(eId); expect(entry.execution?.id).toBe(x2); @@ -200,9 +200,9 @@ test("create entry execution (wrong old xid)", async () => { patient_id: 1, version: TEST_VERSION, }); - const x1 = await newEntryExecution(1, eId, { text: "x1" }, null); + const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null); - expect(async () => newEntryExecution(1, eId, { text: "x2" }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match")); + expect(async () => newEntryExecution(1, eId, { text: "x2", done: true }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match")); }); test("get entries", async () => { @@ -304,3 +304,18 @@ test("get category n entries", async () => { await insertTestEntries(); expect(await getCategoryNEntries(3)).toBe(1); }); + +test("todo execution", async () => { + const eId = await newEntry(1, { + patient_id: 1, + version: TEST_VERSION, + }); + + const n1 = await newEntryExecution(1, eId, { text: "note1", done: false }, null); + + const entry = await getEntry(eId); + expect(entry.execution?.id).toBe(n1); + expect(entry.execution?.text).toBe("note1"); + expect(entry.execution?.author.id).toBe(1); + expect(entry.execution?.done).toBe(false); +}); From 2a7aa618d9386951a1da70cd021db8f9bd0b062f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 10 May 2024 15:20:04 +0200 Subject: [PATCH 3/4] feat: add multi-column sort --- src/lib/components/table/EntryTable.svelte | 5 +-- .../table/FilteredEntryTable.svelte | 4 +- .../table/FilteredPatientTable.svelte | 4 +- src/lib/components/table/PatientTable.svelte | 5 +-- src/lib/components/table/SortHeader.svelte | 25 +++++++----- src/lib/server/query/entry.ts | 17 ++++---- src/lib/server/query/patient.ts | 13 ++---- src/lib/server/query/util.test.ts | 18 ++++++++- src/lib/server/query/util.ts | 40 ++++++++++++++++--- src/lib/server/trpc/routes/entry.ts | 2 +- src/lib/server/trpc/routes/patient.ts | 2 +- src/lib/shared/model/requests.ts | 7 +--- src/lib/shared/model/validation.ts | 7 +--- src/lib/shared/util/util.test.ts | 2 +- src/routes/(app)/visit/+page.ts | 2 +- tests/integration/query/entry.ts | 36 ++++++++++++++--- tests/integration/query/patient.ts | 2 +- 17 files changed, 125 insertions(+), 66 deletions(-) diff --git a/src/lib/components/table/EntryTable.svelte b/src/lib/components/table/EntryTable.svelte index 92305a7..6ea8b98 100644 --- a/src/lib/components/table/EntryTable.svelte +++ b/src/lib/components/table/EntryTable.svelte @@ -1,5 +1,4 @@ diff --git a/src/lib/components/table/FilteredEntryTable.svelte b/src/lib/components/table/FilteredEntryTable.svelte index fcb0e38..ab447ed 100644 --- a/src/lib/components/table/FilteredEntryTable.svelte +++ b/src/lib/components/table/FilteredEntryTable.svelte @@ -5,7 +5,7 @@ import { z } from "zod"; - import type { PaginationRequest, SortRequest } from "$lib/shared/model"; + import type { PaginationRequest } from "$lib/shared/model"; import type { ZEntriesQuery } from "$lib/shared/model/validation"; import { type RouterOutput } from "$lib/shared/trpc"; import { getQueryUrl } from "$lib/shared/util"; @@ -34,7 +34,7 @@ updateQuery({ filter, sort: query.sort }); } - function sortUpdate(sort: SortRequest | undefined): void { + function sortUpdate(sort: string[] | undefined): void { updateQuery({ filter: query.filter, pagination: query.pagination, diff --git a/src/lib/components/table/FilteredPatientTable.svelte b/src/lib/components/table/FilteredPatientTable.svelte index f87ac24..eff3843 100644 --- a/src/lib/components/table/FilteredPatientTable.svelte +++ b/src/lib/components/table/FilteredPatientTable.svelte @@ -5,7 +5,7 @@ import { z } from "zod"; - import type { PaginationRequest, SortRequest } from "$lib/shared/model"; + import type { PaginationRequest } from "$lib/shared/model"; import type { ZPatientsQuery } from "$lib/shared/model/validation"; import { type RouterOutput } from "$lib/shared/trpc"; import { getQueryUrl } from "$lib/shared/util"; @@ -32,7 +32,7 @@ updateQuery({ filter, sort: query.sort }); } - function sortUpdate(sort: SortRequest | undefined): void { + function sortUpdate(sort: string[] | undefined): void { updateQuery({ filter: query.filter, pagination: query.pagination, diff --git a/src/lib/components/table/PatientTable.svelte b/src/lib/components/table/PatientTable.svelte index 8df3916..21a3568 100644 --- a/src/lib/components/table/PatientTable.svelte +++ b/src/lib/components/table/PatientTable.svelte @@ -2,7 +2,6 @@ import { mdiFilter } from "@mdi/js"; import { URL_ENTRIES } from "$lib/shared/constants"; - import type { SortRequest } from "$lib/shared/model"; import type { RouterOutput } from "$lib/shared/trpc"; import { formatDate, gotoEntityQuery } from "$lib/shared/util"; @@ -12,8 +11,8 @@ import SortHeader from "./SortHeader.svelte"; export let patients: RouterOutput["patient"]["list"]; - export let sortData: SortRequest | undefined; - export let sortUpdate: (sort: SortRequest | undefined) => void = () => {}; + export let sortData: string[] | undefined; + export let sortUpdate: (sort: string[] | undefined) => void = () => {}; export let baseUrl: string; diff --git a/src/lib/components/table/SortHeader.svelte b/src/lib/components/table/SortHeader.svelte index 40aab97..c4007fe 100644 --- a/src/lib/components/table/SortHeader.svelte +++ b/src/lib/components/table/SortHeader.svelte @@ -1,29 +1,33 @@ @@ -32,6 +36,9 @@ {#if sorting > 0} {/if} + {#if sortData && sortData.length > 1 && index !== -1} + ({index + 1}) + {/if} {title} diff --git a/src/lib/server/query/entry.ts b/src/lib/server/query/entry.ts index cfe8323..3dcfe7f 100644 --- a/src/lib/server/query/entry.ts +++ b/src/lib/server/query/entry.ts @@ -8,15 +8,16 @@ import type { EntryVersionNew, Pagination, PaginationRequest, - SortRequest, } from "$lib/shared/model"; import { DateRange, normalizeLineEndings, utcDateToYMD } from "$lib/shared/util"; -import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error"; +import { ErrorConflict } from "$lib/shared/util/error"; import { prisma } from "$lib/server/prisma"; import { mapEntry, mapVersion, mapExecution } from "./mapping"; -import { QueryBuilder, filterListToArray, parseSearchQuery } from "./util"; +import { + QueryBuilder, filterListToArray, mapSortFields, parseSearchQuery, +} from "./util"; const USER_SELECT = { select: { id: true, name: true } }; @@ -182,7 +183,7 @@ export async function newEntryExecution( export async function getEntries( filter: EntriesFilter = {}, pagination: PaginationRequest = {}, - sort: SortRequest = { field: "created_at", asc: false }, + sort: string[] = [], ): Promise> { const qb = new QueryBuilder( `select @@ -301,13 +302,11 @@ left join stations s on s.id = r.station_id`, date: ["ev.date"], author: ["vau.name"], executor: ["xau.name"], + priority: ["ev.priority"], }; - const sortFields = SORT_FIELDS[sort.field ?? "updated_at"]; - if (!sortFields) { - throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`); - } - qb.orderByFields(sortFields, sort.asc); + const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS); + qb.orderByFields(sortFields); qb.setPagination(pagination); type RowItem = { diff --git a/src/lib/server/query/patient.ts b/src/lib/server/query/patient.ts index 5180280..f1a29ab 100644 --- a/src/lib/server/query/patient.ts +++ b/src/lib/server/query/patient.ts @@ -5,14 +5,12 @@ import type { PaginationRequest, PatientsFilter, PatientTag, - SortRequest, } from "$lib/shared/model"; -import { ErrorInvalidInput } from "$lib/shared/util/error"; import { prisma } from "$lib/server/prisma"; import { mapPatient } from "./mapping"; -import { QueryBuilder, handleDeleteConflict } from "./util"; +import { QueryBuilder, handleDeleteConflict, mapSortFields } from "./util"; export async function newPatient(patient: PatientNew): Promise { const created = await prisma.patient.create({ data: patient, select: { id: true } }); @@ -69,7 +67,7 @@ export async function getPatientNEntries(id: number): Promise { export async function getPatients( filter: PatientsFilter = {}, pagination: PaginationRequest = {}, - sort: SortRequest = { field: "created_at", asc: false }, + sort: string[] = [], ): Promise> { const qb = new QueryBuilder( `select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden, @@ -107,11 +105,8 @@ export async function getPatients( created_at: ["p.created_at"], }; - const sortFields = SORT_FIELDS[sort.field ?? "created_at"]; - if (!sortFields) { - throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`); - } - qb.orderByFields(sortFields, sort.asc); + const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS); + qb.orderByFields(sortFields); qb.setPagination(pagination); type RowItem = { diff --git a/src/lib/server/query/util.test.ts b/src/lib/server/query/util.test.ts index 8eb1692..e89d520 100644 --- a/src/lib/server/query/util.test.ts +++ b/src/lib/server/query/util.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "vitest"; -import { QueryBuilder, parseSearchQuery } from "./util"; +import { QueryBuilder, mapSortFields, parseSearchQuery } from "./util"; test("query builder", () => { const qb = new QueryBuilder("select e.id, e.text, e.category", "from entries e"); @@ -56,3 +56,19 @@ test("parse search query", () => { "'hello' <-> 'world' & 'banana' & !'cherry' & !('b' <-> 'x' <-> 'y') & 'vis':*", ); }); + +test("mapSortFields", () => { + const SORT_FIELDS: Record = { + id: ["e.id"], + patient: ["p.last_name", "p.first_name"], + priority: ["ev.priority"], + }; + const fields = ["id", "patient", "priority:dsc"]; + const res = mapSortFields(fields, "", SORT_FIELDS); + expect(res).toStrictEqual([ + { name: "e.id", asc: true }, + { name: "p.last_name", asc: true }, + { name: "p.first_name", asc: true }, + { name: "ev.priority", asc: false }, + ]); +}); diff --git a/src/lib/server/query/util.ts b/src/lib/server/query/util.ts index c1c7c3f..1cea26c 100644 --- a/src/lib/server/query/util.ts +++ b/src/lib/server/query/util.ts @@ -2,7 +2,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { PAGINATION_LIMIT } from "$lib/shared/constants"; import type { FilterList, PaginationRequest } from "$lib/shared/model"; -import { ErrorConflict } from "$lib/shared/util/error"; +import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error"; enum QueryComponentType { Normal = 1, @@ -37,6 +37,34 @@ export async function handleDeleteConflict(act: Promise, msg: string): } } +export type SortField = { + name: string, + asc: boolean, +}; + +/** Map sort fields from request to a list of database columns for the query builder */ +export function mapSortFields( + fields: string[], + defaultFilter: string, + mapping: Record, +): SortField[] { + if (!fields || fields.length === 0) fields = [defaultFilter]; + + return fields.flatMap((sf) => { + const [fname, fdir] = sf.split(":", 2); + const mapped = mapping[fname]; + if (!mapped) { + throw new ErrorInvalidInput(`cannot sort by "${sf}"`); + } + let asc = true; + if (fdir) { + if (fdir === "dsc") asc = false; + else if (fdir !== "asc") throw new ErrorInvalidInput("Direction must be asc/dsc"); + } + return mapped.map((name) => ({ name, asc })); + }); +} + class SearchQueryComponent { word: string; @@ -157,10 +185,12 @@ export class QueryBuilder { this.orderClauses.push(orderClause); } - orderByFields(fields: string[], asc: boolean | undefined = undefined): void { - const sortDir = asc === false ? " desc" : " asc"; - const orderClause = fields.join(`${sortDir}, `) + sortDir; - this.addOrderClause(orderClause); + orderByFields(fields: SortField[]): void { + const clauseParts = fields.map((f) => { + const sortDir = f.asc ? " asc" : " desc"; + return f.name + sortDir; + }); + this.addOrderClause(clauseParts.join(", ")); } /** Get the next parameter variable (e.g. $1) and increment the counter */ diff --git a/src/lib/server/trpc/routes/entry.ts b/src/lib/server/trpc/routes/entry.ts index 20fdf4d..6d1d084 100644 --- a/src/lib/server/trpc/routes/entry.ts +++ b/src/lib/server/trpc/routes/entry.ts @@ -39,7 +39,7 @@ export const entryRouter = t.router({ .query(async (opts) => trpcWrap(async () => getEntries( opts.input.filter ?? {}, opts.input.pagination ?? {}, - opts.input.sort ?? {}, + opts.input.sort ?? [], ))), versions: t.procedure .input(ZEntityId) diff --git a/src/lib/server/trpc/routes/patient.ts b/src/lib/server/trpc/routes/patient.ts index 2f90645..a266cfc 100644 --- a/src/lib/server/trpc/routes/patient.ts +++ b/src/lib/server/trpc/routes/patient.ts @@ -31,7 +31,7 @@ export const patientRouter = t.router({ .query(async (opts) => getPatients( opts.input.filter ?? {}, opts.input.pagination ?? {}, - opts.input.sort ?? {}, + opts.input.sort ?? [], )), create: t.procedure .input(ZPatientNew) diff --git a/src/lib/shared/model/requests.ts b/src/lib/shared/model/requests.ts index 39d7086..f231f02 100644 --- a/src/lib/shared/model/requests.ts +++ b/src/lib/shared/model/requests.ts @@ -1,7 +1,7 @@ export type EntityQuery = Partial<{ filter: EntriesFilter | PatientsFilter; pagination: PaginationRequest; - sort: SortRequest; + sort: string[]; }>; export type PaginationRequest = Partial<{ @@ -9,11 +9,6 @@ export type PaginationRequest = Partial<{ offset: number | undefined; }>; -export type SortRequest = Partial<{ - field: string; - asc: boolean; -}>; - export type FilterList = T | T[] | { id: T; name?: string }[]; export type EntriesFilter = Partial<{ diff --git a/src/lib/shared/model/validation.ts b/src/lib/shared/model/validation.ts index 7a402c9..06c8f7a 100644 --- a/src/lib/shared/model/validation.ts +++ b/src/lib/shared/model/validation.ts @@ -123,11 +123,6 @@ export const ZPagination = implement().with({ offset: coercedUint.optional(), }); -export const ZSort = z.object({ - field: z.string().optional(), - asc: coercedBool.optional(), -}); - const ZFilterListEntry = z.object({ id: coercedUint, name: fields.NameString().optional(), @@ -137,7 +132,7 @@ const paginatedQuery = (f: T) => z .object({ filter: f, pagination: ZPagination, - sort: ZSort, + sort: z.array(z.string()), }) .partial(); diff --git a/src/lib/shared/util/util.test.ts b/src/lib/shared/util/util.test.ts index 247ec79..f94ce78 100644 --- a/src/lib/shared/util/util.test.ts +++ b/src/lib/shared/util/util.test.ts @@ -16,7 +16,7 @@ it("getQueryUrl", () => { search: "Hello World", }, pagination: { limit: 10, offset: 20 }, - sort: { field: "room", asc: true }, + sort: ["room"], }; const queryUrl = getQueryUrl(query, ""); diff --git a/src/routes/(app)/visit/+page.ts b/src/routes/(app)/visit/+page.ts index c6b99cc..b3c727a 100644 --- a/src/routes/(app)/visit/+page.ts +++ b/src/routes/(app)/visit/+page.ts @@ -22,7 +22,7 @@ export const load: PageLoad = async (event) => { // Sort entries by date if (!query.sort) { - query.sort = { field: "date" }; + query.sort = ["priority:dsc", "date"]; } const entries = await trpc(event).entry.list.query(query); diff --git a/tests/integration/query/entry.ts b/tests/integration/query/entry.ts index d5932ce..78c87d4 100644 --- a/tests/integration/query/entry.ts +++ b/tests/integration/query/entry.ts @@ -246,8 +246,8 @@ test("get entries", async () => { const entriesPatient = await getEntries({ patient: 1 }, {}); expect(entriesPatient.items).length(2); expect(entriesPatient.total).toBe(2); - expect(entriesPatient.items[0].id).toBe(eId3); - expect(entriesPatient.items[1].id).toBe(eId1); + expect(entriesPatient.items[0].id).toBe(eId1); + expect(entriesPatient.items[1].id).toBe(eId3); // Filter by room const entriesRoom = await getEntries({ room: 1 }, {}); @@ -257,8 +257,8 @@ test("get entries", async () => { const entriesDone = await getEntries({ done: true }, {}); expect(entriesDone.items).length(2); expect(entriesDone.total).toBe(2); - expect(entriesDone.items[0].id).toBe(eId3); - expect(entriesDone.items[1].id).toBe(eId2); + expect(entriesDone.items[0].id).toBe(eId2); + expect(entriesDone.items[1].id).toBe(eId3); // Filter not done const entriesNotDone = await getEntries({ done: false }, {}); @@ -281,8 +281,8 @@ test("get entries", async () => { const entriesDateRange = await getEntries({ date: "2024-01-05..2024-01-06" }); expect(entriesDateRange.items).length(2); expect(entriesDateRange.total).toBe(2); - expect(entriesDateRange.items[0].id).toBe(eId3); - expect(entriesDateRange.items[1].id).toBe(eId2); + expect(entriesDateRange.items[0].id).toBe(eId2); + expect(entriesDateRange.items[1].id).toBe(eId3); // Search const entriesSearch = await getEntries({ search: "Blu" }, {}); @@ -293,6 +293,30 @@ test("get entries", async () => { // NTodo const n = await getNTodo(new Date("2024-01-05")); expect(n).toBe(1); + + // Sort by ID + const entriesSortedId = await getEntries({}, {}, ["id"]); + const entriesSortedIdDsc = await getEntries({}, {}, ["id:dsc"]); + expect(entriesSortedId.total).toBe(3); + expect(entriesSortedIdDsc.total).toBe(3); + for (let i = 0; i < 3; i++) { + expect(entriesSortedId.items[i]).toStrictEqual( + entriesSortedIdDsc.items[entriesSortedIdDsc.items.length - i - 1], + ); + } + + // Sort by patient and ID + const entriesSortedPatientId = await getEntries({}, {}, ["patient", "id"]); + expect(entriesSortedPatientId.items.length).toBe(3); + expect(entriesSortedPatientId.items[0].id).toBe(eId1); + expect(entriesSortedPatientId.items[1].id).toBe(eId3); + expect(entriesSortedPatientId.items[2].id).toBe(eId2); + + const entriesSortedPatientIdDsc = await getEntries({}, {}, ["patient:dsc", "id"]); + expect(entriesSortedPatientIdDsc.items.length).toBe(3); + expect(entriesSortedPatientIdDsc.items[0].id).toBe(eId2); + expect(entriesSortedPatientIdDsc.items[1].id).toBe(eId1); + expect(entriesSortedPatientIdDsc.items[2].id).toBe(eId3); }); test("get patient n entries", async () => { diff --git a/tests/integration/query/patient.ts b/tests/integration/query/patient.ts index e2633de..f039870 100644 --- a/tests/integration/query/patient.ts +++ b/tests/integration/query/patient.ts @@ -11,7 +11,7 @@ import { } from "$lib/server/query"; import { S1, S2 } from "$tests/helpers/testdata"; -const SORT_ID = { field: "id" }; +const SORT_ID = ["id"]; test("create patient", async () => { const pId = await newPatient({ From 6c338e447e886f45f371103a10e0ae8c94199c76 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 11 May 2024 03:02:12 +0200 Subject: [PATCH 4/4] feat: replace carta-md with a fork fix: remove auto-trim from Markdown input --- package.json | 9 +-- patches/carta-md@4.0.2.patch | 27 --------- pnpm-lock.yaml | 59 ++++++++----------- .../components/ui/markdown/Markdown.svelte | 2 +- .../ui/markdown/MarkdownInput.svelte | 4 +- src/lib/components/ui/markdown/carta.pcss | 4 +- src/lib/components/ui/markdown/carta.ts | 2 +- src/lib/shared/model/validation.ts | 4 +- 8 files changed, 35 insertions(+), 76 deletions(-) delete mode 100644 patches/carta-md@4.0.2.patch diff --git a/package.json b/package.json index 2df97c1..9ba37e9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@floating-ui/core": "^1.6.1", "@mdi/js": "^7.4.47", "@prisma/client": "^5.13.0", - "carta-md": "4.0.2", + "@thetadev/carta-md": "^1.0.1", "diff": "^5.2.0", "isomorphic-dompurify": "^2.9.0", "prisma": "^5.13.0", @@ -71,10 +71,5 @@ "vite": "^5.2.11", "vitest": "^1.6.0" }, - "type": "module", - "pnpm": { - "patchedDependencies": { - "carta-md@4.0.2": "patches/carta-md@4.0.2.patch" - } - } + "type": "module" } diff --git a/patches/carta-md@4.0.2.patch b/patches/carta-md@4.0.2.patch deleted file mode 100644 index 2b8589e..0000000 --- a/patches/carta-md@4.0.2.patch +++ /dev/null @@ -1,27 +0,0 @@ -# Change "markdown-body" css class to "prose" for Tailwind compatibility -diff --git a/dist/Markdown.svelte b/dist/Markdown.svelte -index 92b29fade303a14539720b9bc389e7a41202b1cf..cbdeede16d7af17a481bcac519aea92b8959803d 100644 ---- a/dist/Markdown.svelte -+++ b/dist/Markdown.svelte -@@ -15,7 +15,7 @@ onMount(async () => { - }); - - --
-+
- - {@html rendered} - {#if mounted} -diff --git a/dist/internal/components/Renderer.svelte b/dist/internal/components/Renderer.svelte -index 1d2ff1a6937bc490ea1e6eb5c2ef9f3b33e4c326..6a95c154ea4ca7c4d19b20d02b3504fb2b65b7f7 100644 ---- a/dist/internal/components/Renderer.svelte -+++ b/dist/internal/components/Renderer.svelte -@@ -17,7 +17,7 @@ onMount(() => carta.$setRenderer(elem)); - onMount(() => mounted = true); - - --
-+
- - {@html renderedHtml} - {#if mounted} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5a6cc0..23cae13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,11 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - carta-md@4.0.2: - hash: i33ea43vfgrg3ziu25cfu7s2zq - path: patches/carta-md@4.0.2.patch - dependencies: '@auth/core': specifier: ^0.30.0 @@ -22,9 +17,9 @@ dependencies: '@prisma/client': specifier: ^5.13.0 version: 5.13.0(prisma@5.13.0) - carta-md: - specifier: 4.0.2 - version: 4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15) + '@thetadev/carta-md': + specifier: ^1.0.1 + version: 1.0.1(svelte@4.2.15) diff: specifier: ^5.2.0 version: 5.2.0 @@ -889,10 +884,6 @@ packages: dev: true optional: true - /@shikijs/core@1.4.0: - resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==} - dev: false - /@sideway/address@4.1.5: resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} requiresBuild: true @@ -1088,6 +1079,22 @@ packages: tailwindcss: 3.4.3 dev: true + /@thetadev/carta-md@1.0.1(svelte@4.2.15): + resolution: {integrity: sha512-B6TyB5gvrc1T5pEOM5nIZA+N6gG7EK7AJBrQCfokzrpNarq7CvgJEVaQoWh5oukB2MrRYSIQAYUNIoqEKzMSag==} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + dependencies: + prismjs: 1.29.0 + rehype-stringify: 10.0.0 + remark-gfm: 4.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.0 + svelte: 4.2.15 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + dev: false + /@trpc/client@10.45.2(@trpc/server@10.45.2): resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==} peerDependencies: @@ -1757,23 +1764,6 @@ packages: resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==} dev: true - /carta-md@4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15): - resolution: {integrity: sha512-wMlw0r5RZiVwvF3dyxE/vHj9pXlXbzpijJ3m/o9zqZe7Cf6D96AjyBHBpa0A0OPj/uEJVF3k0R6ctopBJCpCQg==} - peerDependencies: - svelte: ^3.54.0 || ^4.0.0 - dependencies: - rehype-stringify: 10.0.0 - remark-gfm: 4.0.0 - remark-parse: 11.0.0 - remark-rehype: 11.1.0 - shiki: 1.4.0 - svelte: 4.2.15 - unified: 11.0.4 - transitivePeerDependencies: - - supports-color - dev: false - patched: true - /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: false @@ -4424,6 +4414,11 @@ packages: '@prisma/engines': 5.13.0 dev: false + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + /property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} requiresBuild: true @@ -4721,12 +4716,6 @@ packages: engines: {node: '>=8'} dev: true - /shiki@1.4.0: - resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==} - dependencies: - '@shikijs/core': 1.4.0 - dev: false - /side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} diff --git a/src/lib/components/ui/markdown/Markdown.svelte b/src/lib/components/ui/markdown/Markdown.svelte index 6562f05..d67cd3e 100644 --- a/src/lib/components/ui/markdown/Markdown.svelte +++ b/src/lib/components/ui/markdown/Markdown.svelte @@ -1,5 +1,5 @@