Migrate Calendar interface patterns

This commit is contained in:
2026-08-03 15:08:18 +02:00
parent a125b666da
commit d7fd9447a2
13 changed files with 486 additions and 42 deletions
@@ -7,9 +7,11 @@ import {
} from "react";
import { ArrowRightLeft, ListChecks, RefreshCw, Trash2 } from "lucide-react";
import {
ActionBlockerHint,
Button,
ColorPickerField,
Dialog,
DocumentationHelpLink,
PasswordField,
SegmentedControl,
ToggleSwitch,
@@ -41,6 +43,10 @@ import {
errorText,
normalizeHexColor,
} from "./calendarViewModel";
import {
CALENDAR_I18N,
CALENDAR_SOURCE_DOCUMENTATION,
} from "./interfacePatterns";
export type CalendarSourceMode = "local" | CalendarSyncSourceKind | "open_xchange";
type CalendarSourceSwitchMode =
@@ -161,12 +167,18 @@ export function CalendarCollectionDialog({
!isExistingSyncSource && !canEditSource ||
canEditSource && (!effectiveCollectionUrl || effectiveAuthType === "basic" && !username.trim() || needsSourceSecret));
const saveDisabled =
saving ||
migrationLocked ||
!canWrite ||
!name.trim() ||
sourceDetailsInvalid;
const saveDisabledReason = saving
? CALENDAR_I18N.saving
: migrationLocked
? CALENDAR_I18N.migrationLocked
: !canWrite
? CALENDAR_I18N.calendarWriteRequired
: !name.trim()
? CALENDAR_I18N.calendarNameRequired
: sourceDetailsInvalid
? CALENDAR_I18N.sourceDetailsRequired
: undefined;
const saveDisabled = Boolean(saveDisabledReason);
const syncing = source ? syncingSourceId === source.id : false;
@@ -348,25 +360,41 @@ export function CalendarCollectionDialog({
<>
<div>
{calendar && canDelete &&
<Button type="button" variant="danger" onClick={() => onRequestDelete(calendar, state.kind === "edit" ? state.eventCount : null, state.kind === "edit" ? state.loadingEventCount : true)} disabled={saving || migrationLocked}>
<Button type="button" variant="danger" onClick={() => onRequestDelete(calendar, state.kind === "edit" ? state.eventCount : null, state.kind === "edit" ? state.loadingEventCount : true)} disabled={saving || migrationLocked} disabledReason={saving ? CALENDAR_I18N.saving : migrationLocked ? CALENDAR_I18N.migrationLocked : undefined}>
<Trash2 size={16} /> {calendarDeleteActionLabel(calendar)}
</Button>
}
</div>
<div className="calendar-dialog-actions">
<Button type="button" onClick={onCancel} disabled={saving}>i18n:govoplan-calendar.cancel.77dfd213</Button>
<Button type="submit" form={formId} variant="primary" disabled={saveDisabled}>{saving ? "i18n:govoplan-calendar.saving.ae7e8875" : isEdit ? "i18n:govoplan-calendar.save.efc007a3" : "i18n:govoplan-calendar.add.61cc55aa"}</Button>
<Button type="submit" form={formId} variant="primary" disabled={saveDisabled} disabledReason={saveDisabledReason}>{saving ? "i18n:govoplan-calendar.saving.ae7e8875" : isEdit ? "i18n:govoplan-calendar.save.efc007a3" : "i18n:govoplan-calendar.add.61cc55aa"}</Button>
</div>
</>
}>
<form id={formId} className="calendar-dialog-form" onSubmit={submit}>
{migrationLocked &&
<p className="calendar-form-note">
Calendar and event changes are locked while the destructive remote move is being reconciled.
</p>
}
{sourceMode !== "local" && !canManageSources && <p className="calendar-form-note">i18n:govoplan-calendar.managing_sync_sources_requires_calendar_administ.835e29fa</p>}
<div className="calendar-dialog-documentation">
<DocumentationHelpLink reference={CALENDAR_SOURCE_DOCUMENTATION} />
</div>
{migrationLocked && (
<ActionBlockerHint
reason={{
summary: CALENDAR_I18N.migrationLocked,
requiredAction: "Open Remote move and reconcile or finish the batch before editing this calendar."
}}
documentation={CALENDAR_SOURCE_DOCUMENTATION}
/>
)}
{sourceMode !== "local" && !canManageSources && (
<ActionBlockerHint
tone="info"
reason={{
summary: CALENDAR_I18N.calendarAdminRequired,
details: "Source URLs, credentials, and synchronization policy are administrator-owned settings."
}}
documentation={CALENDAR_SOURCE_DOCUMENTATION}
/>
)}
{!isEdit &&
<SegmentedControl
className="calendar-source-switch"
@@ -498,7 +526,7 @@ export function CalendarCollectionDialog({
}
<div className={calendarSourceUsesCalDav(sourceMode) ? "calendar-discovery-actions" : "calendar-discovery-actions is-readonly"}>
{calendarSourceUsesCalDav(sourceMode) &&
<Button type="button" onClick={() => void handleDiscover()} disabled={saving || discovering || !canEditSource || !davUrl.trim() || effectiveAuthType === "basic" && !username.trim()}>
<Button type="button" onClick={() => void handleDiscover()} disabled={saving || discovering || !canEditSource || !davUrl.trim() || effectiveAuthType === "basic" && !username.trim()} disabledReason={saving ? CALENDAR_I18N.saving : !canEditSource ? CALENDAR_I18N.calendarAdminRequired : !davUrl.trim() || effectiveAuthType === "basic" && !username.trim() ? CALENDAR_I18N.sourceDetailsRequired : undefined}>
<RefreshCw size={16} /> {discovering ? "i18n:govoplan-calendar.discovering.1884f689" : "i18n:govoplan-calendar.discover.4827ea22"}
</Button>
}
@@ -562,11 +590,12 @@ export function CalendarCollectionDialog({
type="button"
className={syncing ? "calendar-sync-button is-syncing" : "calendar-sync-button"}
onClick={() => void onSync(source, syncTransientPayload(effectiveAuthType, password, bearerToken))}
disabled={saving || syncing || migrationLocked || !canSyncSources}>
disabled={saving || syncing || migrationLocked || !canSyncSources}
disabledReason={saving ? CALENDAR_I18N.saving : syncing ? CALENDAR_I18N.syncing : migrationLocked ? CALENDAR_I18N.migrationLocked : !canSyncSources ? CALENDAR_I18N.syncPermissionRequired : undefined}>
<RefreshCw size={16} className={syncing ? "calendar-sync-spin" : undefined} /> {syncing ? "i18n:govoplan-calendar.syncing.e5c7727a" : "i18n:govoplan-calendar.sync_now.2b7d938e"}
</Button>
<Button type="button" onClick={() => void onSync(source, { ...syncTransientPayload(effectiveAuthType, password, bearerToken), force_full: true })} disabled={saving || syncing || migrationLocked || !canSyncSources}>
<Button type="button" onClick={() => void onSync(source, { ...syncTransientPayload(effectiveAuthType, password, bearerToken), force_full: true })} disabled={saving || syncing || migrationLocked || !canSyncSources} disabledReason={saving ? CALENDAR_I18N.saving : syncing ? CALENDAR_I18N.syncing : migrationLocked ? CALENDAR_I18N.migrationLocked : !canSyncSources ? CALENDAR_I18N.syncPermissionRequired : undefined}>
i18n:govoplan-calendar.full_sync.21b89c76
</Button>
{source.source_kind === "caldav" && canManageSources && (
@@ -660,6 +689,9 @@ export function CalendarCollectionDeleteDialog({
}>
<div className="calendar-delete-dialog-body">
<div className="calendar-dialog-documentation">
<DocumentationHelpLink reference={CALENDAR_SOURCE_DOCUMENTATION} />
</div>
<p className="calendar-delete-warning">
{loadingEventCount ?
"i18n:govoplan-calendar.loading_event_count.716ad3c2" :