Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
265 lines
8.0 KiB
TypeScript
265 lines
8.0 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Save } from "lucide-react";
|
|
import { FormGrid, ContentGrid,
|
|
Button,
|
|
Card,
|
|
DismissibleAlert,
|
|
DocumentationHelpLink,
|
|
FormField,
|
|
ToggleSwitch,
|
|
useUnsavedDraftGuard,
|
|
type ApiSettings,
|
|
type AuthInfo
|
|
} from "@govoplan/core-webui";
|
|
import {
|
|
getCalendarViewPreferences,
|
|
updateCalendarViewPreferences,
|
|
type CalendarViewPreferences
|
|
} from "../../api/calendar";
|
|
import {
|
|
CALENDAR_DOCUMENTATION,
|
|
CALENDAR_I18N,
|
|
} from "./interfacePatterns";
|
|
|
|
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(): Promise<boolean> {
|
|
if (draft.workday_end_hour <= draft.workday_start_hour) {
|
|
setTone("warning");
|
|
setMessage("Workday end must be after workday start.");
|
|
return false;
|
|
}
|
|
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")
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
setTone("warning");
|
|
setMessage(errorText(error));
|
|
return false;
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
const disabled = loading || saving;
|
|
const saveDisabledReason = loading
|
|
? CALENDAR_I18N.loading
|
|
: saving
|
|
? CALENDAR_I18N.saving
|
|
: !dirty
|
|
? CALENDAR_I18N.noChanges
|
|
: undefined;
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty,
|
|
onSave: savePreferences,
|
|
onDiscard: () => setDraft(loaded ?? FALLBACK_DRAFT)
|
|
});
|
|
|
|
return (
|
|
<ContentGrid columns={2} collapseAt="workspace" className="calendar-settings-panel">
|
|
<Card title="Calendar display" titleHelp={<DocumentationHelpLink reference={CALENDAR_DOCUMENTATION} />}>
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<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 }))
|
|
}
|
|
/>
|
|
<FormGrid columns={2} gap="small" collapseAt="narrow">
|
|
<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>
|
|
</FormGrid>
|
|
</FormGrid>
|
|
</Card>
|
|
<Card title="Continuous view">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<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={Boolean(saveDisabledReason)}
|
|
disabledReason={saveDisabledReason}
|
|
onClick={() => void savePreferences()}
|
|
>
|
|
<Save size={16} /> {saving ? "Saving" : "Save preferences"}
|
|
</Button>
|
|
</div>
|
|
{message && (
|
|
<DismissibleAlert tone={tone} resetKey={message} floating>
|
|
{message}
|
|
</DismissibleAlert>
|
|
)}
|
|
</FormGrid>
|
|
</Card>
|
|
</ContentGrid>
|
|
);
|
|
}
|
|
|
|
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.";
|
|
}
|