feat(PBI-76): migrate jobs-column to user-settings store

Per-instance filter state (kinds + statuses) now lives under
views.jobsColumns[storageKeyPrefix] in user-settings. Removes
the local CSV-encoding helpers — store keeps arrays natively.
A single persist() call writes both fields together so the
two arrays cannot drift in optimistic mid-flight updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-10 12:56:36 +02:00
parent a119d6b12f
commit e0084228f3

View file

@ -1,11 +1,13 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { Button } from '@/components/ui/button'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import JobCard from './job-card'
import { JOB_STATUS_LABELS } from '@/components/shared/job-status'
import { jobStatusToApi, type ClaudeJobStatusApi } from '@/lib/job-status'
import { useUserSettingsStore } from '@/stores/user-settings/store'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import type { JobWithRelations } from '@/actions/jobs-page'
@ -82,20 +84,6 @@ function MultiFilterPills<T extends string>({
)
}
function parseCsv<T extends string>(raw: string | null, allowed: Set<T>): Set<T> {
if (!raw) return new Set()
const out = new Set<T>()
for (const part of raw.split(',')) {
const v = part.trim()
if (v && allowed.has(v as T)) out.add(v as T)
}
return out
}
function setToCsv<T extends string>(s: Set<T>): string {
return Array.from(s).join(',')
}
interface JobsColumnProps {
title: string
jobs: JobWithRelations[]
@ -115,45 +103,50 @@ export default function JobsColumn({
statusOptions,
emptyText,
}: JobsColumnProps) {
const kindKey = `${storageKeyPrefix}_filter_kind`
const statusKey = `${storageKeyPrefix}_filter_status`
const statusValues = useMemo(
const allowedStatuses = useMemo(
() => new Set<ClaudeJobStatusApi>(statusOptions.map((o) => o.value)),
[statusOptions]
[statusOptions],
)
const colPrefs = useUserSettingsStore(
useShallow((s) => s.entities.settings.views?.jobsColumns?.[storageKeyPrefix]),
)
const setPref = useUserSettingsStore((s) => s.setPref)
const [filterKinds, setFilterKinds] = useState<Set<ClaudeJobKind>>(() => new Set())
const [filterStatuses, setFilterStatuses] = useState<Set<ClaudeJobStatusApi>>(() => new Set())
const [prefsLoaded, setPrefsLoaded] = useState(false)
const filterKinds = useMemo<Set<ClaudeJobKind>>(() => {
const out = new Set<ClaudeJobKind>()
for (const v of colPrefs?.kinds ?? []) {
if (KIND_VALUES.has(v as ClaudeJobKind)) out.add(v as ClaudeJobKind)
}
return out
}, [colPrefs?.kinds])
useEffect(() => {
/* eslint-disable react-hooks/set-state-in-effect */
setFilterKinds(parseCsv<ClaudeJobKind>(localStorage.getItem(kindKey), KIND_VALUES))
setFilterStatuses(parseCsv<ClaudeJobStatusApi>(localStorage.getItem(statusKey), statusValues))
setPrefsLoaded(true)
/* eslint-enable react-hooks/set-state-in-effect */
}, [kindKey, statusKey, statusValues])
const filterStatuses = useMemo<Set<ClaudeJobStatusApi>>(() => {
const out = new Set<ClaudeJobStatusApi>()
for (const v of colPrefs?.statuses ?? []) {
if (allowedStatuses.has(v as ClaudeJobStatusApi)) out.add(v as ClaudeJobStatusApi)
}
return out
}, [colPrefs?.statuses, allowedStatuses])
useEffect(() => { if (prefsLoaded) localStorage.setItem(kindKey, setToCsv(filterKinds)) }, [filterKinds, prefsLoaded, kindKey])
useEffect(() => { if (prefsLoaded) localStorage.setItem(statusKey, setToCsv(filterStatuses)) }, [filterStatuses, prefsLoaded, statusKey])
function toggleKind(v: ClaudeJobKind) {
setFilterKinds((prev) => {
const next = new Set(prev)
if (next.has(v)) next.delete(v)
else next.add(v)
return next
function persist(kinds: Set<ClaudeJobKind>, statuses: Set<ClaudeJobStatusApi>) {
void setPref(['views', 'jobsColumns', storageKeyPrefix], {
kinds: Array.from(kinds),
statuses: Array.from(statuses),
})
}
function toggleKind(v: ClaudeJobKind) {
const next = new Set(filterKinds)
if (next.has(v)) next.delete(v)
else next.add(v)
persist(next, filterStatuses)
}
function toggleStatus(v: ClaudeJobStatusApi) {
setFilterStatuses((prev) => {
const next = new Set(prev)
if (next.has(v)) next.delete(v)
else next.add(v)
return next
})
const next = new Set(filterStatuses)
if (next.has(v)) next.delete(v)
else next.add(v)
persist(filterKinds, next)
}
const filtered = jobs.filter((j) => {
@ -207,14 +200,14 @@ export default function JobsColumn({
options={KIND_OPTIONS}
selected={filterKinds}
onToggle={toggleKind}
onClear={() => setFilterKinds(new Set())}
onClear={() => persist(new Set(), filterStatuses)}
/>
<MultiFilterPills
label="Status"
options={statusOptions}
selected={filterStatuses}
onToggle={toggleStatus}
onClear={() => setFilterStatuses(new Set())}
onClear={() => persist(filterKinds, new Set())}
/>
<div className="flex justify-end pt-1 border-t border-border">
<Button
@ -223,10 +216,7 @@ export default function JobsColumn({
size="sm"
className="h-7 text-xs"
disabled={activeFilterCount === 0}
onClick={() => {
setFilterKinds(new Set())
setFilterStatuses(new Set())
}}
onClick={() => persist(new Set(), new Set())}
>
Wis filters
</Button>