diff --git a/lib/user-settings.ts b/lib/user-settings.ts new file mode 100644 index 0000000..86621fd --- /dev/null +++ b/lib/user-settings.ts @@ -0,0 +1,83 @@ +import { z } from 'zod' + +const PriorityFilter = z.union([ + z.number().int().min(1).max(4), + z.literal('all'), +]) + +const SortDir = z.enum(['asc', 'desc']) + +const SprintBacklogPrefs = z.object({ + filterPriority: PriorityFilter.optional(), + filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(), + sort: z.enum(['priority', 'status', 'code']).optional(), + sortDir: SortDir.optional(), + collapsedPbis: z.array(z.string()).optional(), + filterPopoverOpen: z.boolean().optional(), +}).strict() + +const PbiListPrefs = z.object({ + sort: z.enum(['priority', 'code', 'date']).optional(), + filterPriority: PriorityFilter.optional(), + filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(), + sortDir: SortDir.optional(), +}).strict() + +const StoryPanelPrefs = z.object({ + sort: z.enum(['priority', 'code', 'date']).optional(), +}).strict() + +const JobsColumnPrefs = z.object({ + kinds: z.array(z.string()), + statuses: z.array(z.string()), +}).strict() + +const ViewsPrefs = z.object({ + sprintBacklog: SprintBacklogPrefs.optional(), + pbiList: PbiListPrefs.optional(), + storyPanel: StoryPanelPrefs.optional(), + jobsColumns: z.record(z.string(), JobsColumnPrefs).optional(), +}).strict() + +const DevToolsPrefs = z.object({ + debugMode: z.boolean().optional(), +}).strict() + +export const UserSettingsSchema = z.object({ + views: ViewsPrefs.optional(), + devTools: DevToolsPrefs.optional(), +}).strict() + +export type UserSettings = z.infer + +export const DEFAULT_USER_SETTINGS: UserSettings = {} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export function mergeSettings( + prev: UserSettings, + patch: Partial, +): UserSettings { + const out: Record = { ...prev } + for (const [key, patchValue] of Object.entries(patch)) { + if (patchValue === undefined) continue + const prevValue = (prev as Record)[key] + if (isPlainObject(patchValue) && isPlainObject(prevValue)) { + out[key] = mergeSettings( + prevValue as UserSettings, + patchValue as Partial, + ) + } else { + out[key] = patchValue + } + } + return out as UserSettings +} + +export function parseUserSettings(raw: unknown): UserSettings { + if (raw === null || raw === undefined) return DEFAULT_USER_SETTINGS + const result = UserSettingsSchema.safeParse(raw) + return result.success ? result.data : DEFAULT_USER_SETTINGS +}