Complete recurrence editing and calendar preferences
This commit is contained in:
@@ -51,6 +51,10 @@ export type CalendarEvent = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
instance_id?: string | null;
|
||||
series_event_id?: string | null;
|
||||
is_occurrence?: boolean;
|
||||
is_override?: boolean;
|
||||
};
|
||||
|
||||
export type CalendarCollectionListResponse = { calendars: CalendarCollection[] };
|
||||
@@ -243,6 +247,29 @@ export type CalendarEventCreatePayload = {
|
||||
};
|
||||
|
||||
export type CalendarEventUpdatePayload = Partial<CalendarEventCreatePayload>;
|
||||
export type CalendarViewPreferences = {
|
||||
dim_weekends: boolean;
|
||||
dim_off_hours: boolean;
|
||||
workday_start_hour: number;
|
||||
workday_end_hour: number;
|
||||
continuous_virtualization: boolean;
|
||||
continuous_overscan_weeks: number;
|
||||
alternate_continuous_months: boolean;
|
||||
overridden_fields: string[];
|
||||
defaults: Record<string, boolean | number>;
|
||||
};
|
||||
export type CalendarViewPreferencesUpdatePayload = Partial<
|
||||
Pick<
|
||||
CalendarViewPreferences,
|
||||
| "dim_weekends"
|
||||
| "dim_off_hours"
|
||||
| "workday_start_hour"
|
||||
| "workday_end_hour"
|
||||
| "continuous_virtualization"
|
||||
| "continuous_overscan_weeks"
|
||||
| "alternate_continuous_months"
|
||||
>
|
||||
>;
|
||||
|
||||
export function listCalendars(settings: ApiSettings): Promise<CalendarCollectionListResponse> {
|
||||
return apiFetch<CalendarCollectionListResponse>(settings, "/api/v1/calendar/calendars");
|
||||
@@ -320,12 +347,13 @@ export function syncCalDavSource(settings: ApiSettings, sourceId: string, payloa
|
||||
|
||||
export function listCalendarEvents(
|
||||
settings: ApiSettings,
|
||||
params: { calendar_id?: string; start_at?: string; end_at?: string } = {}
|
||||
params: { calendar_id?: string; start_at?: string; end_at?: string; expand_recurring?: boolean } = {}
|
||||
): Promise<CalendarEventListResponse> {
|
||||
const search = new URLSearchParams();
|
||||
if (params.calendar_id) search.set("calendar_id", params.calendar_id);
|
||||
if (params.start_at) search.set("start_at", params.start_at);
|
||||
if (params.end_at) search.set("end_at", params.end_at);
|
||||
if (params.expand_recurring) search.set("expand_recurring", "true");
|
||||
const suffix = search.toString() ? `?${search.toString()}` : "";
|
||||
return apiFetch<CalendarEventListResponse>(settings, `/api/v1/calendar/events${suffix}`);
|
||||
}
|
||||
@@ -344,6 +372,10 @@ export function listCalendarEventsDelta(
|
||||
return apiFetch<CalendarEventDeltaResponse>(settings, `/api/v1/calendar/events/delta${suffix}`);
|
||||
}
|
||||
|
||||
export function getCalendarEvent(settings: ApiSettings, eventId: string): Promise<CalendarEvent> {
|
||||
return apiFetch<CalendarEvent>(settings, `/api/v1/calendar/events/${eventId}`);
|
||||
}
|
||||
|
||||
export function createCalendarEvent(settings: ApiSettings, payload: CalendarEventCreatePayload): Promise<CalendarEvent> {
|
||||
return apiFetch<CalendarEvent>(settings, "/api/v1/calendar/events", { method: "POST", body: JSON.stringify(payload) });
|
||||
}
|
||||
@@ -355,3 +387,49 @@ export function updateCalendarEvent(settings: ApiSettings, eventId: string, payl
|
||||
export function deleteCalendarEvent(settings: ApiSettings, eventId: string): Promise<void> {
|
||||
return apiFetch<void>(settings, `/api/v1/calendar/events/${eventId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function updateCalendarEventOccurrence(
|
||||
settings: ApiSettings,
|
||||
seriesEventId: string,
|
||||
recurrenceId: string,
|
||||
payload: CalendarEventUpdatePayload
|
||||
): Promise<CalendarEvent> {
|
||||
return apiFetch<CalendarEvent>(
|
||||
settings,
|
||||
`/api/v1/calendar/events/${seriesEventId}/occurrence`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ ...payload, recurrence_id: recurrenceId })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCalendarEventOccurrence(
|
||||
settings: ApiSettings,
|
||||
seriesEventId: string,
|
||||
recurrenceId: string
|
||||
): Promise<CalendarEvent> {
|
||||
return apiFetch<CalendarEvent>(
|
||||
settings,
|
||||
`/api/v1/calendar/events/${seriesEventId}/occurrence`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ recurrence_id: recurrenceId })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getCalendarViewPreferences(settings: ApiSettings): Promise<CalendarViewPreferences> {
|
||||
return apiFetch<CalendarViewPreferences>(settings, "/api/v1/calendar/preferences/view");
|
||||
}
|
||||
|
||||
export function updateCalendarViewPreferences(
|
||||
settings: ApiSettings,
|
||||
payload: CalendarViewPreferencesUpdatePayload
|
||||
): Promise<CalendarViewPreferences> {
|
||||
return apiFetch<CalendarViewPreferences>(
|
||||
settings,
|
||||
"/api/v1/calendar/preferences/view",
|
||||
{ method: "PATCH", body: JSON.stringify(payload) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
DateField,
|
||||
Dialog,
|
||||
SegmentedControl,
|
||||
TimeField,
|
||||
ToggleSwitch,
|
||||
useUnsavedDraftGuard,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
} from "./calendarViewModel";
|
||||
|
||||
type CalendarEventEndMode = "end" | "duration";
|
||||
type CalendarEventEditScope = "occurrence" | "series";
|
||||
type CalendarEventAdvancedPayload = Pick<
|
||||
CalendarEventCreatePayload,
|
||||
| "organizer"
|
||||
@@ -49,25 +51,40 @@ export function CalendarEventDialog({
|
||||
calendars,
|
||||
defaultCalendarId,
|
||||
event,
|
||||
editScope,
|
||||
canChooseSeries,
|
||||
seriesLoaded,
|
||||
focusDate,
|
||||
saving,
|
||||
canWrite,
|
||||
canDelete,
|
||||
onEditScopeChange,
|
||||
onCancel,
|
||||
onSave,
|
||||
onDelete
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {calendars: CalendarCollection[];defaultCalendarId: string;event: CalendarEvent | null;focusDate: Date;saving: boolean;canWrite: boolean;canDelete: boolean;onCancel: () => void;onSave: (payload: CalendarEventCreatePayload, event: CalendarEvent | null) => Promise<boolean>;onDelete: (event: CalendarEvent) => Promise<void>;}) {
|
||||
}: {
|
||||
calendars: CalendarCollection[];
|
||||
defaultCalendarId: string;
|
||||
event: CalendarEvent | null;
|
||||
editScope: CalendarEventEditScope;
|
||||
canChooseSeries: boolean;
|
||||
seriesLoaded: boolean;
|
||||
focusDate: Date;
|
||||
saving: boolean;
|
||||
canWrite: boolean;
|
||||
canDelete: boolean;
|
||||
onEditScopeChange: (scope: CalendarEventEditScope) => void;
|
||||
onCancel: () => void;
|
||||
onSave: (
|
||||
payload: CalendarEventCreatePayload,
|
||||
event: CalendarEvent | null,
|
||||
editScope: CalendarEventEditScope
|
||||
) => Promise<boolean>;
|
||||
onDelete: (
|
||||
event: CalendarEvent,
|
||||
editScope: CalendarEventEditScope
|
||||
) => Promise<void>;
|
||||
}) {
|
||||
const initialStart = event ? new Date(event.start_at) : focusDate;
|
||||
const initialEnd = event?.end_at ? new Date(event.end_at) : addHours(initialStart, 1);
|
||||
const initialAllDayEnd = event?.all_day && event.end_at ? addDays(startOfDay(initialEnd), -1) : initialEnd;
|
||||
@@ -231,7 +248,7 @@ export function CalendarEventDialog({
|
||||
if (uid.trim()) payload.uid = uid.trim();
|
||||
if (recurrenceId.trim()) payload.recurrence_id = recurrenceId.trim();
|
||||
}
|
||||
return onSave(payload, event);
|
||||
return onSave(payload, event, editScope);
|
||||
}
|
||||
|
||||
function submit(formEvent: FormEvent<HTMLFormElement>) {
|
||||
@@ -245,7 +262,7 @@ export function CalendarEventDialog({
|
||||
setConfirmingDelete(true);
|
||||
return;
|
||||
}
|
||||
void onDelete(event);
|
||||
void onDelete(event, editScope);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -273,6 +290,24 @@ export function CalendarEventDialog({
|
||||
}>
|
||||
|
||||
<form id={formId} className="calendar-dialog-form" onSubmit={submit}>
|
||||
{canChooseSeries && (
|
||||
<SegmentedControl
|
||||
ariaLabel="Recurring event edit scope"
|
||||
value={editScope}
|
||||
onChange={onEditScopeChange}
|
||||
width="fill"
|
||||
size="equal"
|
||||
disabled={saving}
|
||||
options={[
|
||||
{ id: "occurrence", label: "This occurrence" },
|
||||
{
|
||||
id: "series",
|
||||
label: seriesLoaded ? "Whole series" : "Loading series",
|
||||
disabled: !seriesLoaded
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{formError && <p className="calendar-form-error">{formError}</p>}
|
||||
<label>
|
||||
<span>i18n:govoplan-calendar.title.768e0c1c</span>
|
||||
@@ -559,4 +594,3 @@ function eventUsesDuration(event: CalendarEvent): boolean {
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,17 @@ import {
|
||||
createSyncSource,
|
||||
deleteCalendar,
|
||||
deleteCalendarEvent,
|
||||
deleteCalendarEventOccurrence,
|
||||
discoverCalDavCalendars,
|
||||
getCalendarEvent,
|
||||
getCalendarViewPreferences,
|
||||
listCalendarEvents,
|
||||
listCalendarEventsDelta,
|
||||
listCalendars,
|
||||
listSyncSources,
|
||||
syncSyncSource,
|
||||
updateCalendar,
|
||||
updateCalendarEvent,
|
||||
updateCalendarEventOccurrence,
|
||||
updateSyncSource,
|
||||
type CalendarCalDavDiscoveryPayload,
|
||||
type CalendarCollection,
|
||||
@@ -41,6 +44,7 @@ import {
|
||||
type CalendarEvent,
|
||||
type CalendarEventCreatePayload,
|
||||
type CalendarSyncSource,
|
||||
type CalendarViewPreferences as CalendarViewPreferencesResponse,
|
||||
} from
|
||||
"../../api/calendar";
|
||||
import {
|
||||
@@ -66,6 +70,7 @@ import {
|
||||
agendaDateLabel,
|
||||
agendaGroupsForDays,
|
||||
calendarEventColorStyle,
|
||||
calendarEventInstanceId,
|
||||
continuousEventWindow,
|
||||
daysBetweenCount,
|
||||
daysForMode,
|
||||
@@ -76,7 +81,6 @@ import {
|
||||
headingForMode,
|
||||
loadCalendarMode,
|
||||
loadCalendarViewPreferences,
|
||||
mergeCalendarEventDelta,
|
||||
moveEventToDay,
|
||||
moveEventToTime,
|
||||
normalizeHexColor,
|
||||
@@ -89,10 +93,19 @@ import {
|
||||
type CalendarDropTarget,
|
||||
type CalendarMode,
|
||||
type CalendarResizeEdge,
|
||||
type CalendarViewPreferences,
|
||||
type ContinuousViewport,
|
||||
} from "./calendarViewModel";
|
||||
|
||||
type EventDialogState = {kind: "create";} | {kind: "edit";event: CalendarEvent;};
|
||||
type EventEditScope = "occurrence" | "series";
|
||||
type EventDialogState =
|
||||
| { kind: "create" }
|
||||
| {
|
||||
kind: "edit";
|
||||
occurrence: CalendarEvent;
|
||||
seriesEvent: CalendarEvent | null;
|
||||
editScope: EventEditScope;
|
||||
};
|
||||
const INITIAL_CONTINUOUS_WEEKS = { before: 4, after: 6 };
|
||||
|
||||
const modeOptions: {id: CalendarMode;label: string;}[] = [
|
||||
@@ -109,8 +122,6 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
const [visibleCalendarIds, setVisibleCalendarIds] = useState<string[]>([]);
|
||||
const [eventCalendarId, setEventCalendarId] = useState("");
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const eventsRef = useRef<CalendarEvent[]>([]);
|
||||
const eventDeltaRef = useRef<{key: string;watermark: string | null;}>({ key: "", watermark: null });
|
||||
const eventRequestRef = useRef(0);
|
||||
const [mode, setMode] = useState<CalendarMode>(() => loadCalendarMode());
|
||||
const [focusDate, setFocusDate] = useState(() => startOfDay(new Date()));
|
||||
@@ -132,7 +143,9 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
const pendingContinuousScrollDayRef = useRef<Date | null>(null);
|
||||
const dragActionRef = useRef<CalendarDragAction | null>(null);
|
||||
|
||||
const viewPreferences = useMemo(() => loadCalendarViewPreferences(), []);
|
||||
const [viewPreferences, setViewPreferences] = useState<CalendarViewPreferences>(
|
||||
() => loadCalendarViewPreferences()
|
||||
);
|
||||
const visibleCalendarIdSet = useMemo(() => new Set(visibleCalendarIds), [visibleCalendarIds]);
|
||||
const visibleEvents = useMemo(() => events.filter((event) => visibleCalendarIdSet.has(event.calendar_id)), [events, visibleCalendarIdSet]);
|
||||
const syncSourceByCalendarId = useMemo(() => new Map(syncSources.map((source) => [source.calendar_id, source])), [syncSources]);
|
||||
@@ -152,6 +165,12 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
const canManageCalendars = hasScope(auth, "calendar:calendar:write");
|
||||
const canDeleteCalendars = hasScope(auth, "calendar:calendar:admin");
|
||||
const heading = headingForMode(mode, focusDate, visibleRange);
|
||||
const dialogEvent =
|
||||
eventDialog?.kind === "edit"
|
||||
? eventDialog.editScope === "series"
|
||||
? eventDialog.seriesEvent
|
||||
: eventDialog.occurrence
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
void loadCalendars();
|
||||
@@ -161,11 +180,35 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
saveCalendarMode(mode);
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const preferences = await getCalendarViewPreferences(settings);
|
||||
if (active) setViewPreferences(calendarViewPreferences(preferences));
|
||||
} catch (err) {
|
||||
if (active) setError(errorText(err));
|
||||
}
|
||||
};
|
||||
const handlePreferenceChange = () => void load();
|
||||
void load();
|
||||
window.addEventListener(
|
||||
"govoplan:calendar-preferences-changed",
|
||||
handlePreferenceChange
|
||||
);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener(
|
||||
"govoplan:calendar-preferences-changed",
|
||||
handlePreferenceChange
|
||||
);
|
||||
};
|
||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (calendars.length) {
|
||||
void loadEvents();
|
||||
} else {
|
||||
eventsRef.current = [];
|
||||
setEvents([]);
|
||||
}
|
||||
}, [calendars.length, eventWindow.start.getTime(), eventWindow.end.getTime()]);
|
||||
@@ -222,29 +265,17 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
setError("");
|
||||
const start = eventWindow.start.toISOString();
|
||||
const end = eventWindow.end.toISOString();
|
||||
const deltaKey = `${start}|${end}`;
|
||||
try {
|
||||
let since = eventDeltaRef.current.key === deltaKey ? eventDeltaRef.current.watermark : null;
|
||||
let nextEvents = eventDeltaRef.current.key === deltaKey ? eventsRef.current : [];
|
||||
let response = null;
|
||||
do {
|
||||
response = await listCalendarEventsDelta(settings, {
|
||||
start_at: start,
|
||||
end_at: end,
|
||||
since
|
||||
});
|
||||
nextEvents = mergeCalendarEventDelta(nextEvents, response);
|
||||
since = response.watermark || null;
|
||||
} while (response.has_more && response.watermark);
|
||||
const response = await listCalendarEvents(settings, {
|
||||
start_at: start,
|
||||
end_at: end,
|
||||
expand_recurring: true
|
||||
});
|
||||
if (requestId !== eventRequestRef.current) return;
|
||||
eventsRef.current = nextEvents;
|
||||
eventDeltaRef.current = { key: deltaKey, watermark: since };
|
||||
setEvents(nextEvents);
|
||||
setEvents(response.events);
|
||||
} catch (err) {
|
||||
if (requestId !== eventRequestRef.current) return;
|
||||
setError(errorText(err));
|
||||
eventsRef.current = [];
|
||||
eventDeltaRef.current = { key: "", watermark: null };
|
||||
setEvents([]);
|
||||
} finally {
|
||||
if (requestId === eventRequestRef.current) setLoading(false);
|
||||
@@ -352,7 +383,6 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
if (calendars.length > 1 || payload.event_action === "move") {
|
||||
await loadEvents();
|
||||
} else {
|
||||
eventsRef.current = [];
|
||||
setEvents([]);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -469,15 +499,50 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
window.requestAnimationFrame(() => updateContinuousViewport(node));
|
||||
}
|
||||
|
||||
function openEventEditor(event: CalendarEvent) {
|
||||
const recurringOccurrence = Boolean(
|
||||
event.is_occurrence &&
|
||||
event.series_event_id &&
|
||||
event.recurrence_id
|
||||
);
|
||||
setEventDialog({
|
||||
kind: "edit",
|
||||
occurrence: event,
|
||||
seriesEvent: recurringOccurrence ? null : event,
|
||||
editScope: recurringOccurrence ? "occurrence" : "series"
|
||||
});
|
||||
if (!recurringOccurrence || !event.series_event_id) return;
|
||||
const instanceId = calendarEventInstanceId(event);
|
||||
void getCalendarEvent(settings, event.series_event_id)
|
||||
.then((seriesEvent) => {
|
||||
setEventDialog((current) =>
|
||||
current?.kind === "edit" &&
|
||||
calendarEventInstanceId(current.occurrence) === instanceId
|
||||
? { ...current, seriesEvent }
|
||||
: current
|
||||
);
|
||||
})
|
||||
.catch((err) => setError(errorText(err)));
|
||||
}
|
||||
|
||||
function setEventEditScope(scope: EventEditScope) {
|
||||
setEventDialog((current) =>
|
||||
current?.kind === "edit" &&
|
||||
(scope === "occurrence" || current.seriesEvent)
|
||||
? { ...current, editScope: scope }
|
||||
: current
|
||||
);
|
||||
}
|
||||
|
||||
function beginEventDrag(dragEvent: ReactDragEvent<HTMLElement>, action: CalendarDragAction) {
|
||||
if (!canWrite) {
|
||||
dragEvent.preventDefault();
|
||||
return;
|
||||
}
|
||||
dragActionRef.current = action;
|
||||
setDraggingEventId(action.event.id);
|
||||
setDraggingEventId(calendarEventInstanceId(action.event));
|
||||
dragEvent.dataTransfer.effectAllowed = "move";
|
||||
dragEvent.dataTransfer.setData("text/plain", action.event.id);
|
||||
dragEvent.dataTransfer.setData("text/plain", calendarEventInstanceId(action.event));
|
||||
}
|
||||
|
||||
function handleEventDragStart(dragEvent: ReactDragEvent<HTMLElement>, calendarEvent: CalendarEvent) {
|
||||
@@ -553,11 +618,25 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
await updateCalendarEvent(settings, calendarEvent.id, {
|
||||
const payload = {
|
||||
start_at: next.startAt.toISOString(),
|
||||
end_at: next.endAt ? next.endAt.toISOString() : null,
|
||||
all_day: next.allDay
|
||||
});
|
||||
};
|
||||
if (
|
||||
calendarEvent.is_occurrence &&
|
||||
calendarEvent.series_event_id &&
|
||||
calendarEvent.recurrence_id
|
||||
) {
|
||||
await updateCalendarEventOccurrence(
|
||||
settings,
|
||||
calendarEvent.series_event_id,
|
||||
calendarEvent.recurrence_id,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
await updateCalendarEvent(settings, calendarEvent.id, payload);
|
||||
}
|
||||
await loadEvents();
|
||||
} catch (err) {
|
||||
setError(errorText(err));
|
||||
@@ -576,11 +655,29 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSave(payload: CalendarEventCreatePayload, event: CalendarEvent | null): Promise<boolean> {
|
||||
async function handleSave(
|
||||
payload: CalendarEventCreatePayload,
|
||||
event: CalendarEvent | null,
|
||||
editScope: EventEditScope
|
||||
): Promise<boolean> {
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
if (event) {
|
||||
const currentDialog = eventDialog;
|
||||
if (
|
||||
event &&
|
||||
currentDialog?.kind === "edit" &&
|
||||
editScope === "occurrence" &&
|
||||
currentDialog.occurrence.series_event_id &&
|
||||
currentDialog.occurrence.recurrence_id
|
||||
) {
|
||||
await updateCalendarEventOccurrence(
|
||||
settings,
|
||||
currentDialog.occurrence.series_event_id,
|
||||
currentDialog.occurrence.recurrence_id,
|
||||
payload
|
||||
);
|
||||
} else if (event) {
|
||||
await updateCalendarEvent(settings, event.id, payload);
|
||||
} else {
|
||||
await createCalendarEvent(settings, payload);
|
||||
@@ -600,11 +697,28 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(event: CalendarEvent) {
|
||||
async function handleDelete(
|
||||
event: CalendarEvent,
|
||||
editScope: EventEditScope
|
||||
) {
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
await deleteCalendarEvent(settings, event.id);
|
||||
const currentDialog = eventDialog;
|
||||
if (
|
||||
currentDialog?.kind === "edit" &&
|
||||
editScope === "occurrence" &&
|
||||
currentDialog.occurrence.series_event_id &&
|
||||
currentDialog.occurrence.recurrence_id
|
||||
) {
|
||||
await deleteCalendarEventOccurrence(
|
||||
settings,
|
||||
currentDialog.occurrence.series_event_id,
|
||||
currentDialog.occurrence.recurrence_id
|
||||
);
|
||||
} else {
|
||||
await deleteCalendarEvent(settings, event.id);
|
||||
}
|
||||
setEventDialog(null);
|
||||
await loadEvents();
|
||||
} catch (err) {
|
||||
@@ -690,15 +804,15 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
<h3>{agendaDateLabel(group.date)}</h3>
|
||||
{group.events.map((event) =>
|
||||
<button
|
||||
key={`${group.key}-${event.id}`}
|
||||
key={`${group.key}-${calendarEventInstanceId(event)}`}
|
||||
type="button"
|
||||
className={["calendar-agenda-item", hoveredEventId === event.id ? "is-linked-hover" : ""].filter(Boolean).join(" ")}
|
||||
className={["calendar-agenda-item", hoveredEventId === calendarEventInstanceId(event) ? "is-linked-hover" : ""].filter(Boolean).join(" ")}
|
||||
style={calendarEventColorStyle(calendarColorById.get(event.calendar_id))}
|
||||
onMouseEnter={() => setHoveredEventId(event.id)}
|
||||
onMouseLeave={() => setHoveredEventId((current) => current === event.id ? "" : current)}
|
||||
onFocus={() => setHoveredEventId(event.id)}
|
||||
onBlur={() => setHoveredEventId((current) => current === event.id ? "" : current)}
|
||||
onClick={() => setEventDialog({ kind: "edit", event })}>
|
||||
onMouseEnter={() => setHoveredEventId(calendarEventInstanceId(event))}
|
||||
onMouseLeave={() => setHoveredEventId((current) => current === calendarEventInstanceId(event) ? "" : current)}
|
||||
onFocus={() => setHoveredEventId(calendarEventInstanceId(event))}
|
||||
onBlur={() => setHoveredEventId((current) => current === calendarEventInstanceId(event) ? "" : current)}
|
||||
onClick={() => openEventEditor(event)}>
|
||||
|
||||
<EventInlineLabel event={event} />
|
||||
</button>
|
||||
@@ -779,7 +893,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
hoveredEventId={hoveredEventId}
|
||||
dropTarget={dropTarget}
|
||||
viewport={continuousViewport}
|
||||
onEventSelect={(event) => setEventDialog({ kind: "edit", event })}
|
||||
onEventSelect={openEventEditor}
|
||||
onEventHover={setHoveredEventId}
|
||||
onEventDragStart={handleEventDragStart}
|
||||
onEventDragEnd={handleEventDragEnd}
|
||||
@@ -800,7 +914,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
draggingEventId={draggingEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
dropTarget={dropTarget}
|
||||
onEventSelect={(event) => setEventDialog({ kind: "edit", event })}
|
||||
onEventSelect={openEventEditor}
|
||||
onEventHover={setHoveredEventId}
|
||||
onEventDragStart={handleEventDragStart}
|
||||
onEventDragEnd={handleEventDragEnd}
|
||||
@@ -819,7 +933,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
draggingEventId={draggingEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
dropTarget={dropTarget}
|
||||
onEventSelect={(event) => setEventDialog({ kind: "edit", event })}
|
||||
onEventSelect={openEventEditor}
|
||||
onEventHover={setHoveredEventId}
|
||||
onEventDragStart={handleEventDragStart}
|
||||
onEventResizeDragStart={handleEventResizeDragStart}
|
||||
@@ -838,13 +952,28 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
|
||||
{eventDialog && calendars.length > 0 &&
|
||||
<CalendarEventDialog
|
||||
key={
|
||||
eventDialog.kind === "edit"
|
||||
? `${calendarEventInstanceId(eventDialog.occurrence)}:${eventDialog.editScope}`
|
||||
: "new-event"
|
||||
}
|
||||
calendars={calendars}
|
||||
defaultCalendarId={targetCalendarId}
|
||||
event={eventDialog.kind === "edit" ? eventDialog.event : null}
|
||||
event={dialogEvent}
|
||||
editScope={eventDialog.kind === "edit" ? eventDialog.editScope : "series"}
|
||||
canChooseSeries={
|
||||
eventDialog.kind === "edit" &&
|
||||
Boolean(eventDialog.occurrence.is_occurrence)
|
||||
}
|
||||
seriesLoaded={
|
||||
eventDialog.kind !== "edit" ||
|
||||
eventDialog.seriesEvent !== null
|
||||
}
|
||||
focusDate={focusDate}
|
||||
saving={saving}
|
||||
canWrite={canWrite}
|
||||
canDelete={canDelete}
|
||||
onEditScopeChange={setEventEditScope}
|
||||
onCancel={() => setEventDialog(null)}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete} />
|
||||
@@ -887,3 +1016,17 @@ function compareCalendars(left: CalendarCollection, right: CalendarCollection):
|
||||
if (left.is_default !== right.is_default) return left.is_default ? -1 : 1;
|
||||
return left.name.localeCompare(right.name);
|
||||
}
|
||||
|
||||
function calendarViewPreferences(
|
||||
response: CalendarViewPreferencesResponse
|
||||
): CalendarViewPreferences {
|
||||
return {
|
||||
dimWeekends: response.dim_weekends,
|
||||
dimOffHours: response.dim_off_hours,
|
||||
workdayStartHour: response.workday_start_hour,
|
||||
workdayEndHour: response.workday_end_hour,
|
||||
continuousVirtualization: response.continuous_virtualization,
|
||||
continuousOverscanWeeks: response.continuous_overscan_weeks,
|
||||
alternateContinuousMonths: response.alternate_continuous_months
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Save } from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
ToggleSwitch,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
getCalendarViewPreferences,
|
||||
updateCalendarViewPreferences,
|
||||
type CalendarViewPreferences
|
||||
} from "../../api/calendar";
|
||||
|
||||
type CalendarPreferenceDraft = Pick<
|
||||
CalendarViewPreferences,
|
||||
| "dim_weekends"
|
||||
| "dim_off_hours"
|
||||
| "workday_start_hour"
|
||||
| "workday_end_hour"
|
||||
| "continuous_virtualization"
|
||||
| "continuous_overscan_weeks"
|
||||
| "alternate_continuous_months"
|
||||
>;
|
||||
|
||||
const FALLBACK_DRAFT: CalendarPreferenceDraft = {
|
||||
dim_weekends: true,
|
||||
dim_off_hours: true,
|
||||
workday_start_hour: 6,
|
||||
workday_end_hour: 20,
|
||||
continuous_virtualization: true,
|
||||
continuous_overscan_weeks: 6,
|
||||
alternate_continuous_months: true
|
||||
};
|
||||
|
||||
export default function CalendarSettingsPanel({
|
||||
settings,
|
||||
auth
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
}) {
|
||||
const [loaded, setLoaded] = useState<CalendarPreferenceDraft | null>(null);
|
||||
const [draft, setDraft] = useState<CalendarPreferenceDraft>(FALLBACK_DRAFT);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [tone, setTone] = useState<"success" | "warning">("success");
|
||||
const dirty = useMemo(
|
||||
() => loaded !== null && JSON.stringify(loaded) !== JSON.stringify(draft),
|
||||
[draft, loaded]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPreferences();
|
||||
}, [auth.user.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
async function loadPreferences() {
|
||||
setLoading(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const response = await getCalendarViewPreferences(settings);
|
||||
const next = preferenceDraft(response);
|
||||
setLoaded(next);
|
||||
setDraft(next);
|
||||
} catch (error) {
|
||||
setTone("warning");
|
||||
setMessage(errorText(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences() {
|
||||
if (draft.workday_end_hour <= draft.workday_start_hour) {
|
||||
setTone("warning");
|
||||
setMessage("Workday end must be after workday start.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const response = await updateCalendarViewPreferences(settings, draft);
|
||||
const next = preferenceDraft(response);
|
||||
setLoaded(next);
|
||||
setDraft(next);
|
||||
setTone("success");
|
||||
setMessage("Calendar preferences saved.");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("govoplan:calendar-preferences-changed")
|
||||
);
|
||||
} catch (error) {
|
||||
setTone("warning");
|
||||
setMessage(errorText(error));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const disabled = loading || saving;
|
||||
return (
|
||||
<div className="dashboard-grid settings-dashboard-grid calendar-settings-panel">
|
||||
<Card title="Calendar display">
|
||||
<div className="form-grid">
|
||||
<ToggleSwitch
|
||||
label="Dim weekends"
|
||||
help="Use a quieter background for Saturday and Sunday in week views."
|
||||
checked={draft.dim_weekends}
|
||||
disabled={disabled}
|
||||
onChange={(dim_weekends) =>
|
||||
setDraft((current) => ({ ...current, dim_weekends }))
|
||||
}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
label="Dim hours outside the workday"
|
||||
checked={draft.dim_off_hours}
|
||||
disabled={disabled}
|
||||
onChange={(dim_off_hours) =>
|
||||
setDraft((current) => ({ ...current, dim_off_hours }))
|
||||
}
|
||||
/>
|
||||
<div className="calendar-dialog-grid-two">
|
||||
<FormField label="Workday starts">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={23}
|
||||
value={draft.workday_start_hour}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
workday_start_hour: Number(event.target.value)
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Workday ends">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={24}
|
||||
value={draft.workday_end_hour}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
workday_end_hour: Number(event.target.value)
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card title="Continuous view">
|
||||
<div className="form-grid">
|
||||
<ToggleSwitch
|
||||
label="Virtualize distant weeks"
|
||||
help="Keep the continuous calendar responsive by rendering only nearby weeks."
|
||||
checked={draft.continuous_virtualization}
|
||||
disabled={disabled}
|
||||
onChange={(continuous_virtualization) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
continuous_virtualization
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
label="Overscan weeks"
|
||||
help="Additional weeks rendered above and below the viewport."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min={2}
|
||||
max={20}
|
||||
value={draft.continuous_overscan_weeks}
|
||||
disabled={disabled || !draft.continuous_virtualization}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
continuous_overscan_weeks: Number(event.target.value)
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<ToggleSwitch
|
||||
label="Alternate month backgrounds"
|
||||
checked={draft.alternate_continuous_months}
|
||||
disabled={disabled}
|
||||
onChange={(alternate_continuous_months) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
alternate_continuous_months
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div className="button-row compact-actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={disabled || !dirty}
|
||||
onClick={() => void savePreferences()}
|
||||
>
|
||||
<Save size={16} /> {saving ? "Saving" : "Save preferences"}
|
||||
</Button>
|
||||
</div>
|
||||
{message && (
|
||||
<DismissibleAlert tone={tone} resetKey={message} floating>
|
||||
{message}
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function preferenceDraft(
|
||||
preferences: CalendarViewPreferences
|
||||
): CalendarPreferenceDraft {
|
||||
return {
|
||||
dim_weekends: preferences.dim_weekends,
|
||||
dim_off_hours: preferences.dim_off_hours,
|
||||
workday_start_hour: preferences.workday_start_hour,
|
||||
workday_end_hour: preferences.workday_end_hour,
|
||||
continuous_virtualization: preferences.continuous_virtualization,
|
||||
continuous_overscan_weeks: preferences.continuous_overscan_weeks,
|
||||
alternate_continuous_months: preferences.alternate_continuous_months
|
||||
};
|
||||
}
|
||||
|
||||
function errorText(error: unknown): string {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: "Calendar preferences could not be loaded.";
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
HOUR_ROW_HEIGHT,
|
||||
INITIAL_SCROLL_HOUR,
|
||||
calendarEventColorStyle,
|
||||
calendarEventInstanceId,
|
||||
chunk,
|
||||
continuousVirtualWindow,
|
||||
dayKey,
|
||||
@@ -185,12 +186,12 @@ export function CalendarWeekRows({
|
||||
<div className="calendar-event-stack">
|
||||
{dayEvents.slice(0, eventLimit).map((event) => (
|
||||
<CalendarEventChip
|
||||
key={event.id}
|
||||
key={calendarEventInstanceId(event)}
|
||||
event={event}
|
||||
color={calendarColorById.get(event.calendar_id)}
|
||||
canDrag={canWrite}
|
||||
dragging={draggingEventId === event.id}
|
||||
linkedHover={hoveredEventId === event.id}
|
||||
dragging={draggingEventId === calendarEventInstanceId(event)}
|
||||
linkedHover={hoveredEventId === calendarEventInstanceId(event)}
|
||||
onSelect={onEventSelect}
|
||||
onHover={onEventHover}
|
||||
onDragStart={onEventDragStart}
|
||||
@@ -350,13 +351,13 @@ export function CalendarTimeGrid({
|
||||
>
|
||||
{day.events.map((event) => (
|
||||
<CalendarEventChip
|
||||
key={event.id}
|
||||
key={calendarEventInstanceId(event)}
|
||||
event={event}
|
||||
color={calendarColorById.get(event.calendar_id)}
|
||||
compact
|
||||
canDrag={canWrite}
|
||||
dragging={draggingEventId === event.id}
|
||||
linkedHover={hoveredEventId === event.id}
|
||||
dragging={draggingEventId === calendarEventInstanceId(event)}
|
||||
linkedHover={hoveredEventId === calendarEventInstanceId(event)}
|
||||
onSelect={onEventSelect}
|
||||
onHover={onEventHover}
|
||||
onDragStart={onEventDragStart}
|
||||
@@ -503,11 +504,11 @@ function TimedDayColumn({
|
||||
)}
|
||||
{layout.items.map((item) => (
|
||||
<div
|
||||
key={item.event.id}
|
||||
key={calendarEventInstanceId(item.event)}
|
||||
className={[
|
||||
"calendar-timed-event",
|
||||
draggingEventId === item.event.id ? "is-dragging" : "",
|
||||
hoveredEventId === item.event.id
|
||||
draggingEventId === calendarEventInstanceId(item.event) ? "is-dragging" : "",
|
||||
hoveredEventId === calendarEventInstanceId(item.event)
|
||||
? "is-linked-hover"
|
||||
: "",
|
||||
]
|
||||
@@ -518,7 +519,7 @@ function TimedDayColumn({
|
||||
calendarColorById.get(item.event.calendar_id),
|
||||
)}
|
||||
draggable={canWrite}
|
||||
onMouseEnter={() => onEventHover(item.event.id)}
|
||||
onMouseEnter={() => onEventHover(calendarEventInstanceId(item.event))}
|
||||
onMouseLeave={() => onEventHover("")}
|
||||
onDragStart={
|
||||
canWrite
|
||||
@@ -546,7 +547,7 @@ function TimedDayColumn({
|
||||
type="button"
|
||||
className="calendar-timed-event-content"
|
||||
onClick={() => onEventSelect(item.event)}
|
||||
onFocus={() => onEventHover(item.event.id)}
|
||||
onFocus={() => onEventHover(calendarEventInstanceId(item.event))}
|
||||
onBlur={() => onEventHover("")}
|
||||
title={i18nMessage(
|
||||
"i18n:govoplan-calendar.edit_value.fad75899",
|
||||
@@ -629,9 +630,9 @@ function CalendarEventChip({
|
||||
style={calendarEventColorStyle(color)}
|
||||
draggable={canDrag}
|
||||
onClick={() => onSelect(event)}
|
||||
onMouseEnter={onHover ? () => onHover(event.id) : undefined}
|
||||
onMouseEnter={onHover ? () => onHover(calendarEventInstanceId(event)) : undefined}
|
||||
onMouseLeave={onHover ? () => onHover("") : undefined}
|
||||
onFocus={onHover ? () => onHover(event.id) : undefined}
|
||||
onFocus={onHover ? () => onHover(calendarEventInstanceId(event)) : undefined}
|
||||
onBlur={onHover ? () => onHover("") : undefined}
|
||||
onDragStart={
|
||||
canDrag && onDragStart
|
||||
|
||||
@@ -32,10 +32,11 @@ export default function UpcomingEventsWidget({
|
||||
end.setDate(end.getDate() + daysAhead);
|
||||
const response = await listCalendarEvents(settings, {
|
||||
start_at: start.toISOString(),
|
||||
end_at: end.toISOString()
|
||||
end_at: end.toISOString(),
|
||||
expand_recurring: true
|
||||
});
|
||||
return [...response.events]
|
||||
.filter((event) => event.status !== "cancelled")
|
||||
.filter((event) => event.status.toUpperCase() !== "CANCELLED")
|
||||
.sort(
|
||||
(left, right) =>
|
||||
new Date(left.start_at).getTime() - new Date(right.start_at).getTime()
|
||||
@@ -57,7 +58,7 @@ export default function UpcomingEventsWidget({
|
||||
<DashboardWidgetList
|
||||
emptyText={`No events in the next ${daysAhead} days.`}
|
||||
items={(events ?? []).map((event) => ({
|
||||
id: event.id,
|
||||
id: event.instance_id || event.id,
|
||||
title: event.summary,
|
||||
detail:
|
||||
showLocation && event.location ? (
|
||||
|
||||
@@ -74,7 +74,7 @@ const MAX_TIMED_EVENT_COLUMNS = 3;
|
||||
const CALENDAR_MODE_STORAGE_KEY = "govoplan.calendar.mode";
|
||||
const CALENDAR_VIEW_PREFERENCES_STORAGE_KEY =
|
||||
"govoplan.calendar.viewPreferences";
|
||||
const DEFAULT_CALENDAR_VIEW_PREFERENCES: CalendarViewPreferences = {
|
||||
export const DEFAULT_CALENDAR_VIEW_PREFERENCES: CalendarViewPreferences = {
|
||||
dimWeekends: true,
|
||||
dimOffHours: true,
|
||||
workdayStartHour: 6,
|
||||
@@ -84,6 +84,10 @@ const DEFAULT_CALENDAR_VIEW_PREFERENCES: CalendarViewPreferences = {
|
||||
alternateContinuousMonths: true,
|
||||
};
|
||||
|
||||
export function calendarEventInstanceId(event: CalendarEvent): string {
|
||||
return event.instance_id || event.id;
|
||||
}
|
||||
|
||||
export function rangeForMode(
|
||||
mode: CalendarMode,
|
||||
focusDate: Date,
|
||||
@@ -677,7 +681,7 @@ export function layoutTimedEventsForDay(
|
||||
}
|
||||
if (hidden.length > 0) {
|
||||
overflows.push({
|
||||
key: `${dayKey(day)}-${hidden[0].event.id}-overflow`,
|
||||
key: `${dayKey(day)}-${calendarEventInstanceId(hidden[0].event)}-overflow`,
|
||||
top: Math.min(...hidden.map((item) => item.top)),
|
||||
column: overflowColumn,
|
||||
columns: MAX_TIMED_EVENT_COLUMNS,
|
||||
|
||||
+28
-2
@@ -2,7 +2,8 @@ import { createElement, lazy } from "react";
|
||||
import type {
|
||||
CalendarPickerUiCapability,
|
||||
DashboardWidgetsUiCapability,
|
||||
PlatformWebModule
|
||||
PlatformWebModule,
|
||||
SettingsSectionsUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import "./styles/calendar.css";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
@@ -10,6 +11,9 @@ import CalendarPicker from "./features/calendar/CalendarPicker";
|
||||
import UpcomingEventsWidget from "./features/calendar/UpcomingEventsWidget";
|
||||
|
||||
const CalendarPage = lazy(() => import("./features/calendar/CalendarPage"));
|
||||
const CalendarSettingsPanel = lazy(
|
||||
() => import("./features/calendar/CalendarSettingsPanel")
|
||||
);
|
||||
|
||||
const eventRead = ["calendar:event:read"];
|
||||
const translations = {
|
||||
@@ -18,6 +22,20 @@ const translations = {
|
||||
};
|
||||
|
||||
const calendarPicker: CalendarPickerUiCapability = { CalendarPicker };
|
||||
const calendarSettingsSections: SettingsSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "calendar",
|
||||
surfaceId: "calendar.settings.preferences",
|
||||
label: "Calendar",
|
||||
group: "ui",
|
||||
order: 45,
|
||||
anyOf: eventRead,
|
||||
render: ({ settings, auth }) =>
|
||||
createElement(CalendarSettingsPanel, { settings, auth })
|
||||
}
|
||||
]
|
||||
};
|
||||
const calendarDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||
widgets: [
|
||||
{
|
||||
@@ -87,6 +105,13 @@ export const calendarModule: PlatformWebModule = {
|
||||
kind: "section",
|
||||
label: "Upcoming events widget",
|
||||
order: 40
|
||||
},
|
||||
{
|
||||
id: "calendar.settings.preferences",
|
||||
moduleId: "calendar",
|
||||
kind: "section",
|
||||
label: "Calendar preferences",
|
||||
order: 45
|
||||
}
|
||||
],
|
||||
navItems: [{ to: "/calendar", label: "i18n:govoplan-calendar.calendar.adab5090", iconName: "calendar", anyOf: eventRead, order: 55 }],
|
||||
@@ -94,7 +119,8 @@ export const calendarModule: PlatformWebModule = {
|
||||
{ path: "/calendar", anyOf: eventRead, order: 55, render: ({ settings, auth }) => createElement(CalendarPage, { settings, auth }) }],
|
||||
uiCapabilities: {
|
||||
"calendar.picker": calendarPicker,
|
||||
"dashboard.widgets": calendarDashboardWidgets
|
||||
"dashboard.widgets": calendarDashboardWidgets,
|
||||
"settings.sections": calendarSettingsSections
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -23,13 +23,19 @@
|
||||
],
|
||||
"react/jsx-runtime": [
|
||||
"../../govoplan-core/webui/node_modules/@types/react/jsx-runtime.d.ts"
|
||||
],
|
||||
"react-router": [
|
||||
"../../govoplan-core/webui/node_modules/react-router/dist/development/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../../govoplan-core/webui/src/vite-env.d.ts",
|
||||
"src/module.ts",
|
||||
"src/api/calendar.ts",
|
||||
"src/features/calendar/CalendarPage.tsx",
|
||||
"src/features/calendar/CalendarSettingsPanel.tsx",
|
||||
"src/features/calendar/UpcomingEventsWidget.tsx",
|
||||
"src/features/calendar/CalendarViews.tsx",
|
||||
"src/features/calendar/CalendarCollectionDialogs.tsx",
|
||||
"src/features/calendar/CalendarEventDialog.tsx",
|
||||
|
||||
Reference in New Issue
Block a user