Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
303 lines
11 KiB
TypeScript
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}`
|
|
}
|