Migrate Calendar interface patterns
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Calendar interface pattern migration
|
||||
|
||||
Calendar uses the platform workspace pattern without changing ownership of calendar or synchronization state.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- `calendar.page` is the route-level workspace.
|
||||
- `calendar.page.sidebar` contains calendar visibility and collection actions; `calendar.page.agenda` is its event list.
|
||||
- `calendar.page.workspace` contains continuous, month, week, workweek, and day views.
|
||||
- `calendar.event-editor` owns event create, edit, occurrence/series selection, and deletion confirmation.
|
||||
- `calendar.collection-editor` owns local and external collection configuration. Its `calendar.sync-status`, `calendar.outbox`, and `calendar.migration` children expose synchronization state and recovery.
|
||||
- `calendar.settings.preferences` and `calendar.widget.upcoming` remain composed Settings and Dashboard surfaces.
|
||||
|
||||
The backend and WebUI manifests publish the same identifiers and parent hierarchy so Views can filter the route and contributed surfaces consistently.
|
||||
|
||||
## Consequences and recovery
|
||||
|
||||
Event and collection drafts use the shared unsaved-change guard. A write that completes but whose refresh fails is not repeated blindly by Calendar synchronization workers; durable outbox and migration state remain the source of recovery evidence. Event deletion uses the shared confirmation dialog. Collection removal and destructive remote moves keep their specialized confirmation because they must expose event counts, transfer choices, authorization text, and evidence.
|
||||
|
||||
Unavailable consequential actions remain visible where possible and explain the missing permission, input, active write, or migration lock. Contextual help resolves through `govoplan-docs` when installed and otherwise uses the hosted documentation fallback.
|
||||
|
||||
## Optional boundaries
|
||||
|
||||
Calendar does not import optional Mail, Campaign, Scheduling, Notifications, Connectors, Audit, or Ops implementations. Integrations continue through declared capabilities, interfaces, and stable references. Local calendars and the Calendar route remain usable without those optional modules.
|
||||
@@ -545,7 +545,23 @@ manifest = ModuleManifest(
|
||||
documentation_types=("user",),
|
||||
audience=("user", "calendar_manager"),
|
||||
related_modules=("scheduling", "notifications"),
|
||||
metadata={"kind": "reference"},
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"calendar.page",
|
||||
"calendar.page.sidebar",
|
||||
"calendar.page.agenda",
|
||||
"calendar.page.workspace",
|
||||
"calendar.event-editor",
|
||||
"calendar.settings.preferences",
|
||||
"calendar.widget.upcoming",
|
||||
"calendar.state.read-only",
|
||||
],
|
||||
"consequence_classes": {
|
||||
"save_event": "create or update the authoritative event and queue synchronized writes when required",
|
||||
"delete_event": "delete the selected occurrence or series and queue synchronized deletion when required",
|
||||
},
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="calendar.external-sources-and-sync",
|
||||
@@ -555,7 +571,22 @@ manifest = ModuleManifest(
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "calendar_manager", "operator"),
|
||||
related_modules=("connectors", "audit", "ops"),
|
||||
metadata={"kind": "reference"},
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"calendar.collection-editor",
|
||||
"calendar.sync-status",
|
||||
"calendar.migration",
|
||||
"calendar.state.source-admin-required",
|
||||
],
|
||||
"consequence_classes": {
|
||||
"change_collection": "change calendar identity, source configuration, credentials, and synchronization policy",
|
||||
"delete_or_remove_collection": "delete a local calendar or remove an external source after explicit event handling",
|
||||
"synchronize_source": "read and, where configured, write remote state using bounded synchronization evidence",
|
||||
"force_full_sync": "re-read the complete remote source and reconcile it against local state",
|
||||
"execute_remote_move": "copy all destination resources before conditionally deleting source resources",
|
||||
},
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="calendar.campaign-invitations-and-replies",
|
||||
@@ -593,7 +624,16 @@ manifest = ModuleManifest(
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("calendar_manager", "operator", "tenant_admin"),
|
||||
related_modules=("ops", "audit"),
|
||||
metadata={"kind": "runbook"},
|
||||
metadata={
|
||||
"kind": "runbook",
|
||||
"help_contexts": [
|
||||
"calendar.outbox",
|
||||
"calendar.state.outbox-recovery",
|
||||
],
|
||||
"consequence_classes": {
|
||||
"reconcile_outbox": "compare the latest desired generation with remote state before retry or discard",
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
capability_factories={
|
||||
@@ -616,6 +656,84 @@ manifest = ModuleManifest(
|
||||
),
|
||||
nav_items=(NavItem(path="/calendar", label="Calendar", icon="calendar", required_any=("calendar:event:read",), order=55),),
|
||||
view_surfaces=(
|
||||
ViewSurface(
|
||||
id="calendar.navigation",
|
||||
module_id="calendar",
|
||||
kind="navigation",
|
||||
label="Calendar navigation",
|
||||
order=10,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.page",
|
||||
module_id="calendar",
|
||||
kind="route",
|
||||
label="Calendar workspace",
|
||||
order=20,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.page.sidebar",
|
||||
module_id="calendar",
|
||||
kind="section",
|
||||
label="Calendar list",
|
||||
parent_id="calendar.page",
|
||||
order=10,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.page.agenda",
|
||||
module_id="calendar",
|
||||
kind="section",
|
||||
label="Calendar agenda",
|
||||
parent_id="calendar.page.sidebar",
|
||||
order=20,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.page.workspace",
|
||||
module_id="calendar",
|
||||
kind="section",
|
||||
label="Calendar view",
|
||||
parent_id="calendar.page",
|
||||
order=30,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.event-editor",
|
||||
module_id="calendar",
|
||||
kind="action",
|
||||
label="Event editor",
|
||||
parent_id="calendar.page",
|
||||
order=40,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.collection-editor",
|
||||
module_id="calendar",
|
||||
kind="action",
|
||||
label="Calendar source editor",
|
||||
parent_id="calendar.page.sidebar",
|
||||
order=50,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.sync-status",
|
||||
module_id="calendar",
|
||||
kind="section",
|
||||
label="Calendar synchronization status",
|
||||
parent_id="calendar.collection-editor",
|
||||
order=60,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.outbox",
|
||||
module_id="calendar",
|
||||
kind="action",
|
||||
label="Outbound calendar changes",
|
||||
parent_id="calendar.sync-status",
|
||||
order=70,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.migration",
|
||||
module_id="calendar",
|
||||
kind="action",
|
||||
label="Remote calendar move",
|
||||
parent_id="calendar.sync-status",
|
||||
order=80,
|
||||
),
|
||||
ViewSurface(
|
||||
id="calendar.widget.upcoming",
|
||||
module_id="calendar",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from govoplan_calendar.backend.manifest import get_manifest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class CalendarInterfaceDocumentationContractTests(unittest.TestCase):
|
||||
def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
|
||||
frontend = get_manifest().frontend
|
||||
self.assertIsNotNone(frontend)
|
||||
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
|
||||
expected = {
|
||||
"calendar.navigation",
|
||||
"calendar.page",
|
||||
"calendar.page.sidebar",
|
||||
"calendar.page.agenda",
|
||||
"calendar.page.workspace",
|
||||
"calendar.event-editor",
|
||||
"calendar.collection-editor",
|
||||
"calendar.sync-status",
|
||||
"calendar.outbox",
|
||||
"calendar.migration",
|
||||
"calendar.settings.preferences",
|
||||
"calendar.widget.upcoming",
|
||||
}
|
||||
self.assertEqual(expected, set(surfaces))
|
||||
self.assertEqual("calendar.page", surfaces["calendar.page.sidebar"].parent_id)
|
||||
self.assertEqual("calendar.page.sidebar", surfaces["calendar.page.agenda"].parent_id)
|
||||
self.assertEqual("calendar.page", surfaces["calendar.page.workspace"].parent_id)
|
||||
self.assertEqual("calendar.page", surfaces["calendar.event-editor"].parent_id)
|
||||
self.assertEqual("calendar.page.sidebar", surfaces["calendar.collection-editor"].parent_id)
|
||||
self.assertEqual("calendar.collection-editor", surfaces["calendar.sync-status"].parent_id)
|
||||
self.assertEqual("calendar.sync-status", surfaces["calendar.outbox"].parent_id)
|
||||
self.assertEqual("calendar.sync-status", surfaces["calendar.migration"].parent_id)
|
||||
|
||||
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||
events = topics["calendar.manage-calendars-and-events"]
|
||||
sources = topics["calendar.external-sources-and-sync"]
|
||||
recovery = topics["calendar.outbound-change-recovery"]
|
||||
|
||||
self.assertIn("calendar.event-editor", events.metadata["help_contexts"])
|
||||
self.assertIn("save_event", events.metadata["consequence_classes"])
|
||||
self.assertIn("delete_event", events.metadata["consequence_classes"])
|
||||
self.assertIn("calendar.collection-editor", sources.metadata["help_contexts"])
|
||||
self.assertIn("force_full_sync", sources.metadata["consequence_classes"])
|
||||
self.assertIn("execute_remote_move", sources.metadata["consequence_classes"])
|
||||
self.assertIn("calendar.outbox", recovery.metadata["help_contexts"])
|
||||
self.assertIn("reconcile_outbox", recovery.metadata["consequence_classes"])
|
||||
|
||||
def test_webui_uses_shared_help_guard_and_confirmation_components(self) -> None:
|
||||
event_dialog = (REPO_ROOT / "webui/src/features/calendar/CalendarEventDialog.tsx").read_text(encoding="utf-8")
|
||||
collection_dialog = (REPO_ROOT / "webui/src/features/calendar/CalendarCollectionDialogs.tsx").read_text(encoding="utf-8")
|
||||
settings_panel = (REPO_ROOT / "webui/src/features/calendar/CalendarSettingsPanel.tsx").read_text(encoding="utf-8")
|
||||
|
||||
for component in ("ActionBlockerHint", "ConfirmDialog", "DocumentationHelpLink", "useUnsavedDraftGuard"):
|
||||
self.assertIn(component, event_dialog)
|
||||
for component in ("ActionBlockerHint", "DocumentationHelpLink", "useUnsavedDraftGuard"):
|
||||
self.assertIn(component, collection_dialog)
|
||||
for component in ("DocumentationHelpLink", "useUnsavedDraftGuard"):
|
||||
self.assertIn(component, settings_panel)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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