refactor(web): upgrade to SvelteKit 3 #107

Merged
eleith merged 3 commits from upgrade/sveltekit3-native into main 2026-10-30 15:00:12 +00:00
138 changed files with 742 additions and 1110 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
'@when/web': patch
---
Upgrade to SvelteKit 3 with native subpath imports, current framework APIs, and trusted reverse-proxy origin handling.
+3 -1
View File
@@ -22,13 +22,15 @@ openssl rand -base64 32
```
Edit `.env` and `config/when.yaml` to make it all cozy and comfortable.
Set `ORIGIN` to your public HTTPS URL and put a trusted reverse proxy in front of
When; see the [proxy requirements](docs/deployment.md#reverse-proxy).
Make `data/` writable by container UID 1000; see [permissions](docs/deployment.md#start-up).
```sh
docker compose up -d
```
Open http://localhost:3000 and `/admin` to get a lay of the land.
Open your public proxy URL and its `/admin` page to get a lay of the land.
There is a lot more going on under the hood, read more documentation if you
want to learn more.
-1
View File
@@ -12,7 +12,6 @@ services:
- ./src:/m/apps/web/src
- ./static:/m/apps/web/static
- ./config:/m/apps/web/config
- ./svelte.config.js:/m/apps/web/svelte.config.js
- ./vite.config.ts:/m/apps/web/vite.config.ts
- ./tsconfig.json:/m/apps/web/tsconfig.json
- ./eslint.config.js:/m/apps/web/eslint.config.js
+66 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from './support/test.ts';
import { ADMIN, signInAsAdminViaForm } from './support/auth.ts';
import { ADMIN, signInAsAdmin, signInAsAdminViaForm } from './support/auth.ts';
test('an admin page sends a signed-out visitor to sign in', async ({ page }) => {
await page.goto('/admin');
@@ -13,6 +13,55 @@ test('an admin page refuses a signed-out non-browser request', async ({ request
expect(response.status()).toBe(403);
});
test('Google connect refuses a signed-out same-origin POST', async ({ baseURL, request }) => {
const response = await request.post('/admin/services/google/connect', {
headers: { origin: baseURL!, accept: 'application/json' },
form: { provider: 'fixture-google' },
maxRedirects: 0
});
expect(response.status()).toBe(403);
expect(await response.json()).toMatchObject({ message: 'Not authorized.' });
expect(response.headers()['set-cookie'] ?? '').not.toContain('when_google_oauth=');
});
test('Google connect redirects to consent with the callback and nonce cookie', async ({
baseURL,
page
}) => {
await signInAsAdmin(page);
// Inspect only the redirect: never follow it to Google or exchange a code.
const response = await page.request.post('/admin/services/google/connect', {
headers: { origin: baseURL! },
form: { provider: 'fixture-google' },
maxRedirects: 0
});
expect(response.status()).toBe(303);
const consent = new URL(response.headers().location);
expect(consent.origin).toBe('https://accounts.google.com');
expect(consent.searchParams.get('client_id')).toBe('e2e-google-client');
expect(consent.searchParams.get('redirect_uri')).toBe(
`${baseURL}/admin/services/google/callback`
);
const state = consent.searchParams.get('state');
expect(state).toBeTruthy();
const stateCookies = response
.headersArray()
.filter(
({ name, value }) =>
name.toLowerCase() === 'set-cookie' && value.startsWith('when_google_oauth=')
);
expect(stateCookies).toHaveLength(1);
const cookie = stateCookies[0].value;
const value = cookie.split(';')[0].slice('when_google_oauth='.length);
expect(JSON.parse(decodeURIComponent(value))).toEqual({ state, service: 'fixture-google' });
expect(cookie).toContain('Path=/admin');
expect(cookie).toContain('HttpOnly');
expect(cookie).toContain('SameSite=Lax');
});
test('the wrong password is rejected', async ({ page }) => {
await page.goto('/signin');
await page.getByLabel('Username').fill(ADMIN.username);
@@ -24,6 +73,22 @@ test('the wrong password is rejected', async ({ page }) => {
await expect(page).not.toHaveURL('/admin');
});
test('a retired sign-in callback does not turn successful authentication into a network error', async ({
page
}) => {
// Keep this extra real-form login independent of the production per-IP limiter.
await page.setExtraHTTPHeaders({ 'x-real-ip': '192.0.2.200' });
await page.goto('/signin?callbackUrl=%2Fretired-page');
await page.getByLabel('Username').fill(ADMIN.username);
await page.getByLabel('Password').fill(ADMIN.password);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await expect(page).toHaveURL('/retired-page');
await expect(page.locator('.signin-form')).toHaveCount(0);
const admin = await page.request.get('/admin', { maxRedirects: 0 });
expect(admin.status()).toBe(200);
});
test('the fixture credentials reach the admin dashboard without a pending banner flash', async ({
page
}) => {
+2
View File
@@ -11,11 +11,13 @@ test('a guest books a meeting from the home page', async ({ page }) => {
await pickFirstBookableDay(page);
await expect(page.getByRole('heading', { name: 'Step 2 of 3: Pick a time' })).toBeVisible();
await expect(page).toHaveURL(/\?date=\d{4}-\d{2}-\d{2}$/);
await pickSlotFromEndOfDay(page);
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('heading', { name: 'Step 3 of 3: Enter your info' })).toBeVisible();
const slot = await page.locator('input[name="slot"]').inputValue();
await expect(page).toHaveURL(`/schedule/${CHAT_MEETING.slug}?slot=${encodeURIComponent(slot)}`);
await page.getByLabel('What is your name?').fill('E2E Guest');
await page.getByLabel('What is your email?').fill('e2e-guest@example.test');
await page.getByRole('button', { name: 'Request' }).click();
+3
View File
@@ -40,5 +40,8 @@ test('a day link with no availability explains itself without breaking the route
await expect(page.getByText('has no availability')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Step 1 of 3: Pick a day' })).toBeVisible();
await expect(page).toHaveURL('/schedule/chat');
await pickLastBookableDay(page);
await expect(page.getByRole('heading', { name: 'Step 2 of 3: Pick a time' })).toBeVisible();
expect(errors).toEqual([]);
});
+6
View File
@@ -35,6 +35,12 @@ providers:
password: 'e2e-video-password'
calendars: {}
fixture-google:
type: 'google'
client_id: 'e2e-google-client'
client_secret: 'e2e-google-secret'
calendars: {}
schedules:
standard:
weekly:
+35
View File
@@ -20,6 +20,41 @@ test('the booking page sends the security headers', async ({ request }) => {
expect(headers['content-security-policy']).toContain("frame-ancestors 'none'");
});
test('booking forms check browser Origin against the same trusted scheme and Host', async ({
baseURL,
request
}) => {
const form = { slot: 'not-a-slot' };
const headers = { origin: baseURL!, accept: 'application/json' };
const sameOrigin = await request.post('/schedule/chat', {
form,
headers,
maxRedirects: 0
});
expect(sameOrigin.status()).toBe(400);
expect(await sameOrigin.json()).toMatchObject({
type: 'failure',
status: 400,
data: expect.stringContaining('Please pick a time slot.')
});
// Change only browser Origin, not the trusted request scheme or authority.
const foreignHeaders = { ...headers, origin: 'https://foreign.example.test' };
const foreignOrigin = await request.post('/schedule/chat', {
form,
headers: foreignHeaders,
maxRedirects: 0
});
expect(foreignOrigin.status()).toBe(403);
// No body/form option: Playwright must not add a Content-Type header.
const missingContentType = await request.post('/schedule/chat', {
headers: foreignHeaders,
maxRedirects: 0
});
expect(missingContentType.status()).toBe(403);
});
test('the booking page renders and hydrates under the policy', async ({ page }) => {
const violations = collectCspViolations(page);
+6 -5
View File
@@ -364,9 +364,10 @@ test('an empty weekend timeline keeps its duration toolbar usable without fetchi
0
);
await page.getByRole('button', { name: 'Back', exact: true }).click();
await expect(
page.locator('.cal-day:not([data-unavailable]):not([data-disabled])').first()
).toBeVisible();
// Remaining bookable days may be in the next month.
await pickFirstBookableDay(page);
await expect(page.getByRole('heading', { name: 'Step 2 of 3: Pick a time' })).toBeVisible();
await expect(page.getByRole('radio').first()).toBeVisible();
page.off('request', trackDurationRequest);
expect(durationRequests).toEqual([]);
});
@@ -387,7 +388,7 @@ test('client navigation between meeting slugs replaces duration defaults, choice
anchor.id = 'e2e-switch-meeting';
anchor.href = href;
anchor.textContent = 'Switch to chat fixture';
anchor.setAttribute('data-sveltekit-preload-data', 'off');
anchor.setAttribute('data-sveltekit-preload-data', 'false');
document.body.append(anchor);
}, `/schedule/chat?date=${day}`);
const documentRequests: string[] = [];
@@ -457,7 +458,7 @@ test('an enhanced host booking conflict preserves entered fields and the chosen
anchor.id = 'e2e-refresh-meeting';
anchor.href = href;
anchor.textContent = 'Refresh this meeting fixture';
anchor.setAttribute('data-sveltekit-preload-data', 'off');
anchor.setAttribute('data-sveltekit-preload-data', 'false');
document.body.append(anchor);
}, `/schedule/${slug}?duration=90&date=${day}`);
const historyLength = await page.evaluate(() => history.length);
+8 -3
View File
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { parse, stringify } from 'yaml';
// Bind first so config, Auth.js, Kit's origin check and the browser all use the
// Bind first so config, Better Auth, Kit's origin check and the browser all use the
// same OS-assigned port. Nothing can reach the app until the handler is installed.
const server = createServer();
// No background worker runs: jobs remain queued for assertions. Closing the web
@@ -23,10 +23,11 @@ await new Promise((resolve, reject) => {
server.once('listening', resolve);
server.once('error', reject);
});
// localhost also lets Playwright's API client send Auth.js's secure session cookie
// localhost also lets Playwright's API client send Better Auth's secure session cookie
// over HTTP, matching the browser's loopback secure-context exception.
const origin = `http://localhost:${server.address().port}`;
process.env.ORIGIN = origin;
process.env.PROTOCOL_HEADER = 'x-forwarded-proto';
const configPath = process.env.CONFIG_PATH;
const directory = dirname(dirname(configPath));
const config = parse(
@@ -41,6 +42,10 @@ await mkdir(dirname(configPath), { recursive: true });
await writeFile(configPath, stringify(config));
const handlerPath = join(process.env.WHEN_E2E_RUN_DIR, 'project/build/handler.js');
const { handler } = await import(pathToFileURL(handlerPath).href);
server.on('request', handler);
server.on('request', (req, res) => {
// This loopback boundary supplies the trusted scheme; Host/port stay native.
req.headers['x-forwarded-proto'] = 'http';
handler(req, res);
});
process.send({ origin });
+1 -8
View File
@@ -18,14 +18,7 @@ export default async function setup() {
await mkdir(join(projectDirectory, 'config'), { recursive: true });
// Kit's post-build workers rediscover config from cwd. Build a fresh copy
// with the normal configs, rather than overriding paths in one build phase.
for (const path of [
'src',
'static',
'package.json',
'svelte.config.js',
'vite.config.ts',
'tsconfig.json'
]) {
for (const path of ['src', 'static', 'package.json', 'vite.config.ts', 'tsconfig.json']) {
await cp(join(webDirectory, path), join(projectDirectory, path), { recursive: true });
}
await symlink(
+6 -1
View File
@@ -6,7 +6,12 @@ import { defineConfig } from 'eslint/config';
import globals from 'globals';
import prettier from 'eslint-config-prettier';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
import { loadConfig } from '@sveltejs/load-config';
const loadedConfig = await loadConfig(import.meta.dirname, { traverse: false });
if (!loadedConfig) throw new Error('Svelte configuration not found');
if ('error' in loadedConfig) throw loadedConfig.error;
const svelteConfig = loadedConfig.config;
const gitignorePath = path.resolve(import.meta.dirname, '../../.gitignore');
+8 -4
View File
@@ -29,8 +29,9 @@
"@eslint/js": "10.0.1",
"@iconify-json/ph": "1.2.2",
"@playwright/test": "1.61.1",
"@sveltejs/adapter-node": "5.5.4",
"@sveltejs/kit": "2.70.1",
"@sveltejs/adapter-node": "6.0.1",
"@sveltejs/kit": "3.1.1",
"@sveltejs/load-config": "0.2.2",
"@sveltejs/vite-plugin-svelte": "7.1.2",
"@types/node": "24.12.4",
"@vitest/coverage-v8": "4.1.7",
@@ -41,8 +42,8 @@
"pino-pretty": "13.1.3",
"prettier": "3.8.3",
"prettier-plugin-svelte": "3.5.2",
"svelte": "5.56.0",
"svelte-check": "4.4.8",
"svelte": "5.57.3",
"svelte-check": "4.7.7",
"typescript": "6.0.3",
"typescript-eslint": "8.60.0",
"unplugin-icons": "23.0.1",
@@ -68,5 +69,8 @@
"temporal-polyfill": "1.0.1",
"ts-ics": "2.4.4",
"yaml": "2.9.0"
},
"imports": {
"#lib/*": "./src/lib/*"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { WhenSession } from '$lib/server/auth';
import type { WhenSession } from '#lib/server/auth/index.js';
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
+6 -6
View File
@@ -1,11 +1,11 @@
import type { Handle } from '@sveltejs/kit/hooks';
import { describe, expect, test, vi } from 'vitest';
import type { Handle } from '@sveltejs/kit';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('$lib/server/boot', () => ({ bootApp: async () => {} }));
vi.mock('$lib/server/appearance', () => ({ themeStyleTag: () => '' }));
vi.mock('$lib/server/state', () => ({ getConfig: () => validConfig }));
vi.mock('$lib/server/auth', () => ({
vi.mock('#lib/server/boot.js', () => ({ bootApp: async () => {} }));
vi.mock('#lib/server/appearance.js', () => ({ themeStyleTag: () => '' }));
vi.mock('#lib/server/state.js', () => ({ getConfig: () => validConfig }));
vi.mock('#lib/server/auth/index.js', () => ({
authHandle: (({ event, resolve }) => resolve(event)) as Handle
}));
+8 -8
View File
@@ -1,11 +1,11 @@
import { sequence } from '@sveltejs/kit/hooks';
import { redirect, error, type Handle } from '@sveltejs/kit';
import { authHandle } from '$lib/server/auth';
import { localRedirect } from '$lib/server/redirect';
import { bootApp } from '$lib/server/boot';
import { logger } from '$lib/server/logger';
import { getConfig } from '$lib/server/state';
import { themeStyleTag } from '$lib/server/appearance';
import { redirect, error } from '@sveltejs/kit';
import { sequence, type Handle } from '@sveltejs/kit/hooks';
import { authHandle } from '#lib/server/auth/index.js';
import { localRedirect } from '#lib/server/redirect.js';
import { bootApp } from '#lib/server/boot.js';
import { logger } from '#lib/server/logger.js';
import { getConfig } from '#lib/server/state.js';
import { themeStyleTag } from '#lib/server/appearance.js';
try {
await bootApp();
+3 -2
View File
@@ -369,8 +369,9 @@ describe('desiredUrlState', () => {
});
describe('urlStateMatches', () => {
test('is true only when every param matches', () => {
const params = new URLSearchParams('duration=60&date=2025-06-15');
test('reads matching values without requiring mutable params', () => {
const search = new URLSearchParams('duration=60&date=2025-06-15');
const params = { get: search.get.bind(search) };
expect(urlStateMatches(params, { duration: '60', slot: null, date: '2025-06-15' })).toBe(true);
expect(urlStateMatches(params, { duration: null, slot: null, date: '2025-06-15' })).toBe(false);
});
+4 -1
View File
@@ -160,7 +160,10 @@ export function desiredUrlState(p: {
}
/** True when the URL already encodes this state — the guard against a re-encode loop. */
export function urlStateMatches(params: URLSearchParams, state: AppointmentUrlState): boolean {
export function urlStateMatches(
params: Pick<URLSearchParams, 'get'>,
state: AppointmentUrlState
): boolean {
return (
params.get('duration') === state.duration &&
params.get('slot') === state.slot &&
@@ -1,103 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { flushSync } from 'svelte';
import { createAppointmentFlow } from './appointmentFlow.svelte';
const m = vi.hoisted(() => ({
page: { url: new URL('http://localhost/schedule/x'), state: {} },
replaceState: vi.fn(),
afterCb: { current: null as null | (() => void) }
}));
vi.mock('$app/state', () => ({ page: m.page }));
vi.mock('$app/navigation', () => ({
afterNavigate: (cb: () => void) => {
m.afterCb.current = cb;
},
replaceState: (...a: unknown[]) => m.replaceState(...a)
}));
import { createAppointmentUrlSync } from './appointmentUrlSync.svelte';
const SLOT = '2026-07-20T10:00:00Z';
function mkFlow() {
const flow = createAppointmentFlow({
slotsByDuration: { 30: { '2026-07-20': [SLOT] } },
durations: [30],
initialDuration: 30
});
flow.setTz('UTC');
return flow;
}
function sync(flow: ReturnType<typeof mkFlow>) {
return createAppointmentUrlSync(flow, { deepLink: true, tz: 'UTC', defaultDuration: 30 });
}
beforeEach(() => {
vi.clearAllMocks();
m.page.url = new URL('http://localhost/schedule/x');
m.afterCb.current = null;
});
describe('createAppointmentUrlSync deep-link application', () => {
it('selects an available deep-linked slot and jumps to the details step', () => {
m.page.url = new URL(`http://localhost/schedule/x?slot=${SLOT}`);
$effect.root(() => {
const flow = mkFlow();
sync(flow);
expect(flow.selectedSlot).toBe(SLOT);
expect(flow.step).toBe(3);
})();
});
it('opens the day and notices an unavailable slot on an available day', () => {
m.page.url = new URL('http://localhost/schedule/x?slot=2026-07-20T11:00:00Z');
$effect.root(() => {
const flow = mkFlow();
const s = sync(flow);
expect(flow.selectedSlot).toBeNull();
expect(flow.viewDate).toBe('2026-07-20');
expect(s.notice?.kind).toBe('slot');
})();
});
it('notices an unavailable date without opening anything', () => {
m.page.url = new URL('http://localhost/schedule/x?date=2030-01-01');
$effect.root(() => {
const flow = mkFlow();
const s = sync(flow);
expect(flow.viewDate).toBeNull();
expect(s.notice?.kind).toBe('date');
})();
});
});
describe('createAppointmentUrlSync effects', () => {
it('mirrors the flow position into the url once the router is ready', () => {
$effect.root(() => {
const flow = mkFlow();
sync(flow);
m.afterCb.current?.();
flow.selectSlot(SLOT);
flow.goToStep(3);
flushSync();
expect(m.replaceState).toHaveBeenCalledWith(
expect.stringContaining(`slot=${encodeURIComponent(SLOT)}`),
expect.anything()
);
})();
});
it('clears a slot notice once a slot is picked', () => {
m.page.url = new URL('http://localhost/schedule/x?slot=2026-07-20T11:00:00Z');
$effect.root(() => {
const flow = mkFlow();
const s = sync(flow);
expect(s.notice?.kind).toBe('slot');
flow.selectSlot(SLOT);
flushSync();
expect(s.notice).toBeNull();
})();
});
});
@@ -1,5 +1,5 @@
import { page } from '$app/state';
import { replaceState, afterNavigate } from '$app/navigation';
import { goto, afterNavigate } from '$app/navigation';
import {
resolveDeepLink,
desiredUrlState,
@@ -66,8 +66,14 @@ export function createAppointmentUrlSync(
selectedSlot: flow.selectedSlot,
viewDate: flow.viewDate
});
if (urlStateMatches(page.url.searchParams, desired)) return;
replaceState(`${page.url.pathname}${buildAppointmentSearch(desired)}`, page.state);
// Shallow navigation changes page.shallow, not the last fully loaded page.url.
const url = page.shallow?.url ?? page.url;
if (urlStateMatches(url.searchParams, desired)) return;
void goto(`${url.pathname}${buildAppointmentSearch(desired)}`, {
shallow: true,
replace: true,
state: page.state
}).catch((error) => console.error('Failed to synchronize booking URL', error));
});
return {
+2 -2
View File
@@ -2,7 +2,7 @@
import type { Snippet } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { authClient } from '$lib/auth-client';
import { authClient } from '#lib/auth-client.js';
import IconCalendarBlank from 'virtual:icons/ph/calendar-blank';
import IconArrowUpLeft from 'virtual:icons/ph/arrow-up-left';
import IconSignOut from 'virtual:icons/ph/sign-out';
@@ -32,7 +32,7 @@
signoutError = error.status === 429 ? 'rate' : 'auth';
return;
}
await goto(redirectTo, { invalidateAll: true });
await goto(redirectTo, { refreshAll: true });
} catch {
signoutError = 'network';
} finally {
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import AdminNav from '$lib/components/AdminNav.svelte';
import AdminNav from '#lib/components/AdminNav.svelte';
// Owns both the full-bleed nav and the centered content column. Pages render this
// rather than a layout so each route can name its own crumb.
@@ -2,7 +2,7 @@
import { DropdownMenu, Dialog } from 'bits-ui';
import { enhance } from '$app/forms';
import { tick } from 'svelte';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import IconDotsThreeVertical from 'virtual:icons/ph/dots-three-vertical';
import IconX from 'virtual:icons/ph/x';
@@ -294,8 +294,9 @@
action="/admin/appointment/{appointmentId}?/rotate"
class="share-rotate"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
return async ({ result, update }) => {
await update({ reset: false, navigate: false });
if (result.type !== 'success') return;
rotated = true;
copied = false;
// The token is the tail of a long value, so scroll it into view.
@@ -14,7 +14,7 @@
import IconCalendarBlank from 'virtual:icons/ph/calendar-blank';
import IconPencilSimple from 'virtual:icons/ph/pencil-simple';
import IconDot from 'virtual:icons/ph/dot-outline';
import { formatTimestamp } from '$lib/datetime';
import { formatTimestamp } from '#lib/datetime.js';
let {
log,
@@ -1,22 +1,22 @@
<script lang="ts">
import { page } from '$app/state';
import DatePicker from '$lib/components/DatePicker.svelte';
import DayTimeline from '$lib/components/DayTimeline.svelte';
import WizardPickerControls from '$lib/components/WizardPickerControls.svelte';
import CalendarSkeleton from '$lib/components/CalendarSkeleton.svelte';
import TimelineSkeleton from '$lib/components/TimelineSkeleton.svelte';
import RescheduleBanner from '$lib/components/RescheduleBanner.svelte';
import LinkNotice from '$lib/components/LinkNotice.svelte';
import WizardContext from '$lib/components/WizardContext.svelte';
import WizardActions from '$lib/components/WizardActions.svelte';
import GuestForm from '$lib/components/GuestForm.svelte';
import { createAppointmentFlow } from '$lib/appointmentFlow.svelte';
import { createAppointmentUrlSync } from '$lib/appointmentUrlSync.svelte';
import { buildDayTimeline } from '$lib/appointment';
import { formatDate, formatDateCompact, formatTimeShort, formatTzAbbrev } from '$lib/datetime';
import { getPreferredTimezone } from '$lib/preferredTimezone.svelte';
import DatePicker from '#lib/components/DatePicker.svelte';
import DayTimeline from '#lib/components/DayTimeline.svelte';
import WizardPickerControls from '#lib/components/WizardPickerControls.svelte';
import CalendarSkeleton from '#lib/components/CalendarSkeleton.svelte';
import TimelineSkeleton from '#lib/components/TimelineSkeleton.svelte';
import RescheduleBanner from '#lib/components/RescheduleBanner.svelte';
import LinkNotice from '#lib/components/LinkNotice.svelte';
import WizardContext from '#lib/components/WizardContext.svelte';
import WizardActions from '#lib/components/WizardActions.svelte';
import GuestForm from '#lib/components/GuestForm.svelte';
import { createAppointmentFlow } from '#lib/appointmentFlow.svelte.js';
import { createAppointmentUrlSync } from '#lib/appointmentUrlSync.svelte.js';
import { buildDayTimeline } from '#lib/appointment.js';
import { formatDate, formatDateCompact, formatTimeShort, formatTzAbbrev } from '#lib/datetime.js';
import { getPreferredTimezone } from '#lib/preferredTimezone.svelte.js';
import type { GuestAnswer, Appearance, FormField } from '@when/config';
import type { PublicEventType } from '$lib/server/appointment/sanitize';
import type { PublicEventType } from '#lib/server/appointment/sanitize.js';
// Shared shape served by both the new-appointment route and the reschedule route.
export interface AppointmentWizardData {
@@ -3,7 +3,7 @@
import IconX from 'virtual:icons/ph/x';
import { Dialog } from 'bits-ui';
import CharacterCount from './CharacterCount.svelte';
import { LIMIT_REASON } from '$lib/forms/validation';
import { LIMIT_REASON } from '#lib/forms/validation.js';
interface Props {
open: boolean;
@@ -1,8 +1,8 @@
<script lang="ts">
import IconWarning from 'virtual:icons/ph/warning';
import type { toAppointmentView } from '$lib/server/appointments';
import AppointmentsBulkActions from '$lib/components/AppointmentsBulkActions.svelte';
import AppointmentsBulkDialog from '$lib/components/AppointmentsBulkDialog.svelte';
import type { toAppointmentView } from '#lib/server/appointments.js';
import AppointmentsBulkActions from '#lib/components/AppointmentsBulkActions.svelte';
import AppointmentsBulkDialog from '#lib/components/AppointmentsBulkDialog.svelte';
interface Props {
appointments: ReturnType<typeof toAppointmentView>[];
@@ -3,7 +3,7 @@
import { CalendarDate, type DateValue } from '@internationalized/date';
import IconCaretLeft from 'virtual:icons/ph/caret-left';
import IconCaretRight from 'virtual:icons/ph/caret-right';
import type { AppointmentFlow } from '$lib/appointmentFlow.svelte';
import type { AppointmentFlow } from '#lib/appointmentFlow.svelte.js';
interface Props {
flow: AppointmentFlow;
@@ -9,8 +9,8 @@
isDirectSlotHit,
type TimelineEventType,
type TimelineSlot
} from '$lib/appointment';
import type { AppointmentFlow } from '$lib/appointmentFlow.svelte';
} from '#lib/appointment.js';
import type { AppointmentFlow } from '#lib/appointmentFlow.svelte.js';
interface Props {
flow: AppointmentFlow;
@@ -9,7 +9,7 @@
LIMIT_EMAIL,
LIMIT_REASON,
validateFieldValue
} from '$lib/forms/validation.js';
} from '#lib/forms/validation.js';
interface Props {
field: FormField;
+10 -15
View File
@@ -1,14 +1,13 @@
<!-- Step 3: the guest fills in their details. Owns all guest-field state and posts the booking. -->
<script lang="ts">
import { applyAction, enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
import { evaluateVisibility } from '$lib/forms/conditional';
import { defaultFieldValue } from '$lib/forms/defaults';
import FormFieldControl from '$lib/components/FormFieldControl.svelte';
import { validateFieldValue } from '$lib/forms/validation.js';
import type { AppointmentFlow } from '$lib/appointmentFlow.svelte';
import { enhance, type SubmitFunction } from '$app/forms';
import { evaluateVisibility } from '#lib/forms/conditional.js';
import { defaultFieldValue } from '#lib/forms/defaults.js';
import FormFieldControl from '#lib/components/FormFieldControl.svelte';
import { validateFieldValue } from '#lib/forms/validation.js';
import type { AppointmentFlow } from '#lib/appointmentFlow.svelte.js';
import type { GuestAnswer, FormField } from '@when/config';
import type { PublicEventType } from '$lib/server/appointment/sanitize';
import type { PublicEventType } from '#lib/server/appointment/sanitize.js';
const RESCHEDULE_FIELD: FormField = {
name: 'reschedule_reason',
@@ -72,8 +71,8 @@
let fieldValues = $state<Record<string, string>>(
Object.fromEntries(formFields.map((f) => [f.name, initialFieldValue(f)]))
);
let rescheduleReasonValue = $state('');
let rescheduleReasonValue = $state('');
const visibleFields = $derived(evaluateVisibility(formFields, (name) => fieldValues[name] ?? ''));
let isFormValid = $derived.by(() => {
@@ -97,12 +96,8 @@
const enhanceWithFeedback: SubmitFunction =
() =>
async ({ result, update }) => {
if (result.type === 'failure') {
await applyAction(result);
return;
}
await update();
async ({ update }) => {
await update({ navigate: false });
};
</script>
@@ -1,8 +1,8 @@
<!-- Warns that a deep-linked slot or day is no longer available. -->
<script lang="ts">
import IconWarningCircle from 'virtual:icons/ph/warning-circle';
import { formatDate, formatSlot } from '$lib/datetime';
import type { DeepLinkResult } from '$lib/appointment';
import { formatDate, formatSlot } from '#lib/datetime.js';
import type { DeepLinkResult } from '#lib/appointment.js';
interface Props {
notice: NonNullable<DeepLinkResult['notice']>;
@@ -1,7 +1,7 @@
<!-- Shown above the wizard when the guest is rescheduling an existing appointment. -->
<script lang="ts">
import IconInfo from 'virtual:icons/ph/info';
import { formatSlot } from '$lib/datetime';
import { formatSlot } from '#lib/datetime.js';
interface Props {
previousHref: string;
@@ -1,8 +1,8 @@
<script lang="ts">
import { tick } from 'svelte';
import { Dialog } from 'bits-ui';
import { getTimezoneIndex, searchTimezones } from '$lib/timezones';
import { getPreferredTimezone } from '$lib/preferredTimezone.svelte';
import { getTimezoneIndex, searchTimezones } from '#lib/timezones.js';
import { getPreferredTimezone } from '#lib/preferredTimezone.svelte.js';
interface Props {
open?: boolean;
@@ -2,9 +2,9 @@
<script lang="ts">
import IconArrowRight from 'virtual:icons/ph/arrow-right';
import IconCaretLeft from 'virtual:icons/ph/caret-left';
import { formatDate, formatTime } from '$lib/datetime';
import type { AppointmentFlow } from '$lib/appointmentFlow.svelte';
import type { PublicEventType } from '$lib/server/appointment/sanitize';
import { formatDate, formatTime } from '#lib/datetime.js';
import type { AppointmentFlow } from '#lib/appointmentFlow.svelte.js';
import type { PublicEventType } from '#lib/server/appointment/sanitize.js';
interface Props {
flow: AppointmentFlow;
@@ -1,7 +1,7 @@
<script lang="ts">
import IconCaretDown from 'virtual:icons/ph/caret-down';
import { formatTzShort } from '$lib/datetime';
import type { AppointmentFlow } from '$lib/appointmentFlow.svelte';
import { formatTzShort } from '#lib/datetime.js';
import type { AppointmentFlow } from '#lib/appointmentFlow.svelte.js';
import DurationDialog from './DurationDialog.svelte';
import TimezoneDialog from './TimezoneDialog.svelte';
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { acceptAppointment } from './accept';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { cancelAppointment } from './cancel';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations, type ActionLogEntry } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -9,7 +9,7 @@ import {
type Appointment,
type NewAppointment
} from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { changeAppointmentDuration } from './change-duration';
import { editAppointment } from './edit';
import type { AppointmentContext } from './context';
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { createAppointment } from './create';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { declineAppointment } from './decline';
import { confirmAppointment } from './transitions';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { openDb, parseActionLog, runMigrations, type Appointment } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { changeAppointmentDuration } from './change-duration';
import { resolveDuration } from './duration';
import { rescheduleAppointment, type RescheduleAppointmentInput } from './reschedule';
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest';
import type { Meeting } from '@when/config';
import type { Appointment } from '@when/db';
import { resolveBookingDurations, resolveDuration } from './duration';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
const base: Meeting = {
...validConfig.meetings['30-min-chat'],
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { editAppointment } from './edit';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations, parseActionLog, withWriteTransaction } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -6,7 +6,7 @@ import {
validateDeclineNote,
validateReason
} from './form.server';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
const baseEvent: Meeting = {
...validConfig.meetings['30-min-chat'],
@@ -6,8 +6,8 @@ import {
LIMIT_LONG,
LIMIT_EMAIL,
LIMIT_REASON
} from '$lib/forms/validation.js';
import { evaluateVisibility } from '$lib/forms/conditional';
} from '#lib/forms/validation.js';
import { evaluateVisibility } from '#lib/forms/conditional.js';
const DEL = 0x7f;
const FIRST_PRINTABLE = 0x20;
@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { purgeAppointment } from './purge';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { openDb, runMigrations, type Database } from '@when/db';
import type { Kysely } from 'kysely';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueuePurgeAppointment: vi.fn() }));
import { enqueuePurgeAppointment } from '../workflow';
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { findAppointment, openDb, runMigrations, withWriteTransaction } from '@when/db';
import { generateVideoChat } from '@when/jobs';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { requestVideoChat } from './request-video-chat';
const { runWorkflow } = vi.hoisted(() => ({ runWorkflow: vi.fn() }));
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { classifyReschedule, rescheduleAppointment } from './reschedule';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import {
openDb,
runMigrations,
@@ -9,7 +9,7 @@ import {
type Appointment
} from '@when/db';
import type { WhenConfiguration } from '@when/config';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
vi.mock('../workflow', () => ({ enqueueAppointmentReconciliation: vi.fn() }));
import { enqueueAppointmentReconciliation } from '../workflow';
@@ -3,7 +3,7 @@ import { toPublicEventType, toPublicAppointment } from './sanitize';
import { resolveBookingDurations } from './duration';
import type { Meeting } from '@when/config';
import type { Appointment } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
const baseEventType: Meeting = {
...validConfig.meetings['30-min-chat'],
+7 -2
View File
@@ -1,5 +1,5 @@
import type { Handle } from '@sveltejs/kit/hooks';
import { afterAll, afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { Handle } from '@sveltejs/kit';
import { parseSetCookieHeader } from 'better-auth/cookies';
import { validConfig } from '../__fixtures__/valid-config';
import { userLoginsTotal } from '../metrics';
@@ -43,7 +43,12 @@ function event(path: string, body?: unknown, headers: Record<string, string> = {
url,
request: new Request(url, {
method: body === undefined ? 'GET' : 'POST',
headers: { origin, 'content-type': 'application/json', 'x-real-ip': address, ...headers },
headers: {
origin,
'content-type': 'application/json',
'x-real-ip': address,
...headers
},
...(body === undefined ? {} : { body: JSON.stringify(body) })
}),
locals: {}
+2 -2
View File
@@ -1,5 +1,5 @@
import { building } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit/hooks';
import { building } from '$app/env';
import { betterAuth, type BetterAuthOptions } from 'better-auth/minimal';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import type { WhenConfiguration } from '@when/config';
@@ -1,7 +1,7 @@
import { expect, test } from 'vitest';
import { buildAuthOptions, verifyCredentials } from './providers';
import { requireAuthSecret } from '$lib/server/auth/secret';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { requireAuthSecret } from '#lib/server/auth/secret.js';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { WhenConfiguration } from '@when/config';
test('credentials config registers only the YAML credentials plugin', () => {
@@ -1,11 +1,11 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import type { WhenConfiguration } from '@when/config';
import type { Appointment } from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { resolveBookingDurations, resolveDuration } from '../appointment/duration';
const h = vi.hoisted(() => ({ nowMs: 0 }));
vi.mock('$lib/server/clock', () => ({ systemClock: { nowMs: () => h.nowMs } }));
vi.mock('#lib/server/clock.js', () => ({ systemClock: { nowMs: () => h.nowMs } }));
vi.mock('@when/db', async (importOriginal) => {
const actual = await importOriginal<typeof import('@when/db')>();
return { ...actual, getBusyIntervals: vi.fn(actual.getBusyIntervals) };
@@ -2,7 +2,7 @@ import type { Kysely } from 'kysely';
import { getBusyIntervals, type Appointment, type BusyInterval, type Database } from '@when/db';
import { busyCalendarsFor } from '@when/calendar';
import type { Meeting, WhenConfiguration } from '@when/config';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
import { computeSlots, type ComputeOptions } from './calc';
import { mergeBlocks } from './intervals';
import { loadAppointmentBlocks } from './blocks';
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
import { computeSlots } from '$lib/server/availability';
import type { AvailabilitySettings, Interval, TimeRange } from '$lib/server/availability';
import { computeSlots } from '#lib/server/availability/index.js';
import type { AvailabilitySettings, Interval, TimeRange } from '#lib/server/availability/index.js';
const NYC = 'America/New_York';
const PARIS = 'Europe/Paris';
@@ -6,7 +6,7 @@ import {
withWriteTransaction,
type NewAppointment
} from '@when/db';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { hasAppointmentConflict } from './conflicts';
const meeting = {
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
import { mergeBlocks, overlapsAny } from './intervals';
import { generateSlots } from '$lib/server/availability/slots';
import { generateSlots } from '#lib/server/availability/slots.js';
const I = (s: string) => Temporal.Instant.from(s);
const interval = (s: string, e: string) => ({ start: I(s), end: I(e) });
@@ -4,7 +4,7 @@ import {
resolveAvailabilitySettings,
resolveAvailabilitySettingsById
} from './settings';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { validateConfig, type WhenConfiguration } from '@when/config';
test('per-event settings flow through', () => {
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
import { buildBaseWindows, candidateDates, localToInstant, weekdayOf } from './windows';
import type { TimeRange, WeeklySchedule } from '$lib/server/availability/types';
import type { TimeRange, WeeklySchedule } from '#lib/server/availability/types.js';
const NYC = 'America/New_York';
@@ -1,7 +1,7 @@
import { expect, test } from 'vitest';
import type { ServiceStatus } from '@when/db';
import type { WhenConfiguration } from '@when/config';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { evaluateCalendarStatuses } from './health.js';
const START = Temporal.Instant.from('2026-05-01T00:00:00Z');
@@ -4,7 +4,7 @@ import type { ServiceOutcome } from '@when/db';
import { fetchBusyIntervals } from '@when/calendar';
import { getOpenWorkflow } from '@when/jobs';
import type { WhenConfiguration } from '@when/config';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import { listCalendars, probeCalendar } from './status';
vi.mock('@when/calendar', async (importOriginal) => {
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Kysely } from 'kysely';
import { allCalendars, calendarTarget, type WhenConfiguration } from '@when/config';
import type { Database } from '@when/db';
import { getOpenWorkflow, testCalendar } from '@when/jobs';
import { workerReachable } from '$lib/server/worker';
import { workerReachable } from '#lib/server/worker.js';
import { lastCalendarRefreshAt, listServiceStatus } from '@when/db';
import { evaluateCalendarStatuses } from './health';
+1 -1
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { stringify } from 'yaml';
import { bootConfig } from './boot';
import { ConfigError } from '@when/config';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
let dir: string;
beforeEach(async () => {
+2 -2
View File
@@ -3,8 +3,8 @@ import type { WhenConfiguration } from '@when/config';
import { senderEmail } from '@when/config';
import { listServiceStatus, type Database } from '@when/db';
import { getOpenWorkflow, testEmail } from '@when/jobs';
import { observedFrom, type ObservedView } from '$lib/server/observed';
import { workerReachable } from '$lib/server/worker';
import { observedFrom, type ObservedView } from '#lib/server/observed.js';
import { workerReachable } from '#lib/server/worker.js';
const RESULT_TIMEOUT_MS = 30_000;
+1 -1
View File
@@ -2,7 +2,7 @@ import { render } from 'svelte/server';
import { ImageResponse } from 'takumi-js/response';
import { Renderer } from 'takumi-js/node';
import type { Appearance, WhenConfiguration } from '@when/config';
import OpengraphImage from '$lib/opengraph/OpengraphImage.svelte';
import OpengraphImage from '#lib/opengraph/OpengraphImage.svelte';
import { getConfig } from './state.js';
import { hexToRgba } from './color.js';
import { FALLBACK_FONT_NAME, bundledFontUrls, fontStack, isBundledFont } from './fonts.js';
@@ -1,6 +1,6 @@
import type { Cookies } from '@sveltejs/kit';
type CookieOptions = Parameters<Cookies['set']>[2];
type CookieOptions = NonNullable<Parameters<Cookies['set']>[2]>;
export const STATE_COOKIE = 'when_google_oauth';
+2 -2
View File
@@ -4,8 +4,8 @@ import type { Database } from '@when/db';
import { getProviderAdapter, type ProviderCalendar } from '@when/calendar';
import { listServiceStatus } from '@when/db';
import { getOpenWorkflow, listProviderCalendars, testProvider } from '@when/jobs';
import { observedFrom, type ObservedView } from '$lib/server/observed';
import { workerReachable } from '$lib/server/worker';
import { observedFrom, type ObservedView } from '#lib/server/observed.js';
import { workerReachable } from '#lib/server/worker.js';
import { googleRedirectUri } from './google-connect';
const PROBE_TIMEOUT_MS = 30_000;
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest';
import { buildTimezoneEntry, getTimezoneIndex, searchTimezones } from '$lib/timezones';
import { buildTimezoneEntry, getTimezoneIndex, searchTimezones } from '#lib/timezones.js';
describe('buildTimezoneEntry', () => {
test('extracts city, offset, and timezone name for America/Los_Angeles', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { tzCity, tzOffset } from '$lib/datetime';
import { tzCity, tzOffset } from '#lib/datetime.js';
export interface TimezoneEntry {
tz: string;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getConfig } from '$lib/server/state';
import { getConfig } from '#lib/server/state.js';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { durationsOf } from '@when/config';
import { getConfig } from '$lib/server/state';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { getConfig } from '#lib/server/state.js';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import IconShieldCheck from 'virtual:icons/ph/shield-check';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
import ThemeToggle from '#lib/components/ThemeToggle.svelte';
let { data } = $props();
</script>
@@ -1,14 +1,14 @@
import { error, fail, redirect } from '@sveltejs/kit';
import { shouldAttachVideoChat } from '@when/video-chat';
import { isEditAllowed, isViewAllowed } from '$lib/server/appointment/access';
import { isEditAllowed, isViewAllowed } from '#lib/server/appointment/access.js';
import {
isActiveStatus,
resolveAppointmentActions,
type AppointmentRole
} from '$lib/server/appointment/actions';
import { buildAddToCalendarLinks } from '$lib/server/calendar-links';
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
} from '#lib/server/appointment/actions.js';
import { buildAddToCalendarLinks } from '#lib/server/calendar-links/index.js';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import {
findAppointment,
findChainTip,
@@ -17,13 +17,13 @@ import {
parseActionLog,
type Appointment
} from '@when/db';
import { toPublicAppointment } from '$lib/server/appointment/sanitize';
import { readMeetingSnapshot } from '$lib/server/appointment/snapshot';
import { resolveBookingDurations } from '$lib/server/appointment/duration';
import { toPublicAppointment } from '#lib/server/appointment/sanitize.js';
import { readMeetingSnapshot } from '#lib/server/appointment/snapshot.js';
import { resolveBookingDurations } from '#lib/server/appointment/duration.js';
import type { Actions, PageServerLoad } from './$types';
import { cancelAppointment } from '$lib/server/appointment/cancel';
import { validateReason } from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { cancelAppointment } from '#lib/server/appointment/cancel.js';
import { validateReason } from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
type ClockStatus = 'upcoming' | 'in_progress' | 'concluded';
@@ -1,8 +1,7 @@
<script lang="ts">
import { Dialog } from 'bits-ui';
import { applyAction, deserialize, enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
import { invalidateAll } from '$app/navigation';
import { deserialize, enhance, type SubmitFunction } from '$app/forms';
import { refreshAll } from '$app/navigation';
import { tick } from 'svelte';
import IconCalendarBlank from 'virtual:icons/ph/calendar-blank';
import IconClock from 'virtual:icons/ph/clock';
@@ -13,14 +12,14 @@
import IconNote from 'virtual:icons/ph/note';
import IconPencilSimple from 'virtual:icons/ph/pencil-simple';
import IconVideo from 'virtual:icons/ph/video-conference';
import AppointmentActions from '$lib/components/AppointmentActions.svelte';
import DurationEditor from '$lib/components/DurationEditor.svelte';
import CharacterCount from '$lib/components/CharacterCount.svelte';
import AppointmentLog from '$lib/components/AppointmentLog.svelte';
import AppointmentQuestions from '$lib/components/AppointmentQuestions.svelte';
import AddToCalendar from '$lib/components/AddToCalendar.svelte';
import { formatDateShort, formatWeekday, formatTimeRange, formatTzShort } from '$lib/datetime';
import { LIMIT_LONG, LIMIT_REASON } from '$lib/forms/validation';
import AppointmentActions from '#lib/components/AppointmentActions.svelte';
import DurationEditor from '#lib/components/DurationEditor.svelte';
import CharacterCount from '#lib/components/CharacterCount.svelte';
import AppointmentLog from '#lib/components/AppointmentLog.svelte';
import AppointmentQuestions from '#lib/components/AppointmentQuestions.svelte';
import AddToCalendar from '#lib/components/AddToCalendar.svelte';
import { formatDateShort, formatWeekday, formatTimeRange, formatTzShort } from '#lib/datetime.js';
import { LIMIT_LONG, LIMIT_REASON } from '#lib/forms/validation.js';
let { data, form } = $props();
@@ -52,15 +51,9 @@
return;
}
declining = true;
return async ({ result, update }) => {
return async ({ update }) => {
try {
if (result.type === 'failure') {
await applyAction(result);
} else {
// A cross-route success must also clear any earlier form error.
if (result.type === 'success') await applyAction(result);
await update();
}
await update({ navigate: false });
} finally {
declining = false;
}
@@ -89,7 +82,7 @@
const body = await res.text();
const result = deserialize(body);
if (result.type === 'success') {
await invalidateAll();
await refreshAll();
} else {
const error = result.type === 'failure' ? result.data?.error : undefined;
videoChatError = { message: typeof error === 'string' ? error : undefined };
@@ -422,7 +415,11 @@
{formatDateShort(data.rescheduledFrom.start_time, displayTz)}
</a>
{/if}
{#if data.rescheduledFrom && data.latestAppointment}&nbsp;·&nbsp;{/if}
{#if data.rescheduledFrom && data.latestAppointment}
&nbsp;&middot;&nbsp;
{/if}
{#if data.latestAppointment}
<a
class="state-meta-link"
@@ -454,7 +451,9 @@
{formatTimeRange(data.appointment.start_time, data.appointment.end_time, displayTz)}
</div>
<div class="detail-secondary">
{formatTzShort(displayTz)}{#if zonesDiffer}&nbsp;&middot; you{/if}
{formatTzShort(displayTz)}{#if zonesDiffer}
&nbsp;&middot; you
{/if}
</div>
{#if zonesDiffer}
<div class="detail-secondary tz-extra">
@@ -462,8 +461,7 @@
data.appointment.start_time,
data.appointment.end_time,
counterpartTz
)}
&middot; {formatTzShort(counterpartTz)} &middot; {counterpartName}
)} · {formatTzShort(counterpartTz)} · {counterpartName}
</div>
{/if}
</div>
@@ -555,8 +553,9 @@
<div class="detail-text">
<div class="detail-primary">People</div>
<div class="detail-secondary">
{data.appointment.guest_name}{#if isHost && data.appointment.guest_email}&nbsp;&lt;{data
.appointment.guest_email}&gt;{/if}
{data.appointment.guest_name}{#if isHost && data.appointment.guest_email}
&nbsp;&lt;{data.appointment.guest_email}&gt;
{/if}
</div>
<div class="detail-secondary">
{data.user.name} (host)
@@ -609,10 +608,8 @@
<button
type="button"
class="decide-btn decide-decline"
onclick={() => (declineDialogOpen = true)}
onclick={() => (declineDialogOpen = true)}>Decline</button
>
Decline
</button>
</div>
{/if}
{#if data.actions.accept.allowed}
@@ -1,11 +1,11 @@
import { error, isHttpError } from '@sveltejs/kit';
import { isViewAllowed } from '$lib/server/appointment/access';
import { systemClock } from '$lib/server/clock';
import { isViewAllowed } from '#lib/server/appointment/access.js';
import { systemClock } from '#lib/server/clock.js';
import { buildIcs } from '@when/calendar';
import { senderEmail } from '@when/config';
import { findAppointment } from '@when/db';
import { getConfig, getDb } from '$lib/server/state';
import { icsDownloadsTotal } from '$lib/server/metrics';
import { getConfig, getDb } from '#lib/server/state.js';
import { icsDownloadsTotal } from '#lib/server/metrics.js';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params, url }) => {
@@ -1,5 +1,5 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { Appointment } from '@when/db';
const h = vi.hoisted(() => ({
@@ -8,12 +8,12 @@ const h = vi.hoisted(() => ({
buildIcs: vi.fn(() => 'BEGIN:VCALENDAR')
}));
vi.mock('$lib/server/state', () => ({ getConfig: () => validConfig, getDb: () => ({}) }));
vi.mock('#lib/server/state.js', () => ({ getConfig: () => validConfig, getDb: () => ({}) }));
vi.mock('@when/db', async (io) => ({
...(await io<typeof import('@when/db')>()),
findAppointment: async () => h.row.current
}));
vi.mock('$lib/server/appointment/access', () => ({
vi.mock('#lib/server/appointment/access.js', () => ({
isViewAllowed: h.viewAllowed
}));
vi.mock('@when/calendar', async (io) => ({
@@ -1,21 +1,21 @@
import { error, fail, redirect } from '@sveltejs/kit';
import { findAppointment } from '@when/db';
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { resolveFormFields } from '@when/config';
import { isRescheduleSlotBookable, loadAvailability } from '$lib/server/availability/booking';
import { resolveDuration } from '$lib/server/appointment/duration';
import { isViewAllowed } from '$lib/server/appointment/access';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { classifyReschedule, rescheduleAppointment } from '$lib/server/appointment/reschedule';
import { isRescheduleSlotBookable, loadAvailability } from '#lib/server/availability/booking.js';
import { resolveDuration } from '#lib/server/appointment/duration.js';
import { isViewAllowed } from '#lib/server/appointment/access.js';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import { classifyReschedule, rescheduleAppointment } from '#lib/server/appointment/reschedule.js';
import {
parseAndValidateAppointmentForm,
parseSlot,
resolveTimezone,
validateReason
} from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { toPublicAppointment, toPublicEventType } from '$lib/server/appointment/sanitize';
} from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
import { toPublicAppointment, toPublicEventType } from '#lib/server/appointment/sanitize.js';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, url, locals }) => {
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import AppointmentWizard from '$lib/components/AppointmentWizard.svelte';
import AppointmentWizard from '#lib/components/AppointmentWizard.svelte';
let { data, form } = $props();
</script>
@@ -1,9 +1,9 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { Appointment } from '@when/db';
import type { Meeting } from '@when/config';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { resolveBookingDurations } from '$lib/server/appointment/duration';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import { resolveBookingDurations } from '#lib/server/appointment/duration.js';
const h = vi.hoisted(() => ({
db: {},
@@ -21,7 +21,7 @@ const h = vi.hoisted(() => ({
toPublicEventType: vi.fn()
}));
vi.mock('$lib/server/state', () => ({ getConfig: () => validConfig, getDb: () => h.db }));
vi.mock('#lib/server/state.js', () => ({ getConfig: () => validConfig, getDb: () => h.db }));
vi.mock('@when/db', async (io) => {
const original = await io<typeof import('@when/db')>();
return {
@@ -29,19 +29,19 @@ vi.mock('@when/db', async (io) => {
findAppointment: async () => h.row.current
};
});
vi.mock('$lib/server/appointment/access', () => ({
vi.mock('#lib/server/appointment/access.js', () => ({
isViewAllowed: h.viewAllowed
}));
vi.mock('$lib/server/appointment/reschedule', () => ({
vi.mock('#lib/server/appointment/reschedule.js', () => ({
classifyReschedule: h.classify,
rescheduleAppointment: h.reschedule
}));
vi.mock('$lib/server/availability/booking', () => ({
vi.mock('#lib/server/availability/booking.js', () => ({
loadAvailability: h.loadAvailability,
isRescheduleSlotBookable: h.isRescheduleSlotBookable
}));
vi.mock('$lib/server/appointment/form.server', async (io) => {
const original = await io<typeof import('$lib/server/appointment/form.server')>();
vi.mock('#lib/server/appointment/form.server.js', async (io) => {
const original = await io<typeof import('#lib/server/appointment/form.server.js')>();
return {
...original,
parseAndValidateAppointmentForm: h.parseForm,
@@ -49,12 +49,12 @@ vi.mock('$lib/server/appointment/form.server', async (io) => {
validateReason: h.validateReason
};
});
vi.mock('$lib/server/appointment/duration', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/duration')>();
vi.mock('#lib/server/appointment/duration.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/duration.js')>();
return { ...original, resolveDuration: h.resolveDuration };
});
vi.mock('$lib/server/appointment/sanitize', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/sanitize')>();
vi.mock('#lib/server/appointment/sanitize.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/sanitize.js')>();
return {
...original,
toPublicAppointment: h.toPublicAppointment,
@@ -1,15 +1,15 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { actions, load } from './+page.server';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { Appointment } from '@when/db';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { resolveBookingDurations } from '$lib/server/appointment/duration';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import { resolveBookingDurations } from '#lib/server/appointment/duration.js';
const h = vi.hoisted(() => ({ cancelAppointment: vi.fn() }));
vi.mock('$lib/server/appointment/cancel', () => ({ cancelAppointment: h.cancelAppointment }));
vi.mock('$lib/server/appointment/duration', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/duration')>();
vi.mock('#lib/server/appointment/cancel.js', () => ({ cancelAppointment: h.cancelAppointment }));
vi.mock('#lib/server/appointment/duration.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/duration.js')>();
return { ...original, resolveBookingDurations: vi.fn(original.resolveBookingDurations) };
});
@@ -130,7 +130,7 @@ const automaticVideoAppointment: Appointment = {
])
};
vi.mock('$lib/server/state', () => ({
vi.mock('#lib/server/state.js', () => ({
getConfig: () => loadConfig,
getDb: () => mockDb
}));
@@ -1,20 +1,20 @@
import { error, fail, redirect } from '@sveltejs/kit';
import { isNewSlotBookable, loadAvailability } from '$lib/server/availability/booking';
import { logger } from '$lib/server/logger';
import { getConfig, getDb } from '$lib/server/state';
import { isNewSlotBookable, loadAvailability } from '#lib/server/availability/booking.js';
import { logger } from '#lib/server/logger.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { resolveFormFields } from '@when/config';
import { createAppointment } from '$lib/server/appointment/create';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { resolveBookingDurations, resolveDuration } from '$lib/server/appointment/duration';
import { createAppointment } from '#lib/server/appointment/create.js';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import { resolveBookingDurations, resolveDuration } from '#lib/server/appointment/duration.js';
import {
parseAndValidateAppointmentForm,
parseSlot,
resolveTimezone
} from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { normalizeDeepLinkParams } from '$lib/appointment';
import { toPublicEventType } from '$lib/server/appointment/sanitize';
import { bookingAttemptsTotal } from '$lib/server/metrics';
} from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
import { normalizeDeepLinkParams } from '#lib/appointment.js';
import { toPublicEventType } from '#lib/server/appointment/sanitize.js';
import { bookingAttemptsTotal } from '#lib/server/metrics.js';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, url, locals }) => {
@@ -1,5 +1,5 @@
<script lang="ts">
import AppointmentWizard from '$lib/components/AppointmentWizard.svelte';
import AppointmentWizard from '#lib/components/AppointmentWizard.svelte';
let { data, form } = $props();
</script>
@@ -1,9 +1,9 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { Meeting, WhenConfiguration } from '@when/config';
import type { AppointmentRole } from '$lib/server/appointment/actions';
import { resolveBookingDurations } from '$lib/server/appointment/duration';
import { toPublicEventType } from '$lib/server/appointment/sanitize';
import type { AppointmentRole } from '#lib/server/appointment/actions.js';
import { resolveBookingDurations } from '#lib/server/appointment/duration.js';
import { toPublicEventType } from '#lib/server/appointment/sanitize.js';
const h = vi.hoisted(() => ({
cfg: { current: null as unknown as WhenConfiguration },
@@ -17,26 +17,26 @@ const h = vi.hoisted(() => ({
resolveDuration: vi.fn()
}));
vi.mock('$lib/server/state', () => ({ getConfig: () => h.cfg.current, getDb: h.getDb }));
vi.mock('$lib/server/availability/booking', () => ({
vi.mock('#lib/server/state.js', () => ({ getConfig: () => h.cfg.current, getDb: h.getDb }));
vi.mock('#lib/server/availability/booking.js', () => ({
loadAvailability: h.loadAvailability,
isNewSlotBookable: h.isNewSlotBookable
}));
vi.mock('$lib/server/appointment/create', () => ({ createAppointment: h.createAppointment }));
vi.mock('$lib/server/appointment/form.server', async (io) => {
const original = await io<typeof import('$lib/server/appointment/form.server')>();
vi.mock('#lib/server/appointment/create.js', () => ({ createAppointment: h.createAppointment }));
vi.mock('#lib/server/appointment/form.server.js', async (io) => {
const original = await io<typeof import('#lib/server/appointment/form.server.js')>();
return {
...original,
parseAndValidateAppointmentForm: h.parseForm,
resolveTimezone: h.resolveTimezone
};
});
vi.mock('$lib/server/appointment/duration', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/duration')>();
vi.mock('#lib/server/appointment/duration.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/duration.js')>();
return { ...original, resolveDuration: h.resolveDuration };
});
vi.mock('$lib/server/appointment/sanitize', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/sanitize')>();
vi.mock('#lib/server/appointment/sanitize.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/sanitize.js')>();
return { ...original, toPublicEventType: vi.fn(original.toPublicEventType) };
});
+2 -2
View File
@@ -1,12 +1,12 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
import type { Meeting, WhenConfiguration } from '@when/config';
const h = vi.hoisted(() => ({
cfg: { current: null as unknown as WhenConfiguration }
}));
vi.mock('$lib/server/state', () => ({ getConfig: () => h.cfg.current }));
vi.mock('#lib/server/state.js', () => ({ getConfig: () => h.cfg.current }));
import { load } from './+page.server';
@@ -1,7 +1,7 @@
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
import { toAppointmentView } from '$lib/server/appointments';
import { evaluateCalendarStatuses } from '$lib/server/calendar/health';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { toAppointmentView } from '#lib/server/appointments.js';
import { evaluateCalendarStatuses } from '#lib/server/calendar/health.js';
import { countAppointments, listAppointmentsPage, listServiceStatus } from '@when/db';
import { allCalendars } from '@when/config';
import { sql } from 'kysely';
@@ -1,7 +1,7 @@
<script lang="ts">
import IconWarning from 'virtual:icons/ph/warning';
import AdminAlert from '$lib/components/AdminAlert.svelte';
import AdminPage from '$lib/components/AdminPage.svelte';
import AdminAlert from '#lib/components/AdminAlert.svelte';
import AdminPage from '#lib/components/AdminPage.svelte';
let { data } = $props();
@@ -1,18 +1,18 @@
import { fail, redirect } from '@sveltejs/kit';
import { getConfig, getDb } from '$lib/server/state';
import { getConfig, getDb } from '#lib/server/state.js';
import { findAppointment, isChainTerminal } from '@when/db';
import { acceptAppointment } from '$lib/server/appointment/accept';
import { declineAppointment } from '$lib/server/appointment/decline';
import { cancelAppointment } from '$lib/server/appointment/cancel';
import { purgeAppointment } from '$lib/server/appointment/purge';
import { editAppointment } from '$lib/server/appointment/edit';
import { changeAppointmentDuration } from '$lib/server/appointment/change-duration';
import { validateDeclineNote, validateReason } from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { rotateGuestTokenTransition } from '$lib/server/appointment/transitions';
import { requestVideoChat } from '$lib/server/appointment/request-video-chat';
import { acceptAppointment } from '#lib/server/appointment/accept.js';
import { declineAppointment } from '#lib/server/appointment/decline.js';
import { cancelAppointment } from '#lib/server/appointment/cancel.js';
import { purgeAppointment } from '#lib/server/appointment/purge.js';
import { editAppointment } from '#lib/server/appointment/edit.js';
import { changeAppointmentDuration } from '#lib/server/appointment/change-duration.js';
import { validateDeclineNote, validateReason } from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
import { rotateGuestTokenTransition } from '#lib/server/appointment/transitions.js';
import { requestVideoChat } from '#lib/server/appointment/request-video-chat.js';
import type { Actions, PageServerLoad } from './$types';
import { systemClock } from '$lib/server/clock';
import { systemClock } from '#lib/server/clock.js';
export const load: PageServerLoad = async ({ params }) => {
// A direct GET request redirects back to the public appointment detail page.
@@ -1,11 +1,11 @@
import { fail, redirect } from '@sveltejs/kit';
import { findAppointment } from '@when/db';
import { getConfig, getDb } from '$lib/server/state';
import { isRescheduleSlotBookable } from '$lib/server/availability/booking';
import { resolveDuration } from '$lib/server/appointment/duration';
import { rescheduleAppointment } from '$lib/server/appointment/reschedule';
import { parseSlot, validateReason } from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { getConfig, getDb } from '#lib/server/state.js';
import { isRescheduleSlotBookable } from '#lib/server/availability/booking.js';
import { resolveDuration } from '#lib/server/appointment/duration.js';
import { rescheduleAppointment } from '#lib/server/appointment/reschedule.js';
import { parseSlot, validateReason } from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
import type { Actions } from './$types';
export const actions: Actions = {
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { validConfig } from '$lib/server/__fixtures__/valid-config';
import { validConfig } from '#lib/server/__fixtures__/valid-config.js';
const h = vi.hoisted(() => ({
db: {},
@@ -10,21 +10,21 @@ const h = vi.hoisted(() => ({
resolveDuration: vi.fn()
}));
vi.mock('$lib/server/state', () => ({ getConfig: () => validConfig, getDb: () => h.db }));
vi.mock('#lib/server/state.js', () => ({ getConfig: () => validConfig, getDb: () => h.db }));
vi.mock('@when/db', async (io) => {
const original = await io<typeof import('@when/db')>();
return { ...original, findAppointment: h.findAppointment };
});
vi.mock('$lib/server/availability/booking', () => ({
vi.mock('#lib/server/availability/booking.js', () => ({
isRescheduleSlotBookable: h.isRescheduleSlotBookable
}));
vi.mock('$lib/server/appointment/reschedule', () => ({ rescheduleAppointment: h.reschedule }));
vi.mock('$lib/server/appointment/duration', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/duration')>();
vi.mock('#lib/server/appointment/reschedule.js', () => ({ rescheduleAppointment: h.reschedule }));
vi.mock('#lib/server/appointment/duration.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/appointment/duration.js')>();
return { ...original, resolveDuration: h.resolveDuration };
});
vi.mock('$lib/server/appointment/form.server', async (io) => {
const original = await io<typeof import('$lib/server/appointment/form.server')>();
vi.mock('#lib/server/appointment/form.server.js', async (io) => {
const original = await io<typeof import('#lib/server/appointment/form.server.js')>();
return { ...original, validateReason: h.validateReason };
});
@@ -1,21 +1,22 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { actions, load } from './+page.server';
import { requestVideoChat } from '$lib/server/appointment/request-video-chat';
import { changeAppointmentDuration } from '$lib/server/appointment/change-duration';
import { declineAppointment } from '$lib/server/appointment/decline';
import { requestVideoChat } from '#lib/server/appointment/request-video-chat.js';
import { changeAppointmentDuration } from '#lib/server/appointment/change-duration.js';
import { declineAppointment } from '#lib/server/appointment/decline.js';
import { findAppointment, type Appointment } from '@when/db';
import type { RequestEvent } from './$types';
vi.mock('$lib/server/appointment/context', () => ({ appointmentContext: () => context }));
vi.mock('$lib/server/appointment/request-video-chat', () => ({ requestVideoChat: vi.fn() }));
vi.mock('$lib/server/appointment/change-duration', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/appointment/change-duration')>();
vi.mock('#lib/server/appointment/context.js', () => ({ appointmentContext: () => context }));
vi.mock('#lib/server/appointment/request-video-chat.js', () => ({ requestVideoChat: vi.fn() }));
vi.mock('#lib/server/appointment/change-duration.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('#lib/server/appointment/change-duration.js')>();
return { ...original, changeAppointmentDuration: vi.fn() };
});
vi.mock('$lib/server/appointment/decline', () => ({ declineAppointment: vi.fn() }));
vi.mock('$lib/server/state', async (importOriginal) => {
const original = await importOriginal<typeof import('$lib/server/state')>();
vi.mock('#lib/server/appointment/decline.js', () => ({ declineAppointment: vi.fn() }));
vi.mock('#lib/server/state.js', async (importOriginal) => {
const original = await importOriginal<typeof import('#lib/server/state.js')>();
return { ...original, getDb: () => context.db };
});
vi.mock('@when/db', async (importOriginal) => {
@@ -1,5 +1,5 @@
import { systemClock } from '$lib/server/clock';
import { getDb } from '$lib/server/state';
import { systemClock } from '#lib/server/clock.js';
import { getDb } from '#lib/server/state.js';
import { countAppointments } from '@when/db';
import { sql } from 'kysely';
import type { LayoutServerLoad } from './$types';
@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
import AdminAlert from '$lib/components/AdminAlert.svelte';
import AdminPage from '$lib/components/AdminPage.svelte';
import AdminAlert from '#lib/components/AdminAlert.svelte';
import AdminPage from '#lib/components/AdminPage.svelte';
import IconCaretLeft from 'virtual:icons/ph/caret-left';
import IconCaretRight from 'virtual:icons/ph/caret-right';
@@ -1,13 +1,13 @@
import { redirect } from '@sveltejs/kit';
import { getConfig, getDb } from '$lib/server/state';
import { getConfig, getDb } from '#lib/server/state.js';
import { findAppointment, isChainTerminal } from '@when/db';
import { purgeAppointment } from '$lib/server/appointment/purge';
import { cancelAppointment } from '$lib/server/appointment/cancel';
import { acceptAppointment } from '$lib/server/appointment/accept';
import { declineAppointment } from '$lib/server/appointment/decline';
import { validateReason } from '$lib/server/appointment/form.server';
import { appointmentContext } from '$lib/server/appointment/context';
import { systemClock } from '$lib/server/clock';
import { purgeAppointment } from '#lib/server/appointment/purge.js';
import { cancelAppointment } from '#lib/server/appointment/cancel.js';
import { acceptAppointment } from '#lib/server/appointment/accept.js';
import { declineAppointment } from '#lib/server/appointment/decline.js';
import { validateReason } from '#lib/server/appointment/form.server.js';
import { appointmentContext } from '#lib/server/appointment/context.js';
import { systemClock } from '#lib/server/clock.js';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = () => {
@@ -1,6 +1,6 @@
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
import { toAppointmentView } from '$lib/server/appointments';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { toAppointmentView } from '#lib/server/appointments.js';
import { countAppointments, listAppointmentsPage } from '@when/db';
import type { PageServerLoad } from './$types';
@@ -1,5 +1,5 @@
<script lang="ts">
import AppointmentsList from '$lib/components/AppointmentsList.svelte';
import AppointmentsList from '#lib/components/AppointmentsList.svelte';
let { data } = $props();
</script>
@@ -1,6 +1,6 @@
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
import { toAppointmentView } from '$lib/server/appointments';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { toAppointmentView } from '#lib/server/appointments.js';
import { countAppointments, listAppointmentsPage } from '@when/db';
import type { PageServerLoad } from './$types';
@@ -1,5 +1,5 @@
<script lang="ts">
import AppointmentsList from '$lib/components/AppointmentsList.svelte';
import AppointmentsList from '#lib/components/AppointmentsList.svelte';
let { data } = $props();
</script>
@@ -1,6 +1,6 @@
import { systemClock } from '$lib/server/clock';
import { getConfig, getDb } from '$lib/server/state';
import { toAppointmentView } from '$lib/server/appointments';
import { systemClock } from '#lib/server/clock.js';
import { getConfig, getDb } from '#lib/server/state.js';
import { toAppointmentView } from '#lib/server/appointments.js';
import { countAppointments, listAppointmentsPage } from '@when/db';
import type { PageServerLoad } from './$types';
@@ -1,5 +1,5 @@
<script lang="ts">
import AppointmentsList from '$lib/components/AppointmentsList.svelte';
import AppointmentsList from '#lib/components/AppointmentsList.svelte';
let { data } = $props();
</script>
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more