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" :
@@ -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>
);
}
+10 -3
View File
@@ -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;
+54
View File
@@ -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
View File
@@ -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,
+10
View File
@@ -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;