add /calendar and /contacts screens over the caldav json door
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>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type { LayoutNode } from 'officerdev';
|
||||
import { WorkspaceView } from 'officerdev';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
// /contacts — the CardDAV half of the same sidecar as /calendar; see CalendarScreen for why the selected
|
||||
// collection is a query param rather than a route segment.
|
||||
|
||||
const ALLOWED_APP_TYPES = new Set<string | null>(['contacts-nav', 'contacts-view', null]);
|
||||
|
||||
function normalizeLayout(node: LayoutNode): LayoutNode {
|
||||
if (node.type === 'panel') {
|
||||
return ALLOWED_APP_TYPES.has(node.appType) ? node : { ...node, appType: 'contacts-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 ContactsScreen = () => {
|
||||
const rawWorkspace = useDashboardState<LayoutNode>('screens/contacts', 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { LayoutNode } from 'officerdev';
|
||||
|
||||
export const defaultLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'contacts-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'contacts-nav', appType: 'contacts-nav' }, size: 22 },
|
||||
{ node: { type: 'panel', id: 'contacts-view', appType: 'contacts-view' }, size: 78 },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ContactsScreen';
|
||||
Reference in New Issue
Block a user