Compare commits
No commits in common. "04d9883c9655379301e0c41cc55ebdaa90c68821" and "d7ecac657fde01bee34c919c900d2e69614ef69a" have entirely different histories.
04d9883c96
...
d7ecac657f
55 changed files with 104 additions and 406 deletions
|
@ -4,8 +4,6 @@ node_modules
|
|||
/package
|
||||
.env
|
||||
.env.*
|
||||
.eslintcache
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
vitest.config.*.js.timestamp-*
|
||||
vitest.config.*.ts.timestamp-*
|
||||
|
|
|
@ -16,19 +16,9 @@ jobs:
|
|||
env:
|
||||
POSTGRES_DB: "test"
|
||||
POSTGRES_PASSWORD: "1234"
|
||||
oidc:
|
||||
image: thetadev256/oidc-mock-server
|
||||
env:
|
||||
CLIENT_ID: visitenbuch
|
||||
CLIENT_SECRET: supersecret
|
||||
CLIENT_REDIRECT_URIS: http://localhost:4173/auth/callback/keycloak
|
||||
CLIENT_LOGOUT_REDIRECT_URIS: http://localhost:4173/login?noAuto=1
|
||||
ISSUER_HOST: oidc:3000
|
||||
env:
|
||||
DATABASE_URL: "postgresql://postgres:1234@postgres:5432/test?schema=public"
|
||||
TEST_DATABASE_URL: "postgresql://postgres:1234@postgres:5432/test?schema=public"
|
||||
KEYCLOAK_ISSUER: http://oidc:3000
|
||||
KEYCLOAK_LOGOUT: http://oidc:3000/session/end
|
||||
steps:
|
||||
- name: 👁️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
@ -46,11 +36,6 @@ jobs:
|
|||
run: |
|
||||
npx prisma migrate reset --force
|
||||
npm run test:integration
|
||||
- name: 👨🔬 E2E test
|
||||
run: |
|
||||
npx playwright install --with-deps
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
|
||||
release:
|
||||
runs-on: cimaster-latest
|
||||
|
@ -60,8 +45,6 @@ jobs:
|
|||
steps:
|
||||
- name: 👁️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 📦 pnpm install
|
||||
run: pnpm install
|
||||
|
@ -82,7 +65,7 @@ jobs:
|
|||
run: |
|
||||
{
|
||||
echo 'CHANGELOG<<END_OF_FILE'
|
||||
git show -s --format=%N "${{ github.ref }}" | tail -n +4 | awk 'BEGIN{RS="-----BEGIN PGP SIGNATURE-----"} NR==1{printf $0}'
|
||||
git show -s --format=%N "${{ github.ref_name }}" | tail -n +4 | awk 'BEGIN{RS="-----BEGIN PGP SIGNATURE-----"} NR==1{printf $0}'
|
||||
echo END_OF_FILE
|
||||
} >> "$GITHUB_ENV"
|
||||
- name: 🎉 Publish release
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,8 +4,5 @@ node_modules
|
|||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.eslintcache
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
vitest.config.*.js.timestamp-*
|
||||
vitest.config.*.ts.timestamp-*
|
||||
|
|
|
@ -14,7 +14,7 @@ import ts from "typescript-eslint";
|
|||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommendedTypeChecked,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs["flat/recommended"],
|
||||
// TS-Svelte
|
||||
{
|
||||
|
@ -585,16 +585,10 @@ export default [
|
|||
},
|
||||
],
|
||||
"@typescript-eslint/return-await": "error",
|
||||
|
||||
"@typescript-eslint/no-shadow": ["error", { allow: ["i", "j"] }],
|
||||
"no-shadow-restricted-names": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/no-base-to-string": "off",
|
||||
|
||||
// these clash with Svelte generics
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-loss-of-precision": "error",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -818,7 +812,7 @@ export default [
|
|||
".svelte-kit/",
|
||||
"*.config.cjs",
|
||||
"vite.config.js.timestamp-*",
|
||||
"vitest.config.*.timestamp-*",
|
||||
"vite.config.ts.timestamp-*",
|
||||
".tmp/",
|
||||
],
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"test": "vitest --run && vitest --config vitest.config.integration.js --run",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint . --max-warnings=0 --cache",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"format": "eslint . --fix",
|
||||
"test:unit": "vitest",
|
||||
"test:integration": "vitest --config vitest.config.integration.js",
|
||||
|
|
|
@ -2,12 +2,9 @@ import { defineConfig } from "@playwright/test";
|
|||
|
||||
export default defineConfig({
|
||||
webServer: {
|
||||
command: "npm run preview -m test",
|
||||
command: "npm run build && npm run preview",
|
||||
port: 4173,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
testDir: "tests/e2e",
|
||||
testMatch: /\.[jt]s$/,
|
||||
globalSetup: "tests/helpers/generate-mockdata.ts",
|
||||
outputDir: ".svelte-kit/test-results",
|
||||
testDir: "tests",
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||
});
|
||||
|
|
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
@ -8,9 +8,7 @@ declare global {
|
|||
interface Locals {
|
||||
session: Session | null;
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
|
|
79
src/app.pcss
79
src/app.pcss
|
@ -43,7 +43,6 @@
|
|||
|
||||
button {
|
||||
text-align: initial;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
@ -73,6 +72,10 @@ button {
|
|||
|
||||
.row {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.row,
|
||||
.rowb {
|
||||
@apply p-2;
|
||||
@apply border-t border-solid border-base-content/30;
|
||||
}
|
||||
|
@ -106,77 +109,3 @@ button {
|
|||
transform: scale(0.99);
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* Make all text black for best look on B/W printers */
|
||||
:root {
|
||||
--bc: 0% 0 0;
|
||||
--pc: 0% 0 0;
|
||||
--sc: 0% 0 0;
|
||||
--ac: 0% 0 0;
|
||||
--nc: 0% 0 0;
|
||||
--inc: 0% 0 0;
|
||||
--suc: 0% 0 0;
|
||||
--wac: 0% 0 0;
|
||||
--erc: 0% 0 0;
|
||||
}
|
||||
|
||||
.btn:not(.btn-id) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-id {
|
||||
font-size: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
@apply rounded-none;
|
||||
}
|
||||
|
||||
.card2 {
|
||||
@apply rounded-none;
|
||||
background: none !important;
|
||||
page-break-inside: avoid;
|
||||
|
||||
.row:first-child {
|
||||
@apply border-solid border-base-content/50 border-t-0 border-b-2;
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.row:last-child,
|
||||
.row:first-child {
|
||||
@apply rounded-none;
|
||||
}
|
||||
|
||||
.c-light,
|
||||
.c-vlight,
|
||||
.c-primary {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card2,
|
||||
.card2 > .row {
|
||||
@apply border-none;
|
||||
}
|
||||
|
||||
.table {
|
||||
td, th {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
@apply border-base-content/50 border;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: transparent !important;
|
||||
color: #000 !important;
|
||||
padding: 0;
|
||||
@apply rounded-none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TRPCClientError } from "@trpc/client";
|
|||
|
||||
const CHECK_CONNECTION = "Die Seite konnte nicht geladen werden, prüfen sie ihre Verbindung";
|
||||
|
||||
export const handleError: HandleClientError = ({ error, message, status }) => {
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
// If there are client-side errors, SvelteKit always returns the nondescript
|
||||
// "Internal error" message. The most common errors should be mapped to a more
|
||||
// detailed description
|
||||
|
|
|
@ -33,5 +33,5 @@ export const handle = sequence(
|
|||
);
|
||||
|
||||
// Allow server application to exit
|
||||
process.on("SIGINT", () => process.exit()); // Ctrl+C
|
||||
process.on("SIGTERM", () => process.exit()); // docker stop
|
||||
process.on("SIGINT", process.exit); // Ctrl+C
|
||||
process.on("SIGTERM", process.exit); // docker stop
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<div class="row">
|
||||
<Markdown src={entry.current_version.text} />
|
||||
</div>
|
||||
<div class="row c-vlight text-sm block">
|
||||
<div class="rowb c-vlight text-sm">
|
||||
Zuletzt bearbeitet am {formatDate(entry.current_version.created_at, true)} von
|
||||
<UserField filterName="author" user={entry.current_version.author} />
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script generics="T extends BaseItem" lang="ts">
|
||||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { MaybePromise } from "@sveltejs/kit";
|
||||
import { onMount } from "svelte";
|
||||
import { createFloatingActions } from "svelte-floating-ui";
|
||||
import { shift } from "svelte-floating-ui/dom";
|
||||
|
@ -23,8 +22,10 @@
|
|||
* MIT License
|
||||
*/
|
||||
|
||||
type T = $$Generic<BaseItem>;
|
||||
|
||||
/** List of items to choose from (or an async function fetching them) */
|
||||
export let items: T[] | (() => MaybePromise<T[]>);
|
||||
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 */
|
||||
|
@ -95,7 +96,7 @@
|
|||
return false;
|
||||
} else {
|
||||
isLoading = true;
|
||||
Promise.resolve(items()).then((fetchedItems) => {
|
||||
items().then((fetchedItems) => {
|
||||
srcItems = fetchedItems;
|
||||
if (cacheKey) cache[cacheKey] = fetchedItems;
|
||||
isLoading = false;
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
// Filter menu items to be hidden
|
||||
$: hiddenIds = new Set([
|
||||
...Object.values(FILTERS).flatMap((f) => {
|
||||
return f.inputType === InputType.FilterList
|
||||
return f.inputType === 2
|
||||
|| activeFilters.every((af) => af.id !== f.id)
|
||||
? []
|
||||
: [f.id];
|
||||
|
@ -96,15 +96,10 @@
|
|||
activeFilters = filters;
|
||||
}
|
||||
|
||||
/** Get a list of item IDs to hide for the given filter
|
||||
* This ensures that a filter item cannot be selected twice
|
||||
*/
|
||||
function getHiddenIds(fid: string, fpos: number): Set<string | number> {
|
||||
if (FILTERS[fid].inputType === InputType.FilterList) {
|
||||
if (FILTERS[fid].inputType === 2) {
|
||||
return new Set(
|
||||
activeFilters.flatMap((f, i) => {
|
||||
return i !== fpos && f.id === fid && f.selection?.id ? [f.selection?.id] : [];
|
||||
}),
|
||||
activeFilters.flatMap((f, i) => (i !== fpos && f.selection?.id ? [f.selection?.id] : [])),
|
||||
);
|
||||
}
|
||||
return new Set();
|
||||
|
@ -135,7 +130,7 @@
|
|||
}
|
||||
|
||||
if (val !== null && val !== undefined) {
|
||||
if (filter.inputType === InputType.FilterList) {
|
||||
if (filter.inputType === 2) {
|
||||
// @ts-expect-error fd[key] is checked
|
||||
if (Array.isArray(fd[key])) fd[key].push(...val);
|
||||
else fd[key] = val;
|
||||
|
@ -158,7 +153,7 @@
|
|||
const valueless = isFilterValueless(FILTERS[item.id].inputType);
|
||||
|
||||
let selection = null;
|
||||
if (FILTERS[item.id].inputType === InputType.Boolean) {
|
||||
if (FILTERS[item.id].inputType === 3) {
|
||||
selection = { toggle: item.toggle ?? true };
|
||||
}
|
||||
|
||||
|
@ -265,10 +260,4 @@
|
|||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.filterbar-outer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
? filter.name
|
||||
: filter.toggleOff?.name ?? filter.name + TOFF;
|
||||
$: filterIcon = toggleState ? filter.icon : filter.toggleOff?.icon ?? filter.icon;
|
||||
$: hasInputField = filter.inputType !== InputType.None && filter.inputType !== InputType.Boolean;
|
||||
$: hasInputField = filter.inputType !== 0 && filter.inputType !== 3;
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -61,7 +61,7 @@
|
|||
>
|
||||
<button
|
||||
class="flex items-center gap-1"
|
||||
disabled={filter.inputType !== InputType.Boolean}
|
||||
disabled={filter.inputType !== 3}
|
||||
on:click={() => {
|
||||
if (fdata.selection) {
|
||||
fdata.selection.toggle = !fdata.selection.toggle;
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="saved-filters">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-sm h-8 flex items-center">
|
||||
Gespeicherte Filter:
|
||||
</div>
|
||||
|
@ -97,15 +97,3 @@
|
|||
Neu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.saved-filters {
|
||||
@apply flex flex-wrap items-center gap-2;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.saved-filters {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -107,7 +107,7 @@ export const ENTRY_FILTERS: Record<string, FilterDef> = {
|
|||
name: "Datum",
|
||||
icon: mdiCalendar,
|
||||
inputType: InputType.FilterList,
|
||||
options: () => weekFilterItems(),
|
||||
options: async () => weekFilterItems(),
|
||||
textToItem: (s) => {
|
||||
const parsed = DateRange.parseHuman(s);
|
||||
if (parsed) {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import type { MaybePromise } from "@sveltejs/kit";
|
||||
|
||||
export enum InputType {
|
||||
None = 0,
|
||||
FreeText = 1,
|
||||
|
@ -29,7 +27,7 @@ export type FilterDef = {
|
|||
name: string;
|
||||
icon?: string;
|
||||
};
|
||||
options?: () => MaybePromise<BaseItem[]>;
|
||||
options?: () => Promise<BaseItem[]>;
|
||||
textToItem?: (s: string) => BaseItem | void;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</script>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table" data-testid="entry-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader key="id" {sortData} {sortUpdate} title="ID" />
|
||||
|
@ -42,7 +42,7 @@
|
|||
>
|
||||
<td
|
||||
><a
|
||||
class="btn btn-xs btn-primary btn-id"
|
||||
class="btn btn-xs btn-primary"
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/entry/{entry.id}">{entry.id}</a
|
||||
></td
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
// Update page URL
|
||||
const url = getQueryUrl(q, baseUrl);
|
||||
void goto(url, { replaceState: true, keepFocus: true });
|
||||
goto(url, { replaceState: true, keepFocus: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
if (browser) {
|
||||
// Update page URL
|
||||
const url = getQueryUrl(q, baseUrl);
|
||||
void goto(url, { replaceState: true, keepFocus: true });
|
||||
goto(url, { replaceState: true, keepFocus: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</script>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table" data-testid="patient-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader key="id" {sortData} {sortUpdate} title="ID" />
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
} else if (sorting === 2) {
|
||||
delete sortData[index];
|
||||
} else if (index !== -1) {
|
||||
sortData[index] = sortData[index].split(":", 1)[0] + ":dsc";
|
||||
sortData[index] = sortData[index].split(":", 1) + ":dsc";
|
||||
} else {
|
||||
sortData.push(key);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
export let cls = "";
|
||||
export let fixedTop = false;
|
||||
export let alwaysShown = false;
|
||||
|
||||
let navprogress = 0;
|
||||
|
@ -42,7 +41,6 @@
|
|||
class={cls}
|
||||
class:active={alwaysShown || showProgress}
|
||||
class:error={showError}
|
||||
class:loading-bar-top={fixedTop}
|
||||
/>
|
||||
|
||||
<style lang="postcss">
|
||||
|
@ -53,10 +51,6 @@
|
|||
@apply bg-primary;
|
||||
}
|
||||
|
||||
.loading-bar-top {
|
||||
@apply fixed top-0 left-0 z-50;
|
||||
}
|
||||
|
||||
.active {
|
||||
height: 0.2rem;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
{#if editing}
|
||||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
items={() => weekFilterItems()}
|
||||
items={async () => weekFilterItems()}
|
||||
noAutoselect1
|
||||
onClose={stopEditing}
|
||||
onSelect={(item) => {
|
||||
|
|
|
@ -107,7 +107,7 @@ export async function auth(event: RequestEvent): Promise<Session | null> {
|
|||
const { status = 200 } = response;
|
||||
const data = await response.json();
|
||||
if (!data || !Object.keys(data).length) return null;
|
||||
if (status === 200) return data as Session;
|
||||
if (status === 200) return data;
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ export function filterListToArray<T>(fl: FilterList<T>): T[] {
|
|||
// @ts-expect-error checked if id is present
|
||||
if (fl[0].id) {
|
||||
// @ts-expect-error checked if id is present
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return fl.map((itm) => itm.id);
|
||||
}
|
||||
// @ts-expect-error output type checked
|
||||
|
@ -201,7 +200,7 @@ export class QueryBuilder {
|
|||
}
|
||||
|
||||
/** Add a simple filter checking for equality */
|
||||
addFilter(fname: string, val: unknown): void {
|
||||
addFilter(fname: string, val: unknown | undefined): void {
|
||||
if (val === undefined) return;
|
||||
|
||||
this.params.push(val);
|
||||
|
@ -215,7 +214,7 @@ export class QueryBuilder {
|
|||
}
|
||||
|
||||
/** Add a list filter (value matches any item from the filter list) */
|
||||
addFilterList(fname: string, fl: FilterList<unknown>): void {
|
||||
addFilterList(fname: string, fl: FilterList<unknown> | undefined): void {
|
||||
if (fl === undefined) return;
|
||||
|
||||
this.filterClauses.push(`${fname} = any (${this.pvar()})`);
|
||||
|
|
|
@ -4,7 +4,9 @@ import { type inferAsyncReturnType, TRPCError } from "@trpc/server";
|
|||
import type { User } from "$lib/shared/model";
|
||||
import { ZUser } from "$lib/shared/model/validation";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
// we're not using the event parameter is this example,
|
||||
// hence the eslint-disable rule
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export async function createContext(event: RequestEvent): Promise<{ user: User }> {
|
||||
if (!event.locals.session?.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "not logged in" });
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
import { t } from ".";
|
||||
import { categoryRouter } from "./routes/category";
|
||||
import { entryRouter } from "./routes/entry";
|
||||
|
@ -8,6 +10,12 @@ import { stationRouter } from "./routes/station";
|
|||
import { userRouter } from "./routes/user";
|
||||
|
||||
export const router = t.router({
|
||||
greeting: t.procedure.query(
|
||||
async () => `Hello tRPC @ ${new Date().toLocaleTimeString()}`,
|
||||
),
|
||||
testError: t.procedure.query(async () => {
|
||||
throw new ErrorInvalidInput("here is your error");
|
||||
}),
|
||||
category: categoryRouter,
|
||||
entry: entryRouter,
|
||||
station: stationRouter,
|
||||
|
|
|
@ -227,7 +227,7 @@ export function shiftDateRange(dateRange: DateRange, week: boolean, fwd: boolean
|
|||
modDir = true;
|
||||
}
|
||||
if (dateRange.end === null) {
|
||||
dateRange.end = new Date(dateRange.start);
|
||||
dateRange.end = new Date(dateRange.start!);
|
||||
modDir = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,11 +38,11 @@ export function gotoEntityQuery(query: EntityQuery, basePath: string): void {
|
|||
filter: { ...oldQuery.filter, ...query.filter },
|
||||
sort: query.sort,
|
||||
};
|
||||
void goto(getQueryUrl(newQuery, basePath));
|
||||
goto(getQueryUrl(newQuery, basePath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
void goto(getQueryUrl(query, basePath));
|
||||
goto(getQueryUrl(query, basePath));
|
||||
}
|
||||
|
||||
/** Wrap a page load query to handle occuring errors
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
$: savedFilters.set(data.savedFilters);
|
||||
</script>
|
||||
|
||||
<div class="navbar-outer">
|
||||
<div
|
||||
class="sticky top-0 z-30 flex h-12 w-full
|
||||
justify-center bg-neutral text-neutral-content"
|
||||
>
|
||||
<nav class="navbar w-full min-h-12">
|
||||
<div class="flex flex-1">
|
||||
<NavLink
|
||||
|
@ -62,22 +65,6 @@
|
|||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<main>
|
||||
<div class="max-w-[100vw] p-4 pb-8 flex flex-col gap-4">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style lang="postcss">
|
||||
.navbar-outer {
|
||||
@apply sticky top-0 z-30 flex h-12 w-full justify-center bg-neutral text-neutral-content;
|
||||
}
|
||||
|
||||
main {
|
||||
@apply max-w-[100vw] p-4 pb-8 flex flex-col gap-4;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.navbar-outer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
|
|
@ -35,10 +35,7 @@
|
|||
<div class="card-body">
|
||||
<h2 class="card-title">Visite</h2>
|
||||
<p>Hier können sie Visitenbucheinträge abarbeiten.</p>
|
||||
<p>Heute müssen
|
||||
<span data-testid="n-entries-todo">{data.nTodo}</span>
|
||||
Einträge erledigt werden.
|
||||
</p>
|
||||
<p>Heute müssen {data.nTodo} Einträge erledigt werden.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a class="btn btn-primary" href="/visit">Visite</a>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</Header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table" data-testid="category-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Beschreibung</th>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<EntryBody entry={data.entry} withExecution />
|
||||
|
||||
{#if !data.entry.execution?.done}
|
||||
<form class="print:hidden" method="POST" use:enhance>
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if version.text.length > 0}
|
||||
<div class="row whitespace-pre-wrap block">
|
||||
<div class="rowb whitespace-pre-wrap">
|
||||
{#each version.text as change}
|
||||
<span class:added={change.added} class:removed={change.removed}>
|
||||
{change.value}
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<div class="max-w-[100vw] px-6 pb-16 xl:pr-2 text-center">
|
||||
<h1 class="text-xl mt-4">Möchten sie sich abmelden?</h1>
|
||||
<form method="POST" use:enhance>
|
||||
<button class="btn btn-primary mt-4" data-testid="btn-logout" type="submit">Abmelden</button>
|
||||
<button class="btn btn-primary mt-4" type="submit">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</Header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table" data-testid="room-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Station</th>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</Header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table" data-testid="stations-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
</thead>
|
||||
|
|
|
@ -77,21 +77,18 @@
|
|||
if (browser) {
|
||||
// Update page URL
|
||||
const url = getQueryUrl(q, URL_VISIT);
|
||||
void goto(url, { replaceState: true, keepFocus: true });
|
||||
goto(url, { replaceState: true, keepFocus: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Visite {dateRange.format()}</title>
|
||||
<title>Visite</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex gap-4 items-baseline">
|
||||
<h1 class="heading">Visite</h1>
|
||||
<span class="hidden print:block">{dateRange.format()}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between print:hidden">
|
||||
<div class="flex flex-wrap gap-2 justify-between">
|
||||
<div class="flex gap-2 items-center">
|
||||
<!-- <span>Zeitraum:</span> -->
|
||||
<WeekSelector onSelect={filterUpdate} bind:dateRange />
|
||||
|
@ -121,7 +118,7 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
{#each data.groups as group}
|
||||
{@const first = group.items[0] ?? group.prio[0]}
|
||||
<div class="bg-base-content/15 rounded-xl px-2 font-bold print:hidden">
|
||||
<div class="bg-base-content/15 rounded-xl px-2 font-bold">
|
||||
{#if data.groupByStation}
|
||||
{#if first.patient.room}
|
||||
Station {first.patient.room?.station.name}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { LayoutServerLoad } from "./$types";
|
||||
|
||||
export const load: LayoutServerLoad = (event) => ({
|
||||
export const load: LayoutServerLoad = async (event) => ({
|
||||
session: event.locals.session,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import "../app.pcss";
|
||||
import "../app.pcss"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
||||
|
||||
import { navigating } from "$app/stores";
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
const options = { pausable: true };
|
||||
</script>
|
||||
|
||||
<LoadingBar bind:this={loadingBar} fixedTop />
|
||||
<LoadingBar bind:this={loadingBar} cls="fixed top-0 left-0 z-50" />
|
||||
<SvelteToast {options} />
|
||||
|
||||
<div class="bg-base-100 text-base-content" bind:clientWidth={$screenWidth}>
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<div class="max-w-[100vw] px-6 pb-16 xl:pr-2 text-center">
|
||||
<h1 class="text-4xl mt-4">Visitenbuch</h1>
|
||||
<form method="POST" use:enhance>
|
||||
<button class="btn btn-primary mt-4" data-testid="btn-login" type="submit">Anmelden</button>
|
||||
<button class="btn btn-primary mt-4" type="submit">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
import {
|
||||
isLoggedIn, loginIfNecessary, loginWithToken, USERNAME, OIDC_BASE_URL,
|
||||
} from "$tests/helpers/login";
|
||||
|
||||
test.describe.configure({ mode: "serial" }); // Parallel account creation may cause issues
|
||||
|
||||
test("login", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await loginIfNecessary(page);
|
||||
await expect(page).toHaveTitle("Visitenbuch");
|
||||
await expect(page.locator("h1.heading")).toHaveText("Hallo, Lucy Login");
|
||||
// Test cases may create more entries
|
||||
expect(parseInt(await page.getByTestId("n-entries-todo").innerText()))
|
||||
.toBeGreaterThanOrEqual(193);
|
||||
});
|
||||
|
||||
test("loginWithToken", async ({ page }) => {
|
||||
await loginWithToken(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page).toHaveTitle("Visitenbuch");
|
||||
await expect(page.locator("h1.heading")).toHaveText("Hallo, " + USERNAME);
|
||||
// Test cases may create more entries
|
||||
expect(parseInt(await page.getByTestId("n-entries-todo").innerText()))
|
||||
.toBeGreaterThanOrEqual(193);
|
||||
});
|
||||
|
||||
test("logout", async ({ page, baseURL }) => {
|
||||
await page.goto("/");
|
||||
await loginIfNecessary(page);
|
||||
|
||||
await page.goto("/logout");
|
||||
await page.getByTestId("btn-logout").click();
|
||||
await page.locator('button[value="yes"]').click();
|
||||
await page.waitForURL("/login?noAuto=1");
|
||||
await expect(page.getByTestId("btn-login")).toBeVisible();
|
||||
expect(await isLoggedIn(page)).toBe(false);
|
||||
|
||||
// Check if application is not accessible if unauthorized
|
||||
await page.goto("/plan");
|
||||
expect(page.url() === baseURL + "/login?returnURL=%2Fplan" || page.url().startsWith(OIDC_BASE_URL)).toBe(true);
|
||||
|
||||
// Check if TRPC API is not accessible if unauthorized
|
||||
const apiResponse = await page.context().request.get("/trpc/savedFilter.getAll");
|
||||
expect(apiResponse.status()).toBe(401);
|
||||
expect(await apiResponse.json()).toMatchObject({ error: { message: "not logged in" } });
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
import { loginWithToken } from "$tests/helpers/login";
|
||||
|
||||
test("filter", async ({ page }) => {
|
||||
await loginWithToken(page);
|
||||
await page.goto("/plan");
|
||||
await expect(page).toHaveTitle("Planung");
|
||||
|
||||
const filterbar = page.locator(".filterbar-outer");
|
||||
const filterIn = filterbar.getByPlaceholder("Filter");
|
||||
await filterIn.click();
|
||||
await filterbar.getByRole("option", { name: "Kategorie" }).click();
|
||||
await filterbar.getByRole("option", { name: "Laborabnahme" }).click();
|
||||
await filterIn.click();
|
||||
await filterbar.getByRole("option", { name: "Zimmer" }).click();
|
||||
await filterbar.getByRole("option", { name: "R1.5" }).click();
|
||||
await filterIn.click();
|
||||
await filterbar.getByRole("option", { name: "Autor" }).click();
|
||||
await filterbar.getByRole("option", { name: "Akeem Wisozk" }).click();
|
||||
|
||||
await expect(page).toHaveURL("http://localhost:4173/plan?filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Laborabnahme&filter%5Broom%5D%5B0%5D%5Bid%5D=5&filter%5Broom%5D%5B0%5D%5Bname%5D=R1.5&filter%5Bauthor%5D%5B0%5D%5Bid%5D=5&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Akeem%20Wisozk");
|
||||
|
||||
const table = page.getByTestId("entry-table");
|
||||
const firstRow = table.locator("tbody > tr").first();
|
||||
|
||||
await expect(firstRow.locator("td > a").first()).toHaveText("275");
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { performance } from "perf_hooks";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
@ -30,17 +29,9 @@ function randomId(len: number): number {
|
|||
return faker.number.int({ min: 1, max: len - 1 });
|
||||
}
|
||||
|
||||
function randomUserId(): number {
|
||||
return randomId(N_USERS) + 1;
|
||||
}
|
||||
|
||||
/** Reset database and create extensive test data for development and E2E tests */
|
||||
export default async () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Reset database
|
||||
await prisma.$transaction([
|
||||
prisma.savedFilter.deleteMany(),
|
||||
prisma.entryExecution.deleteMany(),
|
||||
prisma.entryVersion.deleteMany(),
|
||||
prisma.entry.deleteMany(),
|
||||
|
@ -67,16 +58,9 @@ export default async () => {
|
|||
const entryMockdata: MockEntry[] = file
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((l) => JSON.parse(l) as MockEntry);
|
||||
.map((l) => JSON.parse(l));
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: 1,
|
||||
name: "Tico Testboy",
|
||||
email: "t.testboy@example.com",
|
||||
},
|
||||
});
|
||||
for (let i = 2; i <= N_USERS + 1; i++) {
|
||||
for (let i = 1; i <= N_USERS; i++) {
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
|
||||
|
@ -125,7 +109,7 @@ export default async () => {
|
|||
await prisma.entryVersion.create({
|
||||
data: {
|
||||
entry_id: entry.id,
|
||||
author_id: randomUserId(),
|
||||
author_id: randomId(N_USERS),
|
||||
category_id: CATEGORY_IDS[e.category],
|
||||
date: todo_date,
|
||||
priority,
|
||||
|
@ -139,7 +123,7 @@ export default async () => {
|
|||
await prisma.entryVersion.create({
|
||||
data: {
|
||||
entry_id: entry.id,
|
||||
author_id: randomUserId(),
|
||||
author_id: randomId(N_USERS),
|
||||
category_id: CATEGORY_IDS[e.category],
|
||||
date: todo_date,
|
||||
priority,
|
||||
|
@ -153,7 +137,7 @@ export default async () => {
|
|||
await prisma.entryExecution.create({
|
||||
data: {
|
||||
entry_id: entry.id,
|
||||
author_id: randomUserId(),
|
||||
author_id: randomId(N_USERS),
|
||||
text: e.result,
|
||||
created_at: faker.date.soon({ refDate: todo_date, days: 2 }),
|
||||
},
|
||||
|
@ -162,7 +146,7 @@ export default async () => {
|
|||
|
||||
await prisma.$transaction([
|
||||
prisma.$executeRawUnsafe(
|
||||
`alter sequence users_id_seq restart with ${N_USERS + 2}`,
|
||||
`alter sequence users_id_seq restart with ${N_USERS + 1}`,
|
||||
),
|
||||
prisma.$executeRawUnsafe(
|
||||
`alter sequence categories_id_seq restart with ${CATEGORIES.length + 1}`,
|
||||
|
@ -178,6 +162,4 @@ export default async () => {
|
|||
),
|
||||
]);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated mock data in ${performance.now() - startTime} ms`);
|
||||
};
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import { encode } from "@auth/core/jwt";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
const AUTH_COOKIE = "authjs.session-token";
|
||||
|
||||
export const OIDC_BASE_URL = "http://localhost:9090/interaction/";
|
||||
export const USERNAME = "Tico Testboy";
|
||||
export const USER_EMAIL = "t.testboy@example.org";
|
||||
|
||||
export async function loginIfNecessary(page: Page) {
|
||||
if (page.url().startsWith(OIDC_BASE_URL)) {
|
||||
await page.locator('input[name="login"]').fill("Lucy Login");
|
||||
await page.locator('input[name="password"]').fill("1234");
|
||||
await page.locator("button.login-submit").click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
expect(await isLoggedIn(page)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function isLoggedIn(page: Page): Promise<boolean> {
|
||||
const cookies = await page.context().cookies();
|
||||
return cookies.findIndex((c) => c.name === AUTH_COOKIE) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a session token (Cookie: authjs.session-token) to use for E2E tests
|
||||
* so we dont have to step through the login system every time
|
||||
*/
|
||||
export async function newSessionToken(user_id: number): Promise<string> {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
select: { email: true, name: true },
|
||||
where: { id: user_id },
|
||||
});
|
||||
return encode({
|
||||
salt: AUTH_COOKIE,
|
||||
secret: process.env.AUTH_SECRET!,
|
||||
token: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
sub: user_id.toString(),
|
||||
id: user_id.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function loginWithToken(page: Page, user_id = 1) {
|
||||
const token = await newSessionToken(user_id);
|
||||
await page.context().addCookies([{
|
||||
name: AUTH_COOKIE,
|
||||
value: token,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
}]);
|
||||
}
|
|
@ -4,7 +4,6 @@ import {
|
|||
CATEGORIES, ROOMS, STATIONS, USERS,
|
||||
} from "./testdata";
|
||||
|
||||
/** Reset database and create basic test data for integration tests */
|
||||
export default async () => {
|
||||
await prisma.$transaction([
|
||||
prisma.savedFilter.deleteMany(),
|
||||
|
|
|
@ -26,7 +26,7 @@ test("get categories", async () => {
|
|||
|
||||
test("delete categories", async () => {
|
||||
await deleteCategory(6);
|
||||
await expect(getCategory(6)).rejects.toThrowError("No Category found");
|
||||
expect(getCategory(6)).rejects.toThrowError("No Category found");
|
||||
});
|
||||
|
||||
test("hide category", async () => {
|
||||
|
|
|
@ -149,7 +149,7 @@ test("create entry version (wrong old vid)", async () => {
|
|||
});
|
||||
const entry = await getEntry(eId);
|
||||
|
||||
await expect(async () => {
|
||||
expect(async () => {
|
||||
await newEntryVersion(
|
||||
1,
|
||||
eId,
|
||||
|
@ -202,7 +202,7 @@ test("create entry execution (wrong old xid)", async () => {
|
|||
});
|
||||
const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null);
|
||||
|
||||
await expect(async () => newEntryExecution(1, eId, { text: "x2", done: true }, 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 () => {
|
||||
|
|
|
@ -49,7 +49,7 @@ test("update patient", async () => {
|
|||
|
||||
test("delete patient", async () => {
|
||||
await deletePatient(1);
|
||||
await expect(async () => getPatient(1)).rejects.toThrowError("No Patient found");
|
||||
expect(async () => getPatient(1)).rejects.toThrowError("No Patient found");
|
||||
});
|
||||
|
||||
test("delete patient (restricted)", async () => {
|
||||
|
@ -64,7 +64,7 @@ test("delete patient (restricted)", async () => {
|
|||
},
|
||||
});
|
||||
|
||||
await expect(async () => deletePatient(pId)).rejects.toThrowError();
|
||||
expect(async () => deletePatient(pId)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test("hide patient", async () => {
|
||||
|
|
|
@ -37,7 +37,7 @@ test("update room", async () => {
|
|||
|
||||
test("delete room", async () => {
|
||||
await deleteRoom(ROOMS[3].id);
|
||||
await expect(async () => getRoom(ROOMS[3].id)).rejects.toThrowError("No Room found");
|
||||
expect(async () => getRoom(ROOMS[3].id)).rejects.toThrowError("No Room found");
|
||||
});
|
||||
|
||||
test("hide room", async () => {
|
||||
|
|
|
@ -96,7 +96,7 @@ test("update filter", async () => {
|
|||
});
|
||||
|
||||
test("update filter not found", async () => {
|
||||
await expect(updateSavedFilter(1, "Hello World", 1)).rejects.toThrowError();
|
||||
expect(updateSavedFilter(1, "Hello World", 1)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test("delete filter", async () => {
|
||||
|
|
|
@ -25,7 +25,7 @@ test("update station", async () => {
|
|||
|
||||
test("delete station", async () => {
|
||||
await deleteStation(S3.id);
|
||||
await expect(async () => getStation(S3.id)).rejects.toThrowError("No Station found");
|
||||
expect(async () => getStation(S3.id)).rejects.toThrowError("No Station found");
|
||||
});
|
||||
|
||||
test("hide station", async () => {
|
||||
|
|
|
@ -16,7 +16,7 @@ test("create entry", async () => {
|
|||
});
|
||||
expect(eID).gt(0);
|
||||
|
||||
const entry = await caller().entry.get(eID);
|
||||
const entry = await caller().entry.get(eID!);
|
||||
expect(entry.patient.id).toBe(1);
|
||||
expect(entry.execution).toBeNull();
|
||||
expect(entry.current_version.id).gt(0);
|
||||
|
|
|
@ -2,12 +2,12 @@ import { exec } from "child_process";
|
|||
import { promisify } from "util";
|
||||
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { createViteLicensePlugin, type LicenseMeta } from "rollup-license-plugin";
|
||||
import { createViteLicensePlugin } from "rollup-license-plugin";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// Get current tag/commit and last commit date from git
|
||||
const pexec = promisify(exec);
|
||||
const [version, lastmod] = (
|
||||
let [version, lastmod] = (
|
||||
await Promise.allSettled([
|
||||
pexec("git describe --tags || git rev-parse --short HEAD"),
|
||||
pexec('git log -1 --format=%cd --date=format:"%Y-%m-%d %H:%M"'),
|
||||
|
@ -31,17 +31,13 @@ export default defineConfig({
|
|||
<h1>Open-Source-Lizenzen</h1>
|
||||
<a href="./oss-licenses.json">JSON-formatted license list</a>
|
||||
`;
|
||||
for (const _p of packages) {
|
||||
type LicenseMetaExt = LicenseMeta & {
|
||||
repository: string | null,
|
||||
author: string | { name: string } | null
|
||||
};
|
||||
const p = _p as LicenseMetaExt;
|
||||
const rp = p.repository;
|
||||
let aut = null;
|
||||
if (p.author) {
|
||||
if (typeof p.author === "object" && p.author.name) aut = p.author.name;
|
||||
else if (typeof p.author === "string") aut = p.author;
|
||||
for (const p of packages) {
|
||||
// @ts-expect-error repo not present in type definition
|
||||
let rp = p.repository;
|
||||
// @ts-expect-error author not present in type definition
|
||||
let aut = p.author;
|
||||
if (typeof aut === "object") {
|
||||
aut = aut.name;
|
||||
}
|
||||
|
||||
let repoUrl = null;
|
Loading…
Reference in a new issue