ultisuite-client/lib/agenda/agenda-store.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

303 lines
11 KiB
TypeScript

"use client"
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage"
import {
DEFAULT_AGENDA_USER_SETTINGS,
} from "@/lib/agenda/agenda-settings-defaults"
import {
migrateAutoImportIdentityKeys,
normalizeAutoImportInvitationSources,
normalizeInvitationImportExclusions,
} from "@/lib/agenda/agenda-destination-identities"
import type {
AgendaAutoImportSource,
AgendaCalendarView,
AgendaDurationStep,
AgendaExternalCalendar,
AgendaInvitationExclusion,
AgendaTimeFormat,
AgendaUserSettings,
AgendaVideoProvider,
AgendaWeekStart,
} from "@/lib/agenda/agenda-settings-types"
import {
EXTERNAL_CALENDAR_ID_PREFIX,
isReservedAgendaViewName,
} from "@/lib/agenda/agenda-calendar-visibility"
import type { AgendaView } from "./agenda-url.ts"
function newAgendaId(prefix: string): string {
return `${prefix}${crypto.randomUUID()}`
}
type AgendaSettingsState = AgendaUserSettings & {
quickSettingsOpen: boolean
hiddenCalendarIds: string[]
lastView: AgendaView
activeCalendarViewId: string | null
}
type PersistedAgendaSettings = Partial<
AgendaSettingsState & {
autoImportInvitationIdentityKeys?: string[]
}
>
type AgendaSettingsActions = {
setQuickSettingsOpen: (open: boolean) => void
setDefaultVideoProvider: (provider: AgendaVideoProvider) => void
setVideoProviderApiKey: (provider: AgendaVideoProvider, key: string) => void
setDefaultInvitationIdentityKey: (key: string | null) => void
setAutoImportInvitationSources: (sources: AgendaAutoImportSource[]) => void
setInvitationImportExclusions: (items: AgendaInvitationExclusion[]) => void
setWeekStart: (weekStart: AgendaWeekStart) => void
setDefaultQuickDurationMinutes: (minutes: number) => void
setVisibleHoursStart: (minutes: number) => void
setVisibleHoursEnd: (minutes: number) => void
setTimeFormat: (format: AgendaTimeFormat) => void
setDragSnapMinutes: (minutes: AgendaDurationStep) => void
setButtonSnapMinutes: (minutes: AgendaDurationStep) => void
toggleCalendarVisible: (id: string) => void
setLastView: (view: AgendaView) => void
setExternalCalendars: (calendars: AgendaExternalCalendar[]) => void
addExternalCalendar: (input: Omit<AgendaExternalCalendar, "id">) => string
updateExternalCalendar: (
id: string,
patch: Partial<Omit<AgendaExternalCalendar, "id">>,
) => void
removeExternalCalendar: (id: string) => void
setCalendarViews: (views: AgendaCalendarView[]) => void
addCalendarView: (input: Omit<AgendaCalendarView, "id">) => string
updateCalendarView: (
id: string,
patch: Partial<Omit<AgendaCalendarView, "id">>,
) => void
removeCalendarView: (id: string) => void
setDefaultCalendarViewId: (id: string | null) => void
setActiveCalendarViewId: (id: string | null) => void
setAccountEnabledCalendarIds: (
accountId: string,
calendarIds: string[] | null,
) => void
toggleAccountCalendar: (
accountId: string,
calendarId: string,
allCalendarIds: string[],
) => void
}
export const useAgendaSettingsStore = create<
AgendaSettingsState & AgendaSettingsActions
>()(
persist(
(set, get) => ({
...DEFAULT_AGENDA_USER_SETTINGS,
quickSettingsOpen: false,
hiddenCalendarIds: [],
lastView: "week",
activeCalendarViewId: null,
setQuickSettingsOpen: (quickSettingsOpen) => set({ quickSettingsOpen }),
setDefaultVideoProvider: (defaultVideoProvider) => set({ defaultVideoProvider }),
setVideoProviderApiKey: (provider, key) =>
set((s) => ({
videoProviderApiKeys: { ...s.videoProviderApiKeys, [provider]: key },
})),
setDefaultInvitationIdentityKey: (defaultInvitationIdentityKey) =>
set({ defaultInvitationIdentityKey }),
setAutoImportInvitationSources: (autoImportInvitationSources) =>
set({
autoImportInvitationSources: normalizeAutoImportInvitationSources(
autoImportInvitationSources,
),
}),
setInvitationImportExclusions: (invitationImportExclusions) =>
set({
invitationImportExclusions: normalizeInvitationImportExclusions(
invitationImportExclusions,
),
}),
setWeekStart: (weekStart) => set({ weekStart }),
setDefaultQuickDurationMinutes: (defaultQuickDurationMinutes) =>
set({ defaultQuickDurationMinutes }),
setVisibleHoursStart: (visibleHoursStart) => set({ visibleHoursStart }),
setVisibleHoursEnd: (visibleHoursEnd) => set({ visibleHoursEnd }),
setTimeFormat: (timeFormat) => set({ timeFormat }),
setDragSnapMinutes: (dragSnapMinutes) => set({ dragSnapMinutes }),
setButtonSnapMinutes: (buttonSnapMinutes) => set({ buttonSnapMinutes }),
toggleCalendarVisible: (id) => {
const hidden = get().hiddenCalendarIds
set({
hiddenCalendarIds: hidden.includes(id)
? hidden.filter((h) => h !== id)
: [...hidden, id],
})
},
setLastView: (lastView) => set({ lastView }),
setExternalCalendars: (externalCalendars) => set({ externalCalendars }),
addExternalCalendar: (input) => {
const id = newAgendaId(EXTERNAL_CALENDAR_ID_PREFIX)
set((s) => ({
externalCalendars: [...s.externalCalendars, { ...input, id }],
}))
return id
},
updateExternalCalendar: (id, patch) =>
set((s) => ({
externalCalendars: s.externalCalendars.map((calendar) =>
calendar.id === id ? { ...calendar, ...patch } : calendar,
),
})),
removeExternalCalendar: (id) =>
set((s) => ({
externalCalendars: s.externalCalendars.filter((calendar) => calendar.id !== id),
calendarViews: s.calendarViews.map((view) => ({
...view,
calendar_ids: view.calendar_ids.filter((calendarId) => calendarId !== id),
})),
activeCalendarViewId:
s.activeCalendarViewId === id ? null : s.activeCalendarViewId,
hiddenCalendarIds: s.hiddenCalendarIds.filter((hiddenId) => hiddenId !== id),
accountEnabledCalendarIds: Object.fromEntries(
Object.entries(s.accountEnabledCalendarIds).map(([accountId, calendarIds]) => [
accountId,
calendarIds.filter((calendarId) => calendarId !== id),
]),
),
})),
setCalendarViews: (calendarViews) => set({ calendarViews }),
addCalendarView: (input) => {
const name = input.name.trim()
if (isReservedAgendaViewName(name)) return ""
const id = newAgendaId("view:")
set((s) => ({
calendarViews: [...s.calendarViews, { ...input, name, id }],
}))
return id
},
updateCalendarView: (id, patch) =>
set((s) => {
const nextName = patch.name?.trim()
if (nextName && isReservedAgendaViewName(nextName)) return s
return {
calendarViews: s.calendarViews.map((view) =>
view.id === id
? { ...view, ...patch, ...(nextName ? { name: nextName } : {}) }
: view,
),
}
}),
removeCalendarView: (id) =>
set((s) => ({
calendarViews: s.calendarViews.filter((view) => view.id !== id),
defaultCalendarViewId:
s.defaultCalendarViewId === id ? null : s.defaultCalendarViewId,
activeCalendarViewId:
s.activeCalendarViewId === id ? null : s.activeCalendarViewId,
})),
setDefaultCalendarViewId: (defaultCalendarViewId) =>
set({ defaultCalendarViewId }),
setActiveCalendarViewId: (activeCalendarViewId) => set({ activeCalendarViewId }),
setAccountEnabledCalendarIds: (accountId, calendarIds) =>
set((s) => {
const next = { ...s.accountEnabledCalendarIds }
if (calendarIds === null) {
delete next[accountId]
} else {
next[accountId] = calendarIds
}
return { accountEnabledCalendarIds: next }
}),
toggleAccountCalendar: (accountId, calendarId, allCalendarIds) => {
set((s) => {
const enabled = s.accountEnabledCalendarIds[accountId] ?? allCalendarIds
const next = enabled.includes(calendarId)
? enabled.filter((id) => id !== calendarId)
: [...enabled, calendarId]
const nextMap = { ...s.accountEnabledCalendarIds }
if (
next.length === allCalendarIds.length &&
allCalendarIds.every((id) => next.includes(id))
) {
delete nextMap[accountId]
} else {
nextMap[accountId] = next
}
return { accountEnabledCalendarIds: nextMap }
})
},
}),
{
name: "agenda-settings-store",
version: 3,
migrate: (persisted, version) => {
const state = persisted as PersistedAgendaSettings
if (version < 1) {
if (
!state.autoImportInvitationSources &&
Array.isArray(state.autoImportInvitationIdentityKeys)
) {
state.autoImportInvitationSources = migrateAutoImportIdentityKeys(
state.autoImportInvitationIdentityKeys,
)
}
delete state.autoImportInvitationIdentityKeys
}
if (version < 2) {
state.externalCalendars ??= []
state.calendarViews ??= []
state.defaultCalendarViewId ??= null
state.accountEnabledCalendarIds ??= {}
state.activeCalendarViewId ??= null
}
if (version < 3) {
state.calendarViews = (state.calendarViews ?? []).map((view) =>
isReservedAgendaViewName(view.name)
? { ...view, name: `${view.name} (vue)` }
: view,
)
}
return state as AgendaSettingsState
},
storage: debouncedPersistJSONStorage,
partialize: (s) => ({
defaultVideoProvider: s.defaultVideoProvider,
videoProviderApiKeys: s.videoProviderApiKeys,
defaultInvitationIdentityKey: s.defaultInvitationIdentityKey,
autoImportInvitationSources: s.autoImportInvitationSources,
invitationImportExclusions: s.invitationImportExclusions,
weekStart: s.weekStart,
defaultQuickDurationMinutes: s.defaultQuickDurationMinutes,
visibleHoursStart: s.visibleHoursStart,
visibleHoursEnd: s.visibleHoursEnd,
timeFormat: s.timeFormat,
dragSnapMinutes: s.dragSnapMinutes,
buttonSnapMinutes: s.buttonSnapMinutes,
hiddenCalendarIds: s.hiddenCalendarIds,
lastView: s.lastView,
externalCalendars: s.externalCalendars,
calendarViews: s.calendarViews,
defaultCalendarViewId: s.defaultCalendarViewId,
accountEnabledCalendarIds: s.accountEnabledCalendarIds,
activeCalendarViewId: s.activeCalendarViewId,
}),
},
),
)
interface AgendaUIState {
sidebarCollapsed: boolean
setSidebarCollapsed: (v: boolean) => void
}
export const useAgendaUIStore = create<AgendaUIState>((set) => ({
sidebarCollapsed: false,
setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }),
}))
export function identityKey(email: string, accountId?: string): string {
return `${accountId ?? ""}:${email}`
}