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:
parent
a119d6b12f
commit
e0084228f3
1 changed files with 41 additions and 51 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue