Migrate Calendar interface patterns
This commit is contained in:
@@ -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" :
|
||||
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
} from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
DateField,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
SegmentedControl,
|
||||
TimeField,
|
||||
ToggleSwitch,
|
||||
i18nMessage,
|
||||
useUnsavedDraftGuard,
|
||||
} from "@govoplan/core-webui";
|
||||
import type {
|
||||
@@ -29,6 +33,10 @@ import {
|
||||
toInputDate,
|
||||
toInputTime,
|
||||
} from "./calendarViewModel";
|
||||
import {
|
||||
CALENDAR_DOCUMENTATION,
|
||||
CALENDAR_I18N,
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type CalendarEventEndMode = "end" | "duration";
|
||||
type CalendarEventEditScope = "occurrence" | "series";
|
||||
@@ -160,6 +168,13 @@ export function CalendarEventDialog({
|
||||
};
|
||||
const initialEventDraftKey = useMemo(() => calendarDraftKey(eventDraft), []);
|
||||
const eventDirty = calendarDraftKey(eventDraft) !== initialEventDraftKey;
|
||||
const saveDisabledReason = saving
|
||||
? CALENDAR_I18N.saving
|
||||
: !canWrite
|
||||
? CALENDAR_I18N.readOnly
|
||||
: !summary.trim()
|
||||
? CALENDAR_I18N.eventTitleRequired
|
||||
: undefined;
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: eventDirty,
|
||||
@@ -258,14 +273,11 @@ export function CalendarEventDialog({
|
||||
|
||||
function requestDelete() {
|
||||
if (!event) return;
|
||||
if (!confirmingDelete) {
|
||||
setConfirmingDelete(true);
|
||||
return;
|
||||
}
|
||||
void onDelete(event, editScope);
|
||||
setConfirmingDelete(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open
|
||||
title={event ? "i18n:govoplan-calendar.edit_event.a7028454" : "i18n:govoplan-calendar.new_event.2ef3795c"}
|
||||
@@ -277,19 +289,32 @@ export function CalendarEventDialog({
|
||||
<>
|
||||
<div>
|
||||
{event && canDelete &&
|
||||
<Button type="button" variant="danger" onClick={requestDelete} disabled={saving}>
|
||||
<Trash2 size={16} /> {confirmingDelete ? "i18n:govoplan-calendar.confirm_delete.c9f2829e" : "i18n:govoplan-calendar.delete.f6fdbe48"}
|
||||
<Button type="button" variant="danger" onClick={requestDelete} disabled={saving} disabledReason={saving ? CALENDAR_I18N.saving : undefined}>
|
||||
<Trash2 size={16} /> i18n:govoplan-calendar.delete.f6fdbe48
|
||||
</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={saving || !canWrite}>{saving ? "i18n:govoplan-calendar.saving.ae7e8875" : "i18n:govoplan-calendar.save.efc007a3"}</Button>
|
||||
<Button type="submit" form={formId} variant="primary" disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{saving ? "i18n:govoplan-calendar.saving.ae7e8875" : "i18n:govoplan-calendar.save.efc007a3"}</Button>
|
||||
</div>
|
||||
</>
|
||||
}>
|
||||
|
||||
<form id={formId} className="calendar-dialog-form" onSubmit={submit}>
|
||||
<div className="calendar-dialog-documentation">
|
||||
<DocumentationHelpLink reference={CALENDAR_DOCUMENTATION} />
|
||||
</div>
|
||||
{!canWrite && (
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
reason={{
|
||||
summary: CALENDAR_I18N.readOnly,
|
||||
requiredAction: "Ask a calendar manager for event write permission."
|
||||
}}
|
||||
documentation={CALENDAR_DOCUMENTATION}
|
||||
/>
|
||||
)}
|
||||
{canChooseSeries && (
|
||||
<SegmentedControl
|
||||
ariaLabel="Recurring event edit scope"
|
||||
@@ -503,7 +528,22 @@ export function CalendarEventDialog({
|
||||
</section>
|
||||
</details>
|
||||
</form>
|
||||
</Dialog>);
|
||||
</Dialog>
|
||||
<ConfirmDialog
|
||||
open={Boolean(event && confirmingDelete)}
|
||||
title="i18n:govoplan-calendar.delete_event_title"
|
||||
message={i18nMessage(editScope === "occurrence"
|
||||
? "i18n:govoplan-calendar.delete_event_occurrence_message"
|
||||
: "i18n:govoplan-calendar.delete_event_series_message", {
|
||||
value0: event?.summary || "Event"
|
||||
})}
|
||||
confirmLabel="i18n:govoplan-calendar.delete.f6fdbe48"
|
||||
tone="danger"
|
||||
busy={saving}
|
||||
onCancel={() => setConfirmingDelete(false)}
|
||||
onConfirm={() => event && void onDelete(event, editScope)}
|
||||
/>
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Button,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
LoadingFrame,
|
||||
StatusBadge,
|
||||
type ApiSettings,
|
||||
@@ -14,6 +15,10 @@ import {
|
||||
type CalendarMigrationBatch,
|
||||
} from "../../api/calendar";
|
||||
import { dateTimeLabel, errorText } from "./calendarViewModel";
|
||||
import {
|
||||
CALENDAR_I18N,
|
||||
CALENDAR_SOURCE_DOCUMENTATION,
|
||||
} from "./interfacePatterns";
|
||||
|
||||
export function CalendarMigrationDialog({
|
||||
settings,
|
||||
@@ -107,6 +112,7 @@ export function CalendarMigrationDialog({
|
||||
<LoadingFrame loading label="Loading remote move"><div /></LoadingFrame>
|
||||
) : (
|
||||
<div className="calendar-migration-body">
|
||||
<DocumentationHelpLink reference={CALENDAR_SOURCE_DOCUMENTATION} />
|
||||
{error && (
|
||||
<DismissibleAlert tone="danger" resetKey={error}>
|
||||
{error}
|
||||
@@ -166,6 +172,7 @@ export function CalendarMigrationDialog({
|
||||
variant="danger"
|
||||
onClick={() => void cancelMigration()}
|
||||
disabled={working || cancellationEvidence.trim().length < 10}
|
||||
disabledReason={working ? CALENDAR_I18N.saving : cancellationEvidence.trim().length < 10 ? CALENDAR_I18N.cancellationEvidenceRequired : undefined}
|
||||
>
|
||||
<XCircle size={16} /> Cancel remote move
|
||||
</Button>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { RefreshCw, RotateCcw, ScanSearch, Trash2 } from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
SegmentedControl,
|
||||
StatusBadge,
|
||||
type ApiSettings,
|
||||
@@ -17,6 +19,10 @@ import {
|
||||
type CalendarSyncSource,
|
||||
} from "../../api/calendar";
|
||||
import { dateTimeLabel, errorText } from "./calendarViewModel";
|
||||
import {
|
||||
CALENDAR_I18N,
|
||||
CALENDAR_RECOVERY_DOCUMENTATION,
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type OutboxFilter = "unresolved" | "all";
|
||||
type RecoveryAction = "retry" | "reconcile" | "discard";
|
||||
@@ -94,7 +100,7 @@ export function CalendarOutboxDialog({
|
||||
onClose={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button type="button" onClick={() => void load()} disabled={loading || Boolean(busyOperationId)}>
|
||||
<Button type="button" onClick={() => void load()} disabled={loading || Boolean(busyOperationId)} disabledReason={loading ? CALENDAR_I18N.loading : busyOperationId ? CALENDAR_I18N.saving : undefined}>
|
||||
<RefreshCw size={16} className={loading ? "calendar-sync-spin" : undefined} /> i18n:govoplan-calendar.refresh.56e3badc
|
||||
</Button>
|
||||
<Button type="button" onClick={onClose} disabled={Boolean(busyOperationId)}>
|
||||
@@ -105,6 +111,7 @@ export function CalendarOutboxDialog({
|
||||
>
|
||||
<div className="calendar-outbox-body">
|
||||
<p className="calendar-outbox-calendar-name">{calendar.name}</p>
|
||||
<DocumentationHelpLink reference={CALENDAR_RECOVERY_DOCUMENTATION} />
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
<div className="calendar-outbox-toolbar">
|
||||
<SegmentedControl<OutboxFilter>
|
||||
@@ -191,23 +198,29 @@ function RecoveryActions({
|
||||
{available.length > 0 && (
|
||||
<div className="calendar-outbox-actions">
|
||||
{available.includes("retry") && (
|
||||
<Button type="button" onClick={() => onRecover("retry")} disabled={busy}>
|
||||
<Button type="button" onClick={() => onRecover("retry")} disabled={busy} disabledReason={busy ? CALENDAR_I18N.saving : undefined}>
|
||||
<RotateCcw size={15} /> i18n:govoplan-calendar.retry.3fda8f1c
|
||||
</Button>
|
||||
)}
|
||||
{available.includes("reconcile") && (
|
||||
<Button type="button" onClick={() => onRecover("reconcile")} disabled={busy}>
|
||||
<Button type="button" onClick={() => onRecover("reconcile")} disabled={busy} disabledReason={busy ? CALENDAR_I18N.saving : undefined}>
|
||||
<ScanSearch size={15} /> i18n:govoplan-calendar.reconcile.6c595f64
|
||||
</Button>
|
||||
)}
|
||||
{available.includes("discard") && (
|
||||
<Button type="button" variant="danger" onClick={() => onRecover("discard")} disabled={busy}>
|
||||
<Button type="button" variant="danger" onClick={() => onRecover("discard")} disabled={busy} disabledReason={busy ? CALENDAR_I18N.saving : undefined}>
|
||||
<Trash2 size={15} /> i18n:govoplan-calendar.discard.23a76911
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!available.length && unavailableReason && <p className="calendar-form-note">{unavailableReason}</p>}
|
||||
{!available.length && unavailableReason && (
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
reason={{ summary: unavailableReason }}
|
||||
documentation={CALENDAR_RECOVERY_DOCUMENTATION}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AdminIconButton,
|
||||
Button,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
LoadingFrame,
|
||||
SegmentedControl,
|
||||
TableActionGroup,
|
||||
@@ -98,6 +99,10 @@ import {
|
||||
type CalendarViewPreferences,
|
||||
type ContinuousViewport,
|
||||
} from "./calendarViewModel";
|
||||
import {
|
||||
CALENDAR_DOCUMENTATION,
|
||||
CALENDAR_I18N,
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type EventEditScope = "occurrence" | "series";
|
||||
type EventDialogState =
|
||||
@@ -797,7 +802,8 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
label: syncing ? i18nMessage("i18n:govoplan-calendar.syncing_value.ca80b487", { value0: calendar.name }) : i18nMessage("i18n:govoplan-calendar.sync_value.72e4ba66", { value0: calendar.name }),
|
||||
icon: <RefreshCw size={15} className={syncing ? "calendar-sync-spin" : undefined} />,
|
||||
onClick: () => void handleSyncSource(source),
|
||||
disabled: saving || syncing
|
||||
disabled: saving || syncing,
|
||||
disabledReason: saving || syncing ? CALENDAR_I18N.saving : undefined
|
||||
},
|
||||
(canManageCalendars || canDeleteCalendars) && {
|
||||
id: "edit",
|
||||
@@ -815,7 +821,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
}
|
||||
{canManageCalendars &&
|
||||
<div className="calendar-create-action">
|
||||
<Button type="button" onClick={() => setCalendarDialog({ kind: "create" })} disabled={saving}>
|
||||
<Button type="button" onClick={() => setCalendarDialog({ kind: "create" })} disabled={saving} disabledReason={saving ? CALENDAR_I18N.saving : undefined}>
|
||||
<Plus size={16} /> i18n:govoplan-calendar.add_calendar.124c55eb
|
||||
</Button>
|
||||
</div>
|
||||
@@ -889,13 +895,14 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
</div>
|
||||
|
||||
<div className="calendar-toolbar-right">
|
||||
<DocumentationHelpLink reference={CALENDAR_DOCUMENTATION} />
|
||||
<AdminIconButton
|
||||
label="i18n:govoplan-calendar.refresh.56e3badc"
|
||||
icon={<RefreshCw size={18} />}
|
||||
onClick={() => void loadEvents()}
|
||||
/>
|
||||
{canWrite &&
|
||||
<Button type="button" variant="primary" onClick={() => setEventDialog({ kind: "create" })} disabled={!targetCalendarId}>
|
||||
<Button type="button" variant="primary" onClick={() => setEventDialog({ kind: "create" })} disabled={!targetCalendarId} disabledReason={!targetCalendarId ? CALENDAR_I18N.targetCalendarRequired : undefined}>
|
||||
<Plus size={17} /> i18n:govoplan-calendar.new.6403f2b7
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
ToggleSwitch,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -14,6 +16,10 @@ import {
|
||||
updateCalendarViewPreferences,
|
||||
type CalendarViewPreferences
|
||||
} from "../../api/calendar";
|
||||
import {
|
||||
CALENDAR_DOCUMENTATION,
|
||||
CALENDAR_I18N,
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type CalendarPreferenceDraft = Pick<
|
||||
CalendarViewPreferences,
|
||||
@@ -74,11 +80,11 @@ export default function CalendarSettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences() {
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
@@ -92,17 +98,36 @@ export default function CalendarSettingsPanel({
|
||||
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 (
|
||||
<div className="dashboard-grid settings-dashboard-grid calendar-settings-panel">
|
||||
<div className="calendar-settings-documentation">
|
||||
<DocumentationHelpLink reference={CALENDAR_DOCUMENTATION} />
|
||||
</div>
|
||||
<Card title="Calendar display">
|
||||
<div className="form-grid">
|
||||
<ToggleSwitch
|
||||
@@ -203,7 +228,8 @@ export default function CalendarSettingsPanel({
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={disabled || !dirty}
|
||||
disabled={Boolean(saveDisabledReason)}
|
||||
disabledReason={saveDisabledReason}
|
||||
onClick={() => void savePreferences()}
|
||||
>
|
||||
<Save size={16} /> {saving ? "Saving" : "Save preferences"}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||
|
||||
export const CALENDAR_DOCUMENTATION = {
|
||||
topicId: "calendar.manage-calendars-and-events",
|
||||
documentationType: "user"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const CALENDAR_SOURCE_DOCUMENTATION = {
|
||||
topicId: "calendar.external-sources-and-sync",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const CALENDAR_RECOVERY_DOCUMENTATION = {
|
||||
topicId: "calendar.outbound-change-recovery",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const CALENDAR_I18N = {
|
||||
loading: "i18n:govoplan-calendar.reason.loading",
|
||||
saving: "i18n:govoplan-calendar.reason.saving",
|
||||
syncing: "i18n:govoplan-calendar.reason.syncing",
|
||||
readOnly: "i18n:govoplan-calendar.reason.read_only",
|
||||
calendarWriteRequired: "i18n:govoplan-calendar.reason.calendar_write_required",
|
||||
calendarAdminRequired: "i18n:govoplan-calendar.reason.calendar_admin_required",
|
||||
syncPermissionRequired: "i18n:govoplan-calendar.reason.sync_permission_required",
|
||||
migrationLocked: "i18n:govoplan-calendar.reason.migration_locked",
|
||||
targetCalendarRequired: "i18n:govoplan-calendar.reason.target_calendar_required",
|
||||
eventTitleRequired: "i18n:govoplan-calendar.reason.event_title_required",
|
||||
calendarNameRequired: "i18n:govoplan-calendar.reason.calendar_name_required",
|
||||
sourceDetailsRequired: "i18n:govoplan-calendar.reason.source_details_required",
|
||||
noChanges: "i18n:govoplan-calendar.reason.no_changes",
|
||||
cancellationEvidenceRequired: "i18n:govoplan-calendar.reason.cancellation_evidence_required"
|
||||
} as const;
|
||||
@@ -2,6 +2,33 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-calendar.surface.navigation": "Calendar navigation",
|
||||
"i18n:govoplan-calendar.surface.page": "Calendar workspace",
|
||||
"i18n:govoplan-calendar.surface.sidebar": "Calendar list",
|
||||
"i18n:govoplan-calendar.surface.agenda": "Agenda",
|
||||
"i18n:govoplan-calendar.surface.workspace": "Calendar view",
|
||||
"i18n:govoplan-calendar.surface.event_editor": "Event editor",
|
||||
"i18n:govoplan-calendar.surface.collection_editor": "Calendar source editor",
|
||||
"i18n:govoplan-calendar.surface.sync_status": "Synchronization status",
|
||||
"i18n:govoplan-calendar.surface.outbox": "Outbound changes",
|
||||
"i18n:govoplan-calendar.surface.migration": "Remote calendar move",
|
||||
"i18n:govoplan-calendar.reason.loading": "Calendar data is loading.",
|
||||
"i18n:govoplan-calendar.reason.saving": "A calendar change is already being saved.",
|
||||
"i18n:govoplan-calendar.reason.syncing": "This calendar is already synchronizing.",
|
||||
"i18n:govoplan-calendar.reason.read_only": "You can view this calendar, but your account cannot change events.",
|
||||
"i18n:govoplan-calendar.reason.calendar_write_required": "Calendar write permission is required.",
|
||||
"i18n:govoplan-calendar.reason.calendar_admin_required": "Calendar administration permission is required.",
|
||||
"i18n:govoplan-calendar.reason.sync_permission_required": "Calendar import permission is required to synchronize this source.",
|
||||
"i18n:govoplan-calendar.reason.migration_locked": "Calendar changes are locked while the remote move is active or unresolved.",
|
||||
"i18n:govoplan-calendar.reason.target_calendar_required": "Create or select a calendar before adding an event.",
|
||||
"i18n:govoplan-calendar.reason.event_title_required": "Enter an event title before saving.",
|
||||
"i18n:govoplan-calendar.reason.calendar_name_required": "Enter a calendar name before saving.",
|
||||
"i18n:govoplan-calendar.reason.source_details_required": "Complete the required source URL and credential fields before saving.",
|
||||
"i18n:govoplan-calendar.reason.no_changes": "There are no unsaved changes.",
|
||||
"i18n:govoplan-calendar.reason.cancellation_evidence_required": "Record at least 10 characters of cancellation evidence.",
|
||||
"i18n:govoplan-calendar.delete_event_title": "Delete event?",
|
||||
"i18n:govoplan-calendar.delete_event_occurrence_message": "Delete the selected occurrence of {value0}? A remote deletion is queued when the calendar is synchronized.",
|
||||
"i18n:govoplan-calendar.delete_event_series_message": "Delete the whole series {value0}? A remote deletion is queued when the calendar is synchronized.",
|
||||
"i18n:govoplan-calendar.add_calendar.124c55eb": "Add calendar...",
|
||||
"i18n:govoplan-calendar.add_calendar.8fadb5bc": "Add calendar",
|
||||
"i18n:govoplan-calendar.add.61cc55aa": "Add",
|
||||
@@ -213,6 +240,33 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-calendar.workweek.2fef6ea4": "Workweek"
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-calendar.surface.navigation": "Kalendernavigation",
|
||||
"i18n:govoplan-calendar.surface.page": "Kalenderarbeitsbereich",
|
||||
"i18n:govoplan-calendar.surface.sidebar": "Kalenderliste",
|
||||
"i18n:govoplan-calendar.surface.agenda": "Agenda",
|
||||
"i18n:govoplan-calendar.surface.workspace": "Kalenderansicht",
|
||||
"i18n:govoplan-calendar.surface.event_editor": "Termineditor",
|
||||
"i18n:govoplan-calendar.surface.collection_editor": "Kalenderquellen bearbeiten",
|
||||
"i18n:govoplan-calendar.surface.sync_status": "Synchronisierungsstatus",
|
||||
"i18n:govoplan-calendar.surface.outbox": "Ausgehende Änderungen",
|
||||
"i18n:govoplan-calendar.surface.migration": "Entfernten Kalender verschieben",
|
||||
"i18n:govoplan-calendar.reason.loading": "Kalenderdaten werden geladen.",
|
||||
"i18n:govoplan-calendar.reason.saving": "Eine Kalenderänderung wird bereits gespeichert.",
|
||||
"i18n:govoplan-calendar.reason.syncing": "Dieser Kalender wird bereits synchronisiert.",
|
||||
"i18n:govoplan-calendar.reason.read_only": "Sie können diesen Kalender anzeigen, aber Ihr Konto darf Termine nicht ändern.",
|
||||
"i18n:govoplan-calendar.reason.calendar_write_required": "Die Berechtigung zum Bearbeiten von Kalendern ist erforderlich.",
|
||||
"i18n:govoplan-calendar.reason.calendar_admin_required": "Die Berechtigung zur Kalenderverwaltung ist erforderlich.",
|
||||
"i18n:govoplan-calendar.reason.sync_permission_required": "Zum Synchronisieren dieser Quelle ist die Kalender-Importberechtigung erforderlich.",
|
||||
"i18n:govoplan-calendar.reason.migration_locked": "Kalenderänderungen sind gesperrt, solange die entfernte Verschiebung aktiv oder ungeklärt ist.",
|
||||
"i18n:govoplan-calendar.reason.target_calendar_required": "Erstellen oder wählen Sie einen Kalender, bevor Sie einen Termin hinzufügen.",
|
||||
"i18n:govoplan-calendar.reason.event_title_required": "Geben Sie vor dem Speichern einen Termintitel ein.",
|
||||
"i18n:govoplan-calendar.reason.calendar_name_required": "Geben Sie vor dem Speichern einen Kalendernamen ein.",
|
||||
"i18n:govoplan-calendar.reason.source_details_required": "Vervollständigen Sie vor dem Speichern die erforderlichen Quellen- und Zugangsdaten.",
|
||||
"i18n:govoplan-calendar.reason.no_changes": "Es gibt keine ungespeicherten Änderungen.",
|
||||
"i18n:govoplan-calendar.reason.cancellation_evidence_required": "Dokumentieren Sie die Abbruchbegründung mit mindestens 10 Zeichen.",
|
||||
"i18n:govoplan-calendar.delete_event_title": "Termin löschen?",
|
||||
"i18n:govoplan-calendar.delete_event_occurrence_message": "Das ausgewählte Vorkommen von {value0} löschen? Bei einem synchronisierten Kalender wird eine entfernte Löschung eingeplant.",
|
||||
"i18n:govoplan-calendar.delete_event_series_message": "Die gesamte Serie {value0} löschen? Bei einem synchronisierten Kalender wird eine entfernte Löschung eingeplant.",
|
||||
"i18n:govoplan-calendar.add_calendar.124c55eb": "Add calendar...",
|
||||
"i18n:govoplan-calendar.add_calendar.8fadb5bc": "Add calendar",
|
||||
"i18n:govoplan-calendar.add.61cc55aa": "Hinzufügen",
|
||||
|
||||
+12
-2
@@ -99,6 +99,16 @@ export const calendarModule: PlatformWebModule = {
|
||||
optionalDependencies: ["mail", "tasks", "scheduling", "appointments", "workflow", "notifications", "dms", "connectors"],
|
||||
translations,
|
||||
viewSurfaces: [
|
||||
{ id: "calendar.navigation", moduleId: "calendar", kind: "navigation", label: "i18n:govoplan-calendar.surface.navigation", order: 10 },
|
||||
{ id: "calendar.page", moduleId: "calendar", kind: "route", label: "i18n:govoplan-calendar.surface.page", order: 20 },
|
||||
{ id: "calendar.page.sidebar", moduleId: "calendar", kind: "section", label: "i18n:govoplan-calendar.surface.sidebar", parentId: "calendar.page", order: 10 },
|
||||
{ id: "calendar.page.agenda", moduleId: "calendar", kind: "section", label: "i18n:govoplan-calendar.surface.agenda", parentId: "calendar.page.sidebar", order: 20 },
|
||||
{ id: "calendar.page.workspace", moduleId: "calendar", kind: "section", label: "i18n:govoplan-calendar.surface.workspace", parentId: "calendar.page", order: 30 },
|
||||
{ id: "calendar.event-editor", moduleId: "calendar", kind: "action", label: "i18n:govoplan-calendar.surface.event_editor", parentId: "calendar.page", order: 40 },
|
||||
{ id: "calendar.collection-editor", moduleId: "calendar", kind: "action", label: "i18n:govoplan-calendar.surface.collection_editor", parentId: "calendar.page.sidebar", order: 50 },
|
||||
{ id: "calendar.sync-status", moduleId: "calendar", kind: "section", label: "i18n:govoplan-calendar.surface.sync_status", parentId: "calendar.collection-editor", order: 60 },
|
||||
{ id: "calendar.outbox", moduleId: "calendar", kind: "action", label: "i18n:govoplan-calendar.surface.outbox", parentId: "calendar.sync-status", order: 70 },
|
||||
{ id: "calendar.migration", moduleId: "calendar", kind: "action", label: "i18n:govoplan-calendar.surface.migration", parentId: "calendar.sync-status", order: 80 },
|
||||
{
|
||||
id: "calendar.widget.upcoming",
|
||||
moduleId: "calendar",
|
||||
@@ -114,9 +124,9 @@ export const calendarModule: PlatformWebModule = {
|
||||
order: 45
|
||||
}
|
||||
],
|
||||
navItems: [{ to: "/calendar", label: "i18n:govoplan-calendar.calendar.adab5090", iconName: "calendar", anyOf: eventRead, order: 55 }],
|
||||
navItems: [{ to: "/calendar", label: "i18n:govoplan-calendar.calendar.adab5090", iconName: "calendar", anyOf: eventRead, order: 55, surfaceId: "calendar.navigation" }],
|
||||
routes: [
|
||||
{ path: "/calendar", anyOf: eventRead, order: 55, render: ({ settings, auth }) => createElement(CalendarPage, { settings, auth }) }],
|
||||
{ path: "/calendar", anyOf: eventRead, order: 55, surfaceId: "calendar.page", render: ({ settings, auth }) => createElement(CalendarPage, { settings, auth }) }],
|
||||
uiCapabilities: {
|
||||
"calendar.picker": calendarPicker,
|
||||
"dashboard.widgets": calendarDashboardWidgets,
|
||||
|
||||
@@ -863,6 +863,16 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.calendar-dialog-documentation,
|
||||
.calendar-settings-documentation {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.calendar-settings-documentation {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.calendar-dialog-form label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
|
||||
Reference in New Issue
Block a user