two workspace screens backed by one app folder: the collection list on the left, the selected calendar's agenda or address book on the right. both read the officer-caldav sidecar through the /api/caldav auth proxy, which holds no DAV credentials of its own. the selected collection is a DAV path with slashes in it, so it lives in ?collection= on the same route rather than as a path segment — still in the URL, still bookmarkable, no selection channel. an absent or unknown value resolves to the first collection inside the panels instead of redirecting, because until the list has loaded there is no canonical URL to redirect to. the agenda is a grouped list, not a month grid: the sidecar returns RRULE unexpanded, so a grid would have to invent occurrences the server never claimed existed. a repeating event gets a badge instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import { useEffect, useMemo } from 'react';
|
|
import type { LayoutNode } from 'officerdev';
|
|
import { WorkspaceView } from 'officerdev';
|
|
import { useDashboardState } from 'state/useDashboardState';
|
|
import { defaultLayout } from './defaultLayout';
|
|
|
|
// /calendar uses the Workspace/Panel system (like /transmission): the calendar list on the left, the
|
|
// selected calendar's agenda on the right. Both panels read the officer-caldav sidecar through the
|
|
// /api/caldav auth proxy, which holds no DAV credentials of its own.
|
|
//
|
|
// There is no :param route pair here and so no <Navigate> guard: a collection id is a DAV path with
|
|
// slashes in it, so the selection lives in ?collection= on this same route. An absent or unknown value
|
|
// resolves to the first collection inside the panels rather than by redirecting, because until the
|
|
// collection list has loaded there is no canonical URL to redirect to.
|
|
|
|
const ALLOWED_APP_TYPES = new Set<string | null>(['calendar-nav', 'calendar-view', null]);
|
|
|
|
function normalizeLayout(node: LayoutNode): LayoutNode {
|
|
if (node.type === 'panel') {
|
|
return ALLOWED_APP_TYPES.has(node.appType) ? node : { ...node, appType: 'calendar-view' };
|
|
}
|
|
const children = node.children.map((c) => {
|
|
const fixed = normalizeLayout(c.node);
|
|
return fixed === c.node ? c : { ...c, node: fixed };
|
|
});
|
|
const changed = children.some((c, i) => c !== node.children[i]);
|
|
return changed ? { ...node, children } : node;
|
|
}
|
|
|
|
export const CalendarScreen = () => {
|
|
const rawWorkspace = useDashboardState<LayoutNode>('screens/calendar', defaultLayout);
|
|
|
|
const workspace = useMemo(() => {
|
|
const fixed = normalizeLayout(rawWorkspace.value);
|
|
if (fixed === rawWorkspace.value) return rawWorkspace;
|
|
return { ...rawWorkspace, value: fixed };
|
|
}, [rawWorkspace]);
|
|
|
|
useEffect(() => {
|
|
if (rawWorkspace.isLoaded && workspace.value !== rawWorkspace.value) {
|
|
rawWorkspace.setValue(workspace.value);
|
|
}
|
|
}, [rawWorkspace.isLoaded, workspace.value, rawWorkspace.value]);
|
|
|
|
return (
|
|
<div className="h-full w-full pt-2">
|
|
<WorkspaceView workspace={workspace} locked />
|
|
</div>
|
|
);
|
|
};
|