refactor(web): upgrade to SvelteKit 3 #107
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@when/web': patch
|
||||
---
|
||||
|
||||
Upgrade to SvelteKit 3 with native subpath imports, current framework APIs, and trusted reverse-proxy origin handling.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
@@ -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,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');
|
||||
|
||||
|
||||
@@ -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/*"
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,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,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;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import { tzCity, tzOffset } from '$lib/datetime';
|
||||
import { tzCity, tzOffset } from '#lib/datetime.js';
|
||||
|
||||
export interface TimezoneEntry {
|
||||
tz: string;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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,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} · {/if}
|
||||
|
||||
{#if data.rescheduledFrom && data.latestAppointment}
|
||||
·
|
||||
{/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} · you{/if}
|
||||
{formatTzShort(displayTz)}{#if zonesDiffer}
|
||||
· you
|
||||
{/if}
|
||||
</div>
|
||||
{#if zonesDiffer}
|
||||
<div class="detail-secondary tz-extra">
|
||||
@@ -462,8 +461,7 @@
|
||||
data.appointment.start_time,
|
||||
data.appointment.end_time,
|
||||
counterpartTz
|
||||
)}
|
||||
· {formatTzShort(counterpartTz)} · {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} <{data
|
||||
.appointment.guest_email}>{/if}
|
||||
{data.appointment.guest_name}{#if isHost && data.appointment.guest_email}
|
||||
<{data.appointment.guest_email}>
|
||||
{/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) };
|
||||
});
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user