feat(web): customs section with mark and unmark actions

This commit is contained in:
2026-08-19 18:22:32 +07:00
parent 404f869d42
commit e00648baf7
4 changed files with 129 additions and 6 deletions
+55 -5
View File
@@ -1,11 +1,12 @@
import type { ReactNode } from "react"
import { ArrowRight, CircleCheck, Lock, Pencil, Trash2 } from "lucide-react"
import { ArrowRight, BookmarkCheck, CircleCheck, Lock, Pencil, Trash2, Undo2 } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { cn } from "@/lib/utils"
import type { ChangesetResponse, RecordView } from "@/api/types"
type Tone = "update" | "delete" | "readonly"
type Tone = "update" | "delete" | "custom" | "readonly"
const TONE_META: Record<
Tone,
@@ -25,6 +26,13 @@ const TONE_META: Record<
dot: "var(--diff-delete)",
ring: "ring-[color-mix(in_oklch,var(--diff-delete),transparent_78%)]",
},
custom: {
label: "Customs",
empty: "Нет записей, помеченных как осознанные.",
icon: BookmarkCheck,
dot: "var(--diff-custom)",
ring: "ring-[color-mix(in_oklch,var(--diff-custom),transparent_80%)]",
},
readonly: {
label: "Read-only",
empty: "Нет read-only записей.",
@@ -46,11 +54,13 @@ function RecordRow({
tone,
checked,
onToggle,
action,
}: {
record: RecordView
tone: Tone
checked?: boolean
onToggle?: (key: string) => void
action?: ReactNode
}) {
const meta = TONE_META[tone]
const showArrow = tone !== "delete"
@@ -99,6 +109,8 @@ function RecordRow({
read-only
</Badge>
)}
{action}
</div>
{/* Values line: plain block-level text (not flex) so a long
@@ -137,18 +149,21 @@ function Section({
selected,
onToggle,
onToggleAll,
renderAction,
}: {
tone: Tone
records: RecordView[]
selected?: Set<string>
onToggle?: (key: string) => void
onToggleAll?: (checked: boolean) => void
renderAction?: (record: RecordView) => ReactNode
}) {
const meta = TONE_META[tone]
const Icon = meta.icon
// Read-only (NS/SOA) records are never selectable — only update/delete
// sections receive selection props from DiffView.
const selectable = tone !== "readonly" && !!selected && !!onToggle && !!onToggleAll
// Read-only (NS/SOA) and custom records are never selectable — only
// update/delete sections receive selection props from DiffView.
const selectable =
tone !== "readonly" && tone !== "custom" && !!selected && !!onToggle && !!onToggleAll
const allSelected = selectable && records.length > 0 && records.every((r) => selected!.has(r.key))
const someSelected = selectable && records.some((r) => selected!.has(r.key))
const indeterminate = someSelected && !allSelected
@@ -191,6 +206,7 @@ function Section({
tone={tone}
checked={selectable ? selected!.has(record.key) : undefined}
onToggle={selectable ? onToggle : undefined}
action={renderAction?.(record)}
/>
))}
</div>
@@ -207,6 +223,8 @@ export function DiffView({
onTogglePrune,
onToggleAllUpdates,
onToggleAllPrunes,
onMarkCustom,
onUnmarkCustom,
footerExtra,
}: {
changeset: ChangesetResponse
@@ -216,6 +234,8 @@ export function DiffView({
onTogglePrune: (key: string) => void
onToggleAllUpdates: (checked: boolean) => void
onToggleAllPrunes: (checked: boolean) => void
onMarkCustom?: (key: string) => void
onUnmarkCustom?: (key: string) => void
footerExtra?: ReactNode
}) {
// Defensive: a field may arrive as null (e.g. a nil slice from an older
@@ -235,6 +255,36 @@ export function DiffView({
selected={selectedPrunes}
onToggle={onTogglePrune}
onToggleAll={onToggleAllPrunes}
renderAction={
onMarkCustom &&
((record) => (
<Button
variant="ghost"
size="icon-sm"
aria-label={`В customs ${record.type} ${record.name}`}
onClick={() => onMarkCustom(record.key)}
>
<BookmarkCheck className="size-3.5" strokeWidth={1.75} />
</Button>
))
}
/>
<Section
tone="custom"
records={changeset.customs ?? []}
renderAction={
onUnmarkCustom &&
((record) => (
<Button
variant="ghost"
size="icon-sm"
aria-label={`Вернуть в дифф ${record.type} ${record.name}`}
onClick={() => onUnmarkCustom(record.key)}
>
<Undo2 className="size-3.5" strokeWidth={1.75} />
</Button>
))
}
/>
<Section tone="readonly" records={changeset.readOnly ?? []} />