Compare commits

..

No commits in common. "v0.3.4" and "v0.3.3" have entirely different histories.

15 changed files with 51 additions and 87 deletions

View file

@ -50,7 +50,6 @@ jobs:
id: e2etest id: e2etest
run: | run: |
pnpm run build -l silent pnpm run build -l silent
npx playwright install chromium
pnpm run test:e2e pnpm run test:e2e
- name: 💢 Upload E2E report - name: 💢 Upload E2E report
if: ${{ failure() && steps.e2etest.conclusion == 'failure' }} if: ${{ failure() && steps.e2etest.conclusion == 'failure' }}
@ -68,7 +67,7 @@ jobs:
- name: 👁️ Checkout repository - name: 👁️ Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 # important to fetch tag logs fetch-depth: 0
- name: 📦 pnpm install - name: 📦 pnpm install
run: pnpm install run: pnpm install

View file

@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [v0.3.4](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.3..v0.3.4) - 2024-05-16
### 🚀 Features
- Select table entries on doubleclick - ([c6abf63](https://code.thetadev.de/HSA/Visitenbuch/commit/c6abf633f8ae5e9b562dda36f9f7ab4d6adcb4e1))
### 🐛 Bug Fixes
- Escape HTML for licenses file - ([f76e7fd](https://code.thetadev.de/HSA/Visitenbuch/commit/f76e7fd97f62d9b41ecbabc3334c2c1876be253d))
- Use btn-id class for all tables - ([d5e9a94](https://code.thetadev.de/HSA/Visitenbuch/commit/d5e9a9469f0c57939367141985a97d8404fd6fbe))
- Avoid global state, use context for savedFilters - ([a4eebb9](https://code.thetadev.de/HSA/Visitenbuch/commit/a4eebb944f55da8e87cc899eebada0bd3fd37aa8))
- Close autocomplete on defocus - ([4a3155c](https://code.thetadev.de/HSA/Visitenbuch/commit/4a3155c33aa354973d4e0ca3ffeab2b7fd442040))
- Remove process.on hooks (not necessary) - ([cdb3446](https://code.thetadev.de/HSA/Visitenbuch/commit/cdb344609cde80084876faea9f80e7b26b01d0f2))
- Autocomplete not closing on tab - ([88a5040](https://code.thetadev.de/HSA/Visitenbuch/commit/88a5040f9c4e19ae3efb5ad0894c8dc5b905a92e))
## [v0.3.3](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.2..v0.3.3) - 2024-05-14 ## [v0.3.3](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.2..v0.3.3) - 2024-05-14
### 🚀 Features ### 🚀 Features

View file

@ -1,6 +1,6 @@
{ {
"name": "visitenbuch", "name": "visitenbuch",
"version": "0.3.4", "version": "0.3.3",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View file

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="de"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />

View file

@ -31,3 +31,7 @@ export const handle = sequence(
authorization, authorization,
createTRPCHandle({ router, createContext }), createTRPCHandle({ router, createContext }),
); );
// Allow server application to exit
process.on("SIGINT", () => process.exit()); // Ctrl+C
process.on("SIGTERM", () => process.exit()); // docker stop

View file

@ -166,14 +166,6 @@
if (opened) { if (opened) {
onClose(kb); onClose(kb);
} }
// select remaining item if autoselect is enabled
if (!selection) {
if (!noAutoselect1 && filteredItems.length === 1) {
selectListItem(filteredItems[0], true);
} else {
setInputValue("");
}
}
opened = false; opened = false;
} }
@ -193,38 +185,43 @@
} }
function onKeyDown(e: KeyboardEvent): void { function onKeyDown(e: KeyboardEvent): void {
switch (e.key) { let { key } = e;
case "Tab": if (key === "Tab" && e.shiftKey) key = "ShiftTab";
close(); const fnmap: Record<string, () => void> = {
break; Tab: () => close,
case "ArrowDown": ShiftTab: () => close,
ArrowDown: () => {
open(); open();
if (highlightIndex < filteredItems.length - 1) { if (highlightIndex < filteredItems.length - 1) {
highlightIndex++; highlightIndex++;
highlight(); highlight();
} }
break; },
case "ArrowUp": ArrowUp: () => {
open(); open();
if (highlightIndex > 0) { if (highlightIndex > 0) {
highlightIndex--; highlightIndex--;
highlight(); highlight();
} }
break; },
case "Escape": Escape: () => {
e.stopPropagation(); e.stopPropagation();
if (opened) { if (opened) {
if (inputElm) inputElm.focus(); if (inputElm) inputElm.focus();
close(); close();
} }
break; },
case "Backspace": Backspace: () => {
if (inputValue().length === 0) { if (inputValue().length === 0) {
onBackspace(); onBackspace();
} else if (selection) { } else if (selection) {
clearSelection(); clearSelection();
} }
break; },
};
const fn = fnmap[key];
if (typeof fn === "function") {
fn();
} }
} }
@ -237,6 +234,16 @@
} }
} }
function onBlur(): void {
if (!selection) {
if (!noAutoselect1 && filteredItems.length === 1) {
selectListItem(filteredItems[0], true);
} else {
setInputValue("");
}
}
}
function highlight(): void { function highlight(): void {
if (browser && opened) { if (browser && opened) {
window.setTimeout(() => { window.setTimeout(() => {
@ -296,11 +303,12 @@
on:focus={open} on:focus={open}
on:keydown={onKeyDown} on:keydown={onKeyDown}
on:keypress={onKeyPress} on:keypress={onKeyPress}
on:blur={onBlur}
use:floatingRef use:floatingRef
/> />
{#if opened && filteredItems.length > 0} {#if opened && filteredItems.length > 0}
<div bind:this={listElm} class="autocomplete-list" tabindex="-1" use:floatingContent> <div bind:this={listElm} class="autocomplete-list" use:floatingContent>
{#each filteredItems as item, i} {#each filteredItems as item, i}
<div <div
class="autocomplete-list-item" class="autocomplete-list-item"
@ -336,12 +344,7 @@
{#if clearBtn && selection} {#if clearBtn && selection}
<div class="absolute bottom-0 right-0 h-full flex items-center"> <div class="absolute bottom-0 right-0 h-full flex items-center">
<IconButton <IconButton cls="" path={mdiClose} title="Löschen" on:click={clearSelection} />
cls=""
path={mdiClose}
tabindex={-1}
title="Löschen"
on:click={clearSelection} />
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -8,14 +8,12 @@
import { toastError, toastInfo } from "$lib/shared/util/toast"; import { toastError, toastInfo } from "$lib/shared/util/toast";
import Icon from "$lib/components/ui/Icon.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
import { getSavedFilters } from "$lib/stores"; import { savedFilters } from "$lib/stores";
import Chip from "./SavedFilterChip.svelte"; import Chip from "./SavedFilterChip.svelte";
export let view: string; export let view: string;
const savedFilters = getSavedFilters();
$: filters = $savedFilters[view] ?? []; $: filters = $savedFilters[view] ?? [];
function getQuery(): string { function getQuery(): string {

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import type { RouterOutput } from "$lib/shared/trpc"; import type { RouterOutput } from "$lib/shared/trpc";
import { formatDate } from "$lib/shared/util"; import { formatDate } from "$lib/shared/util";
@ -41,7 +39,6 @@
class="transition-colors hover:bg-neutral-content/10" class="transition-colors hover:bg-neutral-content/10"
class:done={entry.execution?.done} class:done={entry.execution?.done}
class:priority={entry.current_version.priority} class:priority={entry.current_version.priority}
on:dblclick={() => { void goto("/entry/" + entry.id); }}
> >
<td <td
><a ><a

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { mdiFilter } from "@mdi/js"; import { mdiFilter } from "@mdi/js";
import { URL_ENTRIES } from "$lib/shared/constants"; import { URL_ENTRIES } from "$lib/shared/constants";
@ -36,11 +34,10 @@
<tr <tr
class="transition-colors hover:bg-neutral-content/10" class="transition-colors hover:bg-neutral-content/10"
class:p-hidden={patient.hidden} class:p-hidden={patient.hidden}
on:dblclick={() => { void goto("/patient/" + patient.id); }}
> >
<td <td
><a ><a
class="btn btn-xs btn-primary btn-id" class="btn btn-xs btn-primary"
aria-label="Eintrag anzeigen" aria-label="Eintrag anzeigen"
href="/patient/{patient.id}">{patient.id}</a href="/patient/{patient.id}">{patient.id}</a
></td ></td

View file

@ -1,4 +1,3 @@
import { getContext } from "svelte";
import { derived, writable, type Writable } from "svelte/store"; import { derived, writable, type Writable } from "svelte/store";
import type { SavedFilter } from "$lib/shared/model"; import type { SavedFilter } from "$lib/shared/model";
@ -7,6 +6,4 @@ import type { SavedFilter } from "$lib/shared/model";
export const screenWidth = writable(0); export const screenWidth = writable(0);
export const screenWidthSmall = derived(screenWidth, ($mainWidth) => $mainWidth < 500); export const screenWidthSmall = derived(screenWidth, ($mainWidth) => $mainWidth < 500);
// Context key: "savedFilters" export const savedFilters: Writable<Record<string, SavedFilter[]>> = writable({});
export type SavedFilters = Writable<Record<string, SavedFilter[]>>;
export const getSavedFilters: () => SavedFilters = () => getContext("savedFilters");

View file

@ -3,22 +3,16 @@
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { mdiAccount, mdiHome } from "@mdi/js"; import { mdiAccount, mdiHome } from "@mdi/js";
import { setContext } from "svelte";
import { writable } from "svelte/store";
import { defaultFilterUrl, defaultVisitUrl } from "$lib/shared/util"; import { defaultFilterUrl, defaultVisitUrl } from "$lib/shared/util";
import Icon from "$lib/components/ui/Icon.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
import NavLink from "$lib/components/ui/NavLink.svelte"; import NavLink from "$lib/components/ui/NavLink.svelte";
import type { SavedFilters } from "$lib/stores"; import { savedFilters } from "$lib/stores";
export let data: LayoutData; export let data: LayoutData;
const savedFilters: SavedFilters = writable();
$: savedFilters.set(data.savedFilters); $: savedFilters.set(data.savedFilters);
setContext("savedFilters", savedFilters);
</script> </script>
<div class="navbar-outer"> <div class="navbar-outer">

View file

@ -4,11 +4,9 @@
import { defaultFilterUrl } from "$lib/shared/util"; import { defaultFilterUrl } from "$lib/shared/util";
import { getSavedFilters } from "$lib/stores"; import { savedFilters } from "$lib/stores";
export let data: PageData; export let data: PageData;
const savedFilters = getSavedFilters();
</script> </script>
<svelte:head> <svelte:head>

View file

@ -27,7 +27,7 @@
<tbody> <tbody>
{#each data.rooms as room (room.id)} {#each data.rooms as room (room.id)}
<tr> <tr>
<td><a class="btn btn-sm btn-id" href="/room/{room.id}">{room.name}</a></td> <td><a class="btn btn-sm" href="/room/{room.id}">{room.name}</a></td>
<td>{room.station.name}</td> <td>{room.station.name}</td>
</tr> </tr>
{/each} {/each}

View file

@ -27,7 +27,7 @@
{#each data.stations as station (station.id)} {#each data.stations as station (station.id)}
<tr> <tr>
<td> <td>
<a class="btn btn-sm btn-id" href="/station/{station.id}">{station.name}</a> <a class="btn btn-sm" href="/station/{station.id}">{station.name}</a>
</td> </td>
</tr> </tr>
{/each} {/each}

View file

@ -23,8 +23,7 @@ export default defineConfig({
createViteLicensePlugin({ createViteLicensePlugin({
additionalFiles: { additionalFiles: {
"oss-licenses.html": (packages) => { "oss-licenses.html": (packages) => {
let res = `<!DOCTYPE html> let res = `<html>
<html lang="en">
<head> <head>
<title>Visitenbuch - Lizenzen</title> <title>Visitenbuch - Lizenzen</title>
</head> </head>
@ -32,12 +31,6 @@ export default defineConfig({
<h1>Open-Source-Lizenzen</h1> <h1>Open-Source-Lizenzen</h1>
<a href="./oss-licenses.json">JSON-formatted license list</a> <a href="./oss-licenses.json">JSON-formatted license list</a>
`; `;
const escapeHTML = (s: string | null) => s ? s.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#x27;") : "";
for (const _p of packages) { for (const _p of packages) {
type LicenseMetaExt = LicenseMeta & { type LicenseMetaExt = LicenseMeta & {
repository: string | null, repository: string | null,
@ -61,13 +54,13 @@ export default defineConfig({
} }
res += `<div class="package">\n`; res += `<div class="package">\n`;
res += `<h3><a href="https://www.npmjs.com/package/${escapeHTML(p.name)}" target="_blank" rel="noopener noreferrer">${escapeHTML(p.name)}</a></h3>\n`; res += `<h3><a href="https://www.npmjs.com/package/${p.name}" target="_blank" rel="noopener noreferrer">${p.name}</a></h3>\n`;
res += `<table>\n`; res += `<table>\n`;
res += `<tr><td>Version:</td><td>${escapeHTML(p.version)}</td></tr>\n`; res += `<tr><td>Version:</td><td>${p.version}</td></tr>\n`;
if (aut) res += `<tr><td>Author:</td><td>${escapeHTML(aut)}</td></tr>\n`; if (aut) res += `<tr><td>Author:</td><td>${aut}</td></tr>\n`;
res += `<tr><td>License:</td><td>${escapeHTML(p.license)}</td></tr>\n`; res += `<tr><td>License:</td><td>${p.license}</td></tr>\n`;
if (repoUrl) res += `<tr><td>Repository:</td><td><a href="${escapeHTML(repoUrl)}" target="_blank" rel="noopener noreferrer">${escapeHTML(repoUrl)}</a></td></tr>\n`; if (repoUrl) res += `<tr><td>Repository:</td><td><a href="${repoUrl}" target="_blank" rel="noopener noreferrer">${repoUrl}</a></td></tr>\n`;
else if (rp) res += `<tr><td>Repository:</td><td>${escapeHTML(rp)}</td></tr>\n`; else if (rp) res += `<tr><td>Repository:</td><td>${rp}</td></tr>\n`;
res += `</table>\n`; res += `</table>\n`;
res += "</div>"; res += "</div>";
} }