@eifi1/ui-kit 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +9 -6
  2. package/dist/components/data-table.d.ts +16 -0
  3. package/dist/components/data-table.js +17 -0
  4. package/dist/components/data-table.js.map +1 -1
  5. package/dist/components/quantity-field.d.ts +61 -0
  6. package/dist/components/quantity-field.js +95 -0
  7. package/dist/components/quantity-field.js.map +1 -0
  8. package/dist/components/quantity-input.d.ts +139 -0
  9. package/dist/components/quantity-input.js +188 -0
  10. package/dist/components/quantity-input.js.map +1 -0
  11. package/dist/components/quantity-labels.d.ts +2 -0
  12. package/dist/components/quantity-labels.js +11 -0
  13. package/dist/components/quantity-labels.js.map +1 -0
  14. package/dist/eslint/index.js +1 -1
  15. package/dist/feedback/feedback-inbox-bulk.d.ts +88 -0
  16. package/dist/feedback/feedback-inbox-bulk.js +316 -0
  17. package/dist/feedback/feedback-inbox-bulk.js.map +1 -0
  18. package/dist/feedback/feedback-inbox-page.d.ts +47 -3
  19. package/dist/feedback/feedback-inbox-page.js +19 -4
  20. package/dist/feedback/feedback-inbox-page.js.map +1 -1
  21. package/dist/feedback/feedback-row-detail.d.ts +5 -1
  22. package/dist/feedback/feedback-row-detail.js.map +1 -1
  23. package/dist/feedback/feedback-table-labels.js +10 -1
  24. package/dist/feedback/feedback-table-labels.js.map +1 -1
  25. package/dist/feedback/feedback-table.d.ts +26 -0
  26. package/dist/feedback/feedback-table.js.map +1 -1
  27. package/dist/feedback.d.ts +1 -1
  28. package/dist/feedback.js.map +1 -1
  29. package/dist/i18n/defaults.js +3 -1
  30. package/dist/i18n/defaults.js.map +1 -1
  31. package/dist/i18n/german.js +20 -1
  32. package/dist/i18n/german.js.map +1 -1
  33. package/dist/i18n/kit-labels.d.ts +4 -0
  34. package/dist/i18n/kit-labels.js.map +1 -1
  35. package/dist/i18n/locales/en.js +26 -3
  36. package/dist/i18n/locales/en.js.map +3 -3
  37. package/dist/i18n/locales/es.js +20 -1
  38. package/dist/i18n/locales/es.js.map +1 -1
  39. package/dist/i18n/locales/fr.js +25 -1
  40. package/dist/i18n/locales/fr.js.map +1 -1
  41. package/dist/i18n/locales/hu.js +22 -1
  42. package/dist/i18n/locales/hu.js.map +1 -1
  43. package/dist/i18n/locales/it.js +21 -1
  44. package/dist/i18n/locales/it.js.map +1 -1
  45. package/dist/i18n/locales/zh.js +20 -1
  46. package/dist/i18n/locales/zh.js.map +1 -1
  47. package/dist/i18n/review.js +12 -1
  48. package/dist/i18n/review.js.map +1 -1
  49. package/dist/index.d.ts +6 -0
  50. package/dist/index.js +23 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/quantity.d.ts +107 -0
  53. package/dist/lib/quantity.js +166 -0
  54. package/dist/lib/quantity.js.map +1 -0
  55. package/dist/rhf/fields.d.ts +28 -0
  56. package/dist/rhf/fields.js +82 -0
  57. package/dist/rhf/fields.js.map +1 -1
  58. package/dist/rhf.js.map +1 -1
  59. package/dist/theme/contrast.d.ts +1 -1
  60. package/dist/theme/palette-store.d.ts +1 -1
  61. package/dist/theme/text-size.d.ts +1 -1
  62. package/dist/theme/theme-store.d.ts +1 -1
  63. package/package.json +2 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-inbox-page.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useLocation } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { DataTable, type DataTableColumn, type DataTableProps } from \"../components/data-table\";\nimport { FloatingActionGroup } from \"../components/floating-panel\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { PageHeader } from \"../components/page-header\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useSearchParamState } from \"../hooks/use-search-param-state\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport type { PathPattern } from \"../landing/routing\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { captureAppScreenshot } from \"./feedback-capture\";\nimport { useFeedbackStatusLabels } from \"./feedback-labels\";\nimport { runMutation } from \"./feedback-mutation\";\nimport type { FeedbackRecord } from \"./feedback-record\";\nimport {\n FeedbackRowDetail,\n type FeedbackDetailPatch,\n type FeedbackRowDetailProps,\n type FeedbackUpload,\n} from \"./feedback-row-detail\";\nimport { useFeedbackStatusUndo, type FeedbackStatusChange } from \"./feedback-status-undo\";\nimport {\n DEFAULT_FEEDBACK_SWIPE,\n feedbackSwipeOmitShown,\n feedbackSwipePlan,\n type FeedbackSwipeBinding,\n} from \"./feedback-swipe\";\nimport {\n FeedbackAwaitingToggle,\n FeedbackEmptyState,\n FeedbackMobileCard,\n feedbackMobileGroupBy,\n isFeedbackAwaiting,\n useFeedbackColumns,\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport type { FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The feedback inbox, ASSEMBLED ONCE (0.55; kastlan reuse audit 2026-11-02, proposal 2;\n * docs/feedback-harmonization.md §13).\n *\n * Since 0.27 every part of `/feedback` and `/my-feedback` has been the kit's — the columns,\n * the phone card and its day headers, the detail, the Undo, the swipes, the awaiting toggle\n * (feedback-table.tsx, feedback-row-detail.tsx, feedback-swipe.tsx). What was not the kit's\n * was the WIRING between them, and each app wrote the same ~150 lines of it:\n * kastlan `frontend/src/features/feedback/pages/feedback-page.tsx:40-205`, keksdose\n * `frontend/src/features/feedback/feedback-page.tsx:51-303`, Kurvenschmiede\n * `frontend/src/features/feedback/feedback-page.tsx:88-245`. The open row in `?row=` with\n * its numeric parse and its `rowHref`, `useFeedbackStatusUndo` over the page's PATCH, the\n * `DataTable` with a dozen props, `FeedbackRowDetail` with a dozen more, the phone's\n * grouping, card and swipe plan, the awaiting filter and its toggle. So every rename the\n * parts took (0.52's `mutateAsync`, 0.53's removals) was made three times, and three\n * times reviewed.\n *\n * Here that wiring is the kit's: {@link useFeedbackInbox} holds it and returns what the\n * table, the detail and the phone cards need; {@link FeedbackInbox} draws the page from\n * it. What stays the app's is what differs between the three, each a slot: where the rows\n * come from and the three mutations (its API client, TanStack Query in all three — the kit\n * sends no request), who may triage, the votes, the public switch, the comments (the app's\n * `FeedbackCommentThread` over its own queries), its own columns, its swipe binding, its\n * date writer, the page's header and frame, and whatever else `DataTable` takes.\n *\n * Additive: every part it is built from stays exported and unchanged, for a page that\n * wants its own layout — call the hook and spread `inbox.table` into a `DataTable`.\n */\n\n/* ── The mutations ──────────────────────────────────────────────────────────── */\n\n/**\n * One of the page's mutations, as the inbox calls it: **TanStack Query's `useMutation`\n * result, passed as it is** — `update={update}`. The inbox reads `mutateAsync` (whose\n * promise settles for every call, refactor audit 2026-10-30 FLW-3) and `isPending`, and\n * nothing else. Any object with those two fits, so a page on another data layer hands\n * `{ mutateAsync: api.update, isPending }`.\n *\n * Taking the mutation rather than a callback per control is the point of the round: how\n * the parts call it (0.52 moved them from `mutate` to `mutateAsync`) is then the kit's to\n * change, once. Failure is the mutation's own: its `onError` toasts the server's sentence\n * (§4.4), and the inbox catches the rejection.\n */\nexport interface FeedbackInboxMutation<V> {\n mutateAsync: (variables: V) => Promise<unknown>;\n /** In flight: the editors' and the composer's buttons wait for it. */\n isPending?: boolean;\n}\n\n/** What the reply mutation is sent: `POST /feedback/{id}/replies` (§9.7, §12) — the\n * body, the status picked (absent: \"No change\") and the uploaded file's URL (absent:\n * none). Keys that carry nothing are left out, so a `mutationFn` may spread them onto\n * the wire. kastlan's `useFeedbackReply` takes exactly this. */\nexport interface FeedbackReplyPost {\n id: number;\n body: string;\n status?: FeedbackStatus;\n attachmentUrl?: string;\n}\n\n/** What the rework mutation is sent: `POST /feedback/{id}/rework` (§10.3) — the author's\n * trimmed note and the uploaded file's URL (absent: none). kastlan's `useFeedbackRework`\n * takes exactly this. */\nexport interface FeedbackReworkPost {\n id: number;\n note: string;\n attachmentUrl?: string;\n}\n\n/* ── The hook ───────────────────────────────────────────────────────────────── */\n\nexport interface UseFeedbackInboxOptions<T extends FeedbackRecord> {\n /** The list as the app's query answered it (`GET /feedback`, `GET /feedback/my`) —\n * `list.data ?? []`. Shown newest first and, behind the awaiting toggle, narrowed. */\n rows: readonly T[];\n /** `/my-feedback`: the viewer's own reports — no triage there, for an admin too (§4.3). */\n mine?: boolean;\n /** The viewer may triage: the admin. Only on the inbox — `mine` wins — so `isAdmin`\n * is enough. The status cell, the pills, the reply composer, the swipes, the submitter\n * on the card and the awaiting toggle. */\n canEdit: boolean;\n /** The signed-in user's id: who the AUTHOR is (the description editor, Rework, the\n * public switch's window). */\n viewerId?: number | null;\n /**\n * `PATCH /feedback/{id}` (§3.4): every edit of the report itself — a status change from\n * the cell, the pills or a swipe (with the Undo, `useFeedbackStatusUndo`), the\n * description, the legacy rework append and, with {@link publicSwitch}, the flip. ONE\n * mutation for all of them, the page's, so its observer outlives a row a status filter\n * removes (§4.5).\n */\n update: FeedbackInboxMutation<FeedbackDetailPatch>;\n /** `POST /feedback/{id}/replies` — the admin's answer (§9.7) with its one file (§12).\n * Without it no composer is drawn. */\n reply?: FeedbackInboxMutation<FeedbackReplyPost>;\n /** `POST /feedback/{id}/rework` — the author's rework entry (§10.3). Without it a rework\n * goes through {@link update} as the legacy body append (§3.4). */\n rework?: FeedbackInboxMutation<FeedbackReworkPost>;\n /**\n * The **Visible to others** switch in the detail (§9.3), its flip sent through\n * {@link update} as `{ id, public }` — as keksdose and Kurvenschmiede send it. Off by\n * default: an app without public parts (kastlan) has no switch.\n */\n publicSwitch?: boolean;\n /** The **Votes** column and the card's count (§9.6). Off by default (kastlan); keksdose\n * always, Kurvenschmiede for a reader of the public parts. */\n votes?: boolean;\n /**\n * The report's comments, in a section of their own under the replies (§9.8): the app's\n * `FeedbackCommentThread` over its own queries — a component of its own, since its\n * queries are hooks. Return `null` for a row without a thread (a CRASH, a reader who\n * does not see the public parts; `false` is read as `null`). Called only for the open\n * row.\n *\n * ```tsx\n * comments={(row) => <RowComments id={row.id} isAdmin={isAdmin} canComment={!canEdit} />}\n * ```\n */\n comments?: (row: T) => ReactNode;\n /** The detail as drawn, to wrap or extend — kastlan's tour anchor:\n * `(row, detail) => <div data-tour=\"feedback-row-detail\">{detail}</div>`. */\n renderDetail?: (row: T, detail: ReactNode) => ReactNode;\n /**\n * The app's own columns over the kit's (§4.3's nine, and Votes): add, drop, reorder —\n * the keys are stable (`columns.filter((c) => c.key !== \"email\")`). A column brings its\n * filter with it. Keep it stable (module scope or `useCallback`): it is a memo's\n * dependency.\n */\n columns?: (columns: readonly DataTableColumn<T>[]) => readonly DataTableColumn<T>[];\n /** Which swipe does what on the phone (§4.5) — keksdose's Settings → Interaction binding,\n * `{ end: prefs.right, start: prefs.left }`. Default `DEFAULT_FEEDBACK_SWIPE`. */\n swipe?: FeedbackSwipeBinding;\n /** The status writes the TABLE offers — the status cell and the desktop's actions\n * column — commit: under a locked `WriteLockProvider` they stay and say why\n * (`FeedbackStatusTransitions`' `commit`). Off by default. The detail's own saves:\n * `detail={{ commit }}`. */\n commit?: CommitScope;\n /** `POST /feedback/attachments` (§3.3): the rework's file and the reply's. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET for the pictures and the downloads (§3.5). */\n fetcher?: AuthedFetcher | null;\n /** \"Capture screenshot\" beside the rework's and the reply's file field. Default: the\n * kit's `captureAppScreenshot()`; `null` for no button. */\n onCaptureScreenshot?: (() => Promise<File | null>) | null;\n /**\n * The app's date writer (keksdose's `useDateCell()`, the date-format setting): the date\n * and resolved columns, the card, the phone's day headers AND the detail's \"Resolved:\",\n * so all five say the same. Left out, the kit's date with the weekday in the table and\n * on the phone, and in the detail its sentence date — as the parts draw them alone.\n */\n renderDate?: FeedbackRenderDate;\n /** The app's one-shot routes, on top of the kit's (0.52, FLW-7) — for the URL column and\n * the detail's URL section alike. Keep it stable (module scope). */\n pageLinkExclude?: readonly PathPattern[];\n /** The list is still loading (`list.isPending`): the table says \"Loading…\" where its\n * rows will be, not \"None\". */\n loading?: boolean;\n /** Over the detail's props the inbox sets — `commit`, `showSubject`,\n * `reworkAttachment`, `replyAttachments` (on here), `labels`, `className`. Merged last,\n * so it wins. */\n detail?: Partial<Omit<FeedbackRowDetailProps, \"row\">>;\n /** Over the `feedbackPage` namespace: the columns, the card, the empty state, the toggle\n * and the title. Keep it stable: it is a memo's dependency. */\n labels?: LabelOverride<FeedbackPageLabels>;\n}\n\n/** What {@link useFeedbackInbox} returns: the state, and the props of the parts. */\nexport interface FeedbackInboxState<T extends FeedbackRecord> {\n /** `canEdit && !mine`: the admin on the inbox. */\n editable: boolean;\n /** The page's title, `feedbackPage.title` — or `.myTitle` on `/my-feedback`. */\n title: string;\n /** The open row's id, from `?row=`; `null` when none is open or the param named no\n * id (`?row=abc`, `?row=1.5`, `?row=0`). */\n openId: number | null;\n /** Open a row by its id, or close it with `null`: rewrites `?row=`, replacing the\n * history entry rather than stacking one per report looked at. */\n setOpenId: (id: number | null) => void;\n /** A row's own address: this view — the table's page, sort and filters — with the row\n * open. The row's link (a new tab, a copied address); a plain click opens it in place. */\n rowHref: (row: T) => string;\n /** The undoable status change (§4.5): the status cell's, the pills', the swipes'. */\n changeStatus: FeedbackStatusChange;\n /** The phone's \"Only what is waiting for you\" — page state, not the URL (§4.3). */\n awaiting: boolean;\n setAwaiting: (next: boolean) => void;\n /** The one date writer the columns, the card and the day headers use. */\n renderDate: FeedbackRenderDate;\n /** `FeedbackRowDetail`'s props for a row, slots included. */\n detailProps: (row: T) => FeedbackRowDetailProps;\n /** `DataTable`'s props, ready to spread: the rows (newest first, narrowed by\n * `awaiting`), the columns, the open row, the detail, the phone's card, day headers and\n * swipes, the desktop's actions column, the empty state, `urlSync`,\n * `storageKey=\"feedback\"`, `fillHeight`. */\n table: DataTableProps<T>;\n}\n\n/** `?row=`'s value: a report's id is a positive integer, written in digits. Anything\n * else — a hand-edited link, `1e2`, `0x10`, `-3`, `1.5` — opens no row (and the URL\n * keeps it until a row is opened or closed). The three apps parsed with `Number()`, so\n * `?row=1e2` opened #100 in all three. */\nfunction parseRowId(raw: string): number | undefined {\n if (!/^[1-9]\\d*$/.test(raw)) return undefined;\n const id = Number(raw);\n return Number.isSafeInteger(id) ? id : undefined;\n}\n\nconst ROW_PARAM = { parse: parseRowId, history: \"replace\" } as const;\n\n/** Newest first, as `GET /feedback` sends it (§3.3) — the phone's day headers group runs\n * of equal days, so an older row between two of today's would split the day in two.\n * ISO strings from one server sort as their instants; the sort is stable. */\nconst newestFirst = (a: FeedbackRecord, b: FeedbackRecord) =>\n a.created_at < b.created_at ? 1 : a.created_at > b.created_at ? -1 : 0;\n\nconst rowId = (row: FeedbackRecord) => row.id;\nconst rowTitle = (row: FeedbackRecord) => row.title;\nconst captureScreenshot = () => captureAppScreenshot();\nconst ignore = () => {};\n/** A slot's \"nothing\": `cond && <Thread />` answers `false`, which the detail would draw\n * as an empty Comments section (it draws one for anything but `null`). */\nconst threadOf = (node: ReactNode): ReactNode => (node === false || node === \"\" ? undefined : node);\n\n/**\n * The inbox's wiring, once (§13): from the app's rows and mutations to what the table,\n * the detail and the phone cards take.\n *\n * ```tsx\n * const list = useQuery({ queryKey: keys.feedback.all, queryFn: api.listFeedback });\n * const update = useMutation({ mutationFn: ({ id, ...patch }: FeedbackDetailPatch) => api.update(id, patch), … });\n * const inbox = useFeedbackInbox({ rows: list.data ?? [], canEdit: isAdmin, viewerId: user?.id, update });\n * <DataTable {...inbox.table} edgeFade />\n * ```\n *\n * {@link FeedbackInbox} is this hook drawn — use the hook alone for a layout of your own.\n * Needs a react-router Router (`?row=`, `urlSync`).\n */\nexport function useFeedbackInbox<T extends FeedbackRecord>({\n rows,\n mine = false,\n canEdit,\n viewerId,\n update,\n reply,\n rework,\n publicSwitch = false,\n votes = false,\n comments,\n renderDetail,\n columns: ownColumns,\n swipe = DEFAULT_FEEDBACK_SWIPE,\n commit,\n onUpload,\n fetcher,\n onCaptureScreenshot = captureScreenshot,\n renderDate: renderDateProp,\n pageLinkExclude,\n loading = false,\n detail,\n labels,\n}: UseFeedbackInboxOptions<T>): FeedbackInboxState<T> {\n // An admin on /my-feedback reads as any author does (§4.3), as `feedbackColumns` has it.\n const editable = canEdit && !mine;\n const pageLabels = useFeedbackPageLabels(labels);\n const statusLabels = useFeedbackStatusLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n\n // TanStack's `mutateAsync` is stable, so the Undo and the columns' memo hold across\n // renders; the mutation object around it is not.\n const updateAsync = update.mutateAsync;\n const changeStatus = useFeedbackStatusUndo(updateAsync);\n\n // The open row IS the URL (§4.3: `?row=<id>`, every app) — which is what makes a row a\n // real link at all (keksdose feedback #451). Replace, not push: looking through ten\n // reports is not ten steps for Back.\n const [openId, setOpenId] = useSearchParamState<number | null>(\"row\", null, ROW_PARAM);\n const { pathname, search } = useLocation();\n // Through the router, so a basename (and a hash router's `#`) is in the address.\n const here = useHref({ pathname });\n // Built from the CURRENT query, not a bare `?row=`: the table keeps its `f.*`, `sort`\n // and `p` in the URL, and dropping them would open the new tab on an unfiltered list\n // where the row may not be on the first page (keksdose feedback-page.tsx:145).\n const rowHref = useCallback(\n (row: T) => {\n const params = new URLSearchParams(search);\n params.set(\"row\", String(row.id));\n return `${here}?${params.toString()}`;\n },\n [here, search],\n );\n\n // A thumb gesture on one screen, not a view worth linking to (§4.3; keksdose live #330).\n const [awaiting, setAwaiting] = useState(false);\n const shown = useMemo(\n () => (editable && awaiting ? rows.filter((row) => isFeedbackAwaiting(row.status)) : [...rows]).sort(newestFirst),\n [rows, editable, awaiting],\n );\n\n const kitColumns = useFeedbackColumns<T>({\n mine,\n canEdit,\n onStatus: changeStatus,\n renderDate,\n commit,\n votes,\n pageLinkExclude,\n labels,\n });\n const columns = useMemo(() => (ownColumns ? ownColumns(kitColumns) : kitColumns), [ownColumns, kitColumns]);\n const mobileGroupBy = useMemo(() => feedbackMobileGroupBy(renderDate), [renderDate]);\n\n // The rework's sending is the detail's too (keksdose, Kurvenschmiede): its editors and\n // the public switch wait for whichever write is out.\n const pending = Boolean(update.isPending || rework?.isPending);\n const capture = onCaptureScreenshot ?? undefined;\n\n const detailProps = (row: T): FeedbackRowDetailProps => ({\n row,\n canEdit: editable,\n viewerId,\n onUpdate: updateAsync,\n pending,\n // The page's change, not one the detail would build: the same Undo as the cell's.\n statusChange: changeStatus,\n onUpload,\n fetcher,\n onCaptureScreenshot: capture,\n // Only the app's own: left out, the detail's \"Resolved:\" is the kit's sentence date,\n // no weekday — as kastlan's has always been.\n renderDate: renderDateProp,\n pageLinkExclude,\n onReply: reply\n ? (body, status, attachmentUrl) =>\n reply.mutateAsync({\n id: row.id,\n body,\n ...(status ? { status } : {}),\n ...(attachmentUrl ? { attachmentUrl } : {}),\n })\n : undefined,\n // The reply's one file (§12): all three apps are on server-kit 0.18, and the reply\n // mutation's `attachmentUrl` is in its type from the start (the B1 risk the detail's\n // default guards against — an `onReply` that drops the file — cannot arise here).\n replyAttachments: true,\n replying: reply?.isPending ?? false,\n onRework: rework\n ? (note, attachmentUrl) => rework.mutateAsync({ id: row.id, note, ...(attachmentUrl ? { attachmentUrl } : {}) })\n : undefined,\n comments: threadOf(comments?.(row)),\n onPublicChange: publicSwitch ? (next) => runMutation(updateAsync, { id: row.id, public: next }, ignore) : undefined,\n ...detail,\n });\n\n const table: DataTableProps<T> = {\n rows: shown,\n columns,\n rowKey: rowId,\n storageKey: \"feedback\",\n urlSync: true,\n fillHeight: true,\n rowHref,\n // The link covers the row's first cell; a click anywhere on the row opens it — or,\n // on the open one, closes it.\n onRowClick: (row) => setOpenId((id) => (id === row.id ? null : row.id)),\n isExpanded: (row) => row.id === openId,\n expandedRow: (row) => {\n const node = <FeedbackRowDetail {...detailProps(row)} />;\n return renderDetail ? renderDetail(row, node) : node;\n },\n // §7.13: one word, also when a filter or the toggle leaves nothing; and while the\n // first answer is out, what is happening instead.\n empty: loading ? <LoadingState size=\"sm\" inline /> : <FeedbackEmptyState labels={labels} />,\n mobileGroupBy,\n mobileCard: (row) => (\n <FeedbackMobileCard row={row} showSubmitter={editable} renderDate={renderDate} votes={votes} labels={labels} />\n ),\n // The admin inbox only — /my-feedback is read-only by design (keksdose #331) — and\n // every swipe through the same Undo as a tap (§4.5, §7.10).\n mobileSwipeActions: editable\n ? (row) => feedbackSwipePlan(swipe, row, { change: changeStatus, labels: statusLabels })\n : undefined,\n // The swipes as the desktop's last column (0.46, keksdose dev#598), less what the\n // status cell already offers.\n desktopSwipeActions: { omit: feedbackSwipeOmitShown, name: rowTitle, ...(commit === undefined ? {} : { commit }) },\n };\n\n return {\n editable,\n title: mine ? pageLabels.myTitle : pageLabels.title,\n openId,\n setOpenId,\n rowHref,\n changeStatus,\n awaiting,\n setAwaiting,\n renderDate,\n detailProps,\n table,\n };\n}\n\n/* ── The component ──────────────────────────────────────────────────────────── */\n\nexport interface FeedbackInboxProps<T extends FeedbackRecord> extends UseFeedbackInboxOptions<T> {\n /**\n * Above the table. Default: `<PageHeader title={title} className=\"mb-4 md:shrink-0\" />`.\n * A function gets the page's title (`feedbackPage.title` / `.myTitle`) — the app's own\n * header: `(title) => <PageHeader size=\"compact\" title={title} data-tour=\"feedback-header\" />`.\n * `null` for none.\n */\n header?: ReactNode | ((title: string) => ReactNode);\n /** Over the inbox's `DataTable` props — `edgeFade`, `locale`, `mobileExpandAsDialog`,\n * `className`, another `storageKey`. Merged last, so it wins. */\n table?: Partial<DataTableProps<T>>;\n /** Further `FloatingAction`s on the phone: after the awaiting toggle on the inbox, in a\n * group of their own on `/my-feedback`. */\n phoneActions?: ReactNode;\n /** On the page's frame — the fill-height column (`FILL_HEIGHT_CLASS`); the app's page\n * padding goes here. */\n className?: string;\n}\n\n/**\n * The feedback inbox (§13): `/feedback` and `/my-feedback` drawn whole from\n * {@link useFeedbackInbox} — the header, the table with its detail, phone cards and swipes,\n * and the phone's awaiting toggle, in the fill-height frame (page layout §1.3) — so a page\n * is its queries and this:\n *\n * ```tsx\n * export function FeedbackPage({ mine = false }: { mine?: boolean }) {\n * const { user, isAdmin } = useAuth();\n * const list = useQuery({ queryKey: mine ? keys.mine : keys.all, queryFn: mine ? api.mine : api.all });\n * const update = useFeedbackUpdate();\n * const reply = useFeedbackReply();\n * const rework = useFeedbackRework();\n * if (!mine && !isAdmin) return <Navigate to=\"/my-feedback\" replace />;\n * return (\n * <FeedbackInbox\n * rows={list.data ?? []} loading={list.isPending} mine={mine} canEdit={isAdmin} viewerId={user?.id}\n * update={update} reply={reply} rework={rework}\n * onUpload={api.upload} fetcher={api.file}\n * />\n * );\n * }\n * ```\n *\n * Every slot is {@link UseFeedbackInboxOptions}' or one of the four below. The redirect of\n * a non-admin, the routes and the queries stay the app's.\n */\nexport function FeedbackInbox<T extends FeedbackRecord>({\n header,\n table,\n phoneActions,\n className,\n ...options\n}: FeedbackInboxProps<T>) {\n const inbox = useFeedbackInbox<T>(options);\n const pageLabels = useFeedbackPageLabels(options.labels);\n const head =\n header === undefined ? (\n <PageHeader title={inbox.title} className=\"mb-4 md:shrink-0\" />\n ) : typeof header === \"function\" ? (\n header(inbox.title)\n ) : (\n header\n );\n return (\n // A plain element on the way to the table carries the fill (page layout §1.3).\n <div className={cn(FILL_HEIGHT_CLASS, className)} aria-busy={options.loading || undefined} data-feedback-inbox=\"\">\n {head}\n <DataTable {...inbox.table} {...table} />\n {inbox.editable ? (\n // The phone's floating action (keksdose feedback #58): only on the inbox — on\n // /my-feedback the author is not waiting on their own verdict.\n <FeedbackAwaitingToggle pressed={inbox.awaiting} onPressedChange={inbox.setAwaiting} labels={options.labels}>\n {phoneActions}\n </FeedbackAwaitingToggle>\n ) : (\n phoneActions && (\n <FloatingActionGroup aria-label={pageLabels.phoneActions} className=\"md:hidden\">\n {phoneActions}\n </FloatingActionGroup>\n )\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-inbox-page.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useLocation } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { DataTable, type DataTableColumn, type DataTableProps } from \"../components/data-table\";\nimport { FloatingActionGroup } from \"../components/floating-panel\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { PageHeader } from \"../components/page-header\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useSearchParamState } from \"../hooks/use-search-param-state\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport type { PathPattern } from \"../landing/routing\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { captureAppScreenshot } from \"./feedback-capture\";\nimport { FeedbackInboxBulkBar, useFeedbackInboxBulk } from \"./feedback-inbox-bulk\";\nimport { useFeedbackStatusLabels } from \"./feedback-labels\";\nimport { runMutation } from \"./feedback-mutation\";\nimport type { FeedbackRecord } from \"./feedback-record\";\nimport {\n FeedbackRowDetail,\n type FeedbackDetailPatch,\n type FeedbackRowDetailProps,\n type FeedbackUpload,\n} from \"./feedback-row-detail\";\nimport { useFeedbackStatusUndo, type FeedbackStatusChange } from \"./feedback-status-undo\";\nimport {\n DEFAULT_FEEDBACK_SWIPE,\n feedbackSwipeOmitShown,\n feedbackSwipePlan,\n type FeedbackSwipeBinding,\n} from \"./feedback-swipe\";\nimport {\n FeedbackAwaitingToggle,\n FeedbackEmptyState,\n FeedbackMobileCard,\n feedbackMobileGroupBy,\n isFeedbackAwaiting,\n useFeedbackColumns,\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport type { FeedbackCategory, FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The feedback inbox, ASSEMBLED ONCE (0.55; kastlan reuse audit 2026-11-02, proposal 2;\n * docs/feedback-harmonization.md §13).\n *\n * Since 0.27 every part of `/feedback` and `/my-feedback` has been the kit's — the columns,\n * the phone card and its day headers, the detail, the Undo, the swipes, the awaiting toggle\n * (feedback-table.tsx, feedback-row-detail.tsx, feedback-swipe.tsx). What was not the kit's\n * was the WIRING between them, and each app wrote the same ~150 lines of it:\n * kastlan `frontend/src/features/feedback/pages/feedback-page.tsx:40-205`, keksdose\n * `frontend/src/features/feedback/feedback-page.tsx:51-303`, Kurvenschmiede\n * `frontend/src/features/feedback/feedback-page.tsx:88-245`. The open row in `?row=` with\n * its numeric parse and its `rowHref`, `useFeedbackStatusUndo` over the page's PATCH, the\n * `DataTable` with a dozen props, `FeedbackRowDetail` with a dozen more, the phone's\n * grouping, card and swipe plan, the awaiting filter and its toggle. So every rename the\n * parts took (0.52's `mutateAsync`, 0.53's removals) was made three times, and three\n * times reviewed.\n *\n * Here that wiring is the kit's: {@link useFeedbackInbox} holds it and returns what the\n * table, the detail and the phone cards need; {@link FeedbackInbox} draws the page from\n * it. What stays the app's is what differs between the three, each a slot: where the rows\n * come from and the three mutations (its API client, TanStack Query in all three — the kit\n * sends no request), who may triage, the votes, the public switch, the comments (the app's\n * `FeedbackCommentThread` over its own queries), its own columns, its swipe binding, its\n * date writer, the page's header and frame, and whatever else `DataTable` takes.\n *\n * Additive: every part it is built from stays exported and unchanged, for a page that\n * wants its own layout — call the hook and spread `inbox.table` into a `DataTable`.\n *\n * 0.57 (keksdose dev #663): the admin's multi-select and bulk edit — the table's boxes (on\n * the phone, the long press), and a bulk bar that sets the selected reports' status,\n * category or visibility through the same `update`, one PATCH per report, with one Undo\n * (feedback-inbox-bulk.tsx, §13.6).\n */\n\n/* ── The mutations ──────────────────────────────────────────────────────────── */\n\n/**\n * One of the page's mutations, as the inbox calls it: **TanStack Query's `useMutation`\n * result, passed as it is** — `update={update}`. The inbox reads `mutateAsync` (whose\n * promise settles for every call, refactor audit 2026-10-30 FLW-3) and `isPending`, and\n * nothing else. Any object with those two fits, so a page on another data layer hands\n * `{ mutateAsync: api.update, isPending }`.\n *\n * Taking the mutation rather than a callback per control is the point of the round: how\n * the parts call it (0.52 moved them from `mutate` to `mutateAsync`) is then the kit's to\n * change, once. Failure is the mutation's own: its `onError` toasts the server's sentence\n * (§4.4), and the inbox catches the rejection.\n */\nexport interface FeedbackInboxMutation<V> {\n mutateAsync: (variables: V) => Promise<unknown>;\n /** In flight: the editors' and the composer's buttons wait for it. */\n isPending?: boolean;\n}\n\n/** What the reply mutation is sent: `POST /feedback/{id}/replies` (§9.7, §12) — the\n * body, the status picked (absent: \"No change\") and the uploaded file's URL (absent:\n * none). Keys that carry nothing are left out, so a `mutationFn` may spread them onto\n * the wire. kastlan's `useFeedbackReply` takes exactly this. */\nexport interface FeedbackReplyPost {\n id: number;\n body: string;\n status?: FeedbackStatus;\n attachmentUrl?: string;\n}\n\n/** What the rework mutation is sent: `POST /feedback/{id}/rework` (§10.3) — the author's\n * trimmed note and the uploaded file's URL (absent: none). kastlan's `useFeedbackRework`\n * takes exactly this. */\nexport interface FeedbackReworkPost {\n id: number;\n note: string;\n attachmentUrl?: string;\n}\n\n/* ── The hook ───────────────────────────────────────────────────────────────── */\n\nexport interface UseFeedbackInboxOptions<T extends FeedbackRecord> {\n /** The list as the app's query answered it (`GET /feedback`, `GET /feedback/my`) —\n * `list.data ?? []`. Shown newest first and, behind the awaiting toggle, narrowed. */\n rows: readonly T[];\n /** `/my-feedback`: the viewer's own reports — no triage there, for an admin too (§4.3). */\n mine?: boolean;\n /** The viewer may triage: the admin. Only on the inbox — `mine` wins — so `isAdmin`\n * is enough. The status cell, the pills, the reply composer, the swipes, the submitter\n * on the card and the awaiting toggle. */\n canEdit: boolean;\n /** The signed-in user's id: who the AUTHOR is (the description editor, Rework, the\n * public switch's window). */\n viewerId?: number | null;\n /**\n * `PATCH /feedback/{id}` (§3.4): every edit of the report itself — a status change from\n * the cell, the pills or a swipe (with the Undo, `useFeedbackStatusUndo`), the\n * description, the legacy rework append and, with {@link publicSwitch}, the flip. ONE\n * mutation for all of them, the page's, so its observer outlives a row a status filter\n * removes (§4.5).\n */\n update: FeedbackInboxMutation<FeedbackDetailPatch>;\n /** `POST /feedback/{id}/replies` — the admin's answer (§9.7) with its one file (§12).\n * Without it no composer is drawn. */\n reply?: FeedbackInboxMutation<FeedbackReplyPost>;\n /** `POST /feedback/{id}/rework` — the author's rework entry (§10.3). Without it a rework\n * goes through {@link update} as the legacy body append (§3.4). */\n rework?: FeedbackInboxMutation<FeedbackReworkPost>;\n /**\n * The **Visible to others** switch in the detail (§9.3), its flip sent through\n * {@link update} as `{ id, public }` — as keksdose and Kurvenschmiede send it. Off by\n * default: an app without public parts (kastlan) has no switch.\n */\n publicSwitch?: boolean;\n /** The **Votes** column and the card's count (§9.6). Off by default (kastlan); keksdose\n * always, Kurvenschmiede for a reader of the public parts. */\n votes?: boolean;\n /**\n * The report's comments, in a section of their own under the replies (§9.8): the app's\n * `FeedbackCommentThread` over its own queries — a component of its own, since its\n * queries are hooks. Return `null` for a row without a thread (a CRASH, a reader who\n * does not see the public parts; `false` is read as `null`). Called only for the open\n * row.\n *\n * ```tsx\n * comments={(row) => <RowComments id={row.id} isAdmin={isAdmin} canComment={!canEdit} />}\n * ```\n */\n comments?: (row: T) => ReactNode;\n /** The detail as drawn, to wrap or extend — kastlan's tour anchor:\n * `(row, detail) => <div data-tour=\"feedback-row-detail\">{detail}</div>`. */\n renderDetail?: (row: T, detail: ReactNode) => ReactNode;\n /**\n * The app's own columns over the kit's (§4.3's nine, and Votes): add, drop, reorder —\n * the keys are stable (`columns.filter((c) => c.key !== \"email\")`). A column brings its\n * filter with it. Keep it stable (module scope or `useCallback`): it is a memo's\n * dependency.\n */\n columns?: (columns: readonly DataTableColumn<T>[]) => readonly DataTableColumn<T>[];\n /** Which swipe does what on the phone (§4.5) — keksdose's Settings → Interaction binding,\n * `{ end: prefs.right, start: prefs.left }`. Default `DEFAULT_FEEDBACK_SWIPE`. */\n swipe?: FeedbackSwipeBinding;\n /** The status writes the TABLE offers — the status cell and the desktop's actions\n * column — commit: under a locked `WriteLockProvider` they stay and say why\n * (`FeedbackStatusTransitions`' `commit`). Off by default. The detail's own saves:\n * `detail={{ commit }}`. */\n commit?: CommitScope;\n /** `POST /feedback/attachments` (§3.3): the rework's file and the reply's. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET for the pictures and the downloads (§3.5). */\n fetcher?: AuthedFetcher | null;\n /** \"Capture screenshot\" beside the rework's and the reply's file field. Default: the\n * kit's `captureAppScreenshot()`; `null` for no button. */\n onCaptureScreenshot?: (() => Promise<File | null>) | null;\n /**\n * The app's date writer (keksdose's `useDateCell()`, the date-format setting): the date\n * and resolved columns, the card, the phone's day headers AND the detail's \"Resolved:\",\n * so all five say the same. Left out, the kit's date with the weekday in the table and\n * on the phone, and in the detail its sentence date — as the parts draw them alone.\n */\n renderDate?: FeedbackRenderDate;\n /** The app's one-shot routes, on top of the kit's (0.52, FLW-7) — for the URL column and\n * the detail's URL section alike. Keep it stable (module scope). */\n pageLinkExclude?: readonly PathPattern[];\n /** The list is still loading (`list.isPending`): the table says \"Loading…\" where its\n * rows will be, not \"None\". */\n loading?: boolean;\n /** Over the detail's props the inbox sets — `commit`, `showSubject`,\n * `reworkAttachment`, `replyAttachments` (on here), `labels`, `className`. Merged last,\n * so it wins. */\n detail?: Partial<Omit<FeedbackRowDetailProps, \"row\">>;\n /** Over the `feedbackPage` namespace: the columns, the card, the empty state, the toggle\n * and the title. Keep it stable: it is a memo's dependency. */\n labels?: LabelOverride<FeedbackPageLabels>;\n /**\n * The admin's multi-select and bulk edit (0.57, keksdose dev #663; §13.6): a box per row\n * and a select-all on the desktop, the long press on the phone (swipes rest while\n * selecting), and a bulk bar that sets the selected reports' status, category or — with\n * {@link publicSwitch} — visibility through {@link update}, one PATCH per report, with\n * one Undo. **On** wherever the inbox is editable (the admin, not `/my-feedback`);\n * `false` for none. Nothing to wire: it is the page's own mutation.\n */\n selectable?: boolean;\n}\n\n/**\n * The admin's selection and its bulk changes (0.57, keksdose dev #663; §13.6) —\n * `inbox.bulk`, which {@link FeedbackInbox} draws as its bulk bar (feedback-inbox-bulk.tsx).\n * For a page that lays the parts out itself, what its own `BulkActionBar` needs. Every\n * change is {@link UseFeedbackInboxOptions.update}, once per report.\n */\nexport interface FeedbackInboxBulk<T extends FeedbackRecord> {\n /** The selected reports the admin can SEE — the table's filters and the awaiting toggle\n * applied, in the order the inbox lists them: what the bar counts and a change acts\n * on. A selected row a filter hides drops out of the selection. */\n rows: readonly T[];\n /** Empty the selection (the bar's Clear). */\n clear: () => void;\n /** Set every selected report's status: one `update` per report not there yet, one Undo\n * toast for those that changed — each back to its own status. Resolves when every\n * PATCH has answered; a report whose PATCH failed stays selected. */\n setStatus: (status: FeedbackStatus) => Promise<void>;\n /** The same for the category. Never CRASH (§3.4: a 422) — ignored. */\n setCategory: (category: FeedbackCategory) => Promise<void>;\n /** The same for **Visible to others** — with {@link UseFeedbackInboxOptions.publicSwitch}\n * only. A CRASH and a row without `public` are left as they are (§9.3). */\n setPublic?: (next: boolean) => Promise<void>;\n /** The bulk change in flight, or `null`. One at a time: a press while one is out is\n * ignored. */\n pending: \"status\" | \"category\" | \"public\" | null;\n}\n\n/** What {@link useFeedbackInbox} returns: the state, and the props of the parts. */\nexport interface FeedbackInboxState<T extends FeedbackRecord> {\n /** `canEdit && !mine`: the admin on the inbox. */\n editable: boolean;\n /** The page's title, `feedbackPage.title` — or `.myTitle` on `/my-feedback`. */\n title: string;\n /** The open row's id, from `?row=`; `null` when none is open or the param named no\n * id (`?row=abc`, `?row=1.5`, `?row=0`). */\n openId: number | null;\n /** Open a row by its id, or close it with `null`: rewrites `?row=`, replacing the\n * history entry rather than stacking one per report looked at. */\n setOpenId: (id: number | null) => void;\n /** A row's own address: this view — the table's page, sort and filters — with the row\n * open. The row's link (a new tab, a copied address); a plain click opens it in place. */\n rowHref: (row: T) => string;\n /** The undoable status change (§4.5): the status cell's, the pills', the swipes'. */\n changeStatus: FeedbackStatusChange;\n /** The phone's \"Only what is waiting for you\" — page state, not the URL (§4.3). */\n awaiting: boolean;\n setAwaiting: (next: boolean) => void;\n /** The one date writer the columns, the card and the day headers use. */\n renderDate: FeedbackRenderDate;\n /** `FeedbackRowDetail`'s props for a row, slots included. */\n detailProps: (row: T) => FeedbackRowDetailProps;\n /** `DataTable`'s props, ready to spread: the rows (newest first, narrowed by\n * `awaiting`), the columns, the open row, the detail, the phone's card, day headers and\n * swipes, the desktop's actions column, the empty state, `urlSync`,\n * `storageKey=\"feedback\"`, `fillHeight` — and, with {@link bulk}, the `selection`. */\n table: DataTableProps<T>;\n /** 0.57: the selection and the bulk changes — present where the inbox is editable and\n * `selectable` (the default). {@link FeedbackInbox} draws them as its bulk bar; a page\n * of its own draws a `BulkActionBar` from them. */\n bulk?: FeedbackInboxBulk<T>;\n}\n\n/** `?row=`'s value: a report's id is a positive integer, written in digits. Anything\n * else — a hand-edited link, `1e2`, `0x10`, `-3`, `1.5` — opens no row (and the URL\n * keeps it until a row is opened or closed). The three apps parsed with `Number()`, so\n * `?row=1e2` opened #100 in all three. */\nfunction parseRowId(raw: string): number | undefined {\n if (!/^[1-9]\\d*$/.test(raw)) return undefined;\n const id = Number(raw);\n return Number.isSafeInteger(id) ? id : undefined;\n}\n\nconst ROW_PARAM = { parse: parseRowId, history: \"replace\" } as const;\n\n/** Newest first, as `GET /feedback` sends it (§3.3) — the phone's day headers group runs\n * of equal days, so an older row between two of today's would split the day in two.\n * ISO strings from one server sort as their instants; the sort is stable. */\nconst newestFirst = (a: FeedbackRecord, b: FeedbackRecord) =>\n a.created_at < b.created_at ? 1 : a.created_at > b.created_at ? -1 : 0;\n\nconst rowId = (row: FeedbackRecord) => row.id;\nconst rowTitle = (row: FeedbackRecord) => row.title;\nconst captureScreenshot = () => captureAppScreenshot();\nconst ignore = () => {};\n/** A slot's \"nothing\": `cond && <Thread />` answers `false`, which the detail would draw\n * as an empty Comments section (it draws one for anything but `null`). */\nconst threadOf = (node: ReactNode): ReactNode => (node === false || node === \"\" ? undefined : node);\n\n/**\n * The inbox's wiring, once (§13): from the app's rows and mutations to what the table,\n * the detail and the phone cards take.\n *\n * ```tsx\n * const list = useQuery({ queryKey: keys.feedback.all, queryFn: api.listFeedback });\n * const update = useMutation({ mutationFn: ({ id, ...patch }: FeedbackDetailPatch) => api.update(id, patch), … });\n * const inbox = useFeedbackInbox({ rows: list.data ?? [], canEdit: isAdmin, viewerId: user?.id, update });\n * <DataTable {...inbox.table} edgeFade />\n * ```\n *\n * {@link FeedbackInbox} is this hook drawn — use the hook alone for a layout of your own.\n * Needs a react-router Router (`?row=`, `urlSync`).\n */\nexport function useFeedbackInbox<T extends FeedbackRecord>({\n rows,\n mine = false,\n canEdit,\n viewerId,\n update,\n reply,\n rework,\n publicSwitch = false,\n votes = false,\n comments,\n renderDetail,\n columns: ownColumns,\n swipe = DEFAULT_FEEDBACK_SWIPE,\n commit,\n onUpload,\n fetcher,\n onCaptureScreenshot = captureScreenshot,\n renderDate: renderDateProp,\n pageLinkExclude,\n loading = false,\n detail,\n labels,\n selectable = true,\n}: UseFeedbackInboxOptions<T>): FeedbackInboxState<T> {\n // An admin on /my-feedback reads as any author does (§4.3), as `feedbackColumns` has it.\n const editable = canEdit && !mine;\n const pageLabels = useFeedbackPageLabels(labels);\n const statusLabels = useFeedbackStatusLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n\n // TanStack's `mutateAsync` is stable, so the Undo and the columns' memo hold across\n // renders; the mutation object around it is not.\n const updateAsync = update.mutateAsync;\n const changeStatus = useFeedbackStatusUndo(updateAsync);\n\n // The open row IS the URL (§4.3: `?row=<id>`, every app) — which is what makes a row a\n // real link at all (keksdose feedback #451). Replace, not push: looking through ten\n // reports is not ten steps for Back.\n const [openId, setOpenId] = useSearchParamState<number | null>(\"row\", null, ROW_PARAM);\n const { pathname, search } = useLocation();\n // Through the router, so a basename (and a hash router's `#`) is in the address.\n const here = useHref({ pathname });\n // Built from the CURRENT query, not a bare `?row=`: the table keeps its `f.*`, `sort`\n // and `p` in the URL, and dropping them would open the new tab on an unfiltered list\n // where the row may not be on the first page (keksdose feedback-page.tsx:145).\n const rowHref = useCallback(\n (row: T) => {\n const params = new URLSearchParams(search);\n params.set(\"row\", String(row.id));\n return `${here}?${params.toString()}`;\n },\n [here, search],\n );\n\n // A thumb gesture on one screen, not a view worth linking to (§4.3; keksdose live #330).\n const [awaiting, setAwaiting] = useState(false);\n const shown = useMemo(\n () => (editable && awaiting ? rows.filter((row) => isFeedbackAwaiting(row.status)) : [...rows]).sort(newestFirst),\n [rows, editable, awaiting],\n );\n\n // The admin's selection and bulk edit (§13.6): through the same `update` as every other\n // write here, one PATCH per report — no bulk endpoint, none needed.\n const { bulk, selection } = useFeedbackInboxBulk<T>({\n on: editable && selectable,\n rows: shown,\n update: updateAsync,\n publicSwitch,\n labels,\n });\n\n const kitColumns = useFeedbackColumns<T>({\n mine,\n canEdit,\n onStatus: changeStatus,\n renderDate,\n commit,\n votes,\n pageLinkExclude,\n labels,\n });\n const columns = useMemo(() => (ownColumns ? ownColumns(kitColumns) : kitColumns), [ownColumns, kitColumns]);\n const mobileGroupBy = useMemo(() => feedbackMobileGroupBy(renderDate), [renderDate]);\n\n // The rework's sending is the detail's too (keksdose, Kurvenschmiede): its editors and\n // the public switch wait for whichever write is out.\n const pending = Boolean(update.isPending || rework?.isPending);\n const capture = onCaptureScreenshot ?? undefined;\n\n const detailProps = (row: T): FeedbackRowDetailProps => ({\n row,\n canEdit: editable,\n viewerId,\n onUpdate: updateAsync,\n pending,\n // The page's change, not one the detail would build: the same Undo as the cell's.\n statusChange: changeStatus,\n onUpload,\n fetcher,\n onCaptureScreenshot: capture,\n // Only the app's own: left out, the detail's \"Resolved:\" is the kit's sentence date,\n // no weekday — as kastlan's has always been.\n renderDate: renderDateProp,\n pageLinkExclude,\n onReply: reply\n ? (body, status, attachmentUrl) =>\n reply.mutateAsync({\n id: row.id,\n body,\n ...(status ? { status } : {}),\n ...(attachmentUrl ? { attachmentUrl } : {}),\n })\n : undefined,\n // The reply's one file (§12): all three apps are on server-kit 0.18, and the reply\n // mutation's `attachmentUrl` is in its type from the start (the B1 risk the detail's\n // default guards against — an `onReply` that drops the file — cannot arise here).\n replyAttachments: true,\n replying: reply?.isPending ?? false,\n onRework: rework\n ? (note, attachmentUrl) => rework.mutateAsync({ id: row.id, note, ...(attachmentUrl ? { attachmentUrl } : {}) })\n : undefined,\n comments: threadOf(comments?.(row)),\n onPublicChange: publicSwitch ? (next) => runMutation(updateAsync, { id: row.id, public: next }, ignore) : undefined,\n ...detail,\n });\n\n const table: DataTableProps<T> = {\n rows: shown,\n columns,\n rowKey: rowId,\n storageKey: \"feedback\",\n urlSync: true,\n fillHeight: true,\n rowHref,\n // The link covers the row's first cell; a click anywhere on the row opens it — or,\n // on the open one, closes it.\n onRowClick: (row) => setOpenId((id) => (id === row.id ? null : row.id)),\n isExpanded: (row) => row.id === openId,\n expandedRow: (row) => {\n const node = <FeedbackRowDetail {...detailProps(row)} />;\n return renderDetail ? renderDetail(row, node) : node;\n },\n // §7.13: one word, also when a filter or the toggle leaves nothing; and while the\n // first answer is out, what is happening instead.\n empty: loading ? <LoadingState size=\"sm\" inline /> : <FeedbackEmptyState labels={labels} />,\n mobileGroupBy,\n mobileCard: (row) => (\n <FeedbackMobileCard row={row} showSubmitter={editable} renderDate={renderDate} votes={votes} labels={labels} />\n ),\n // The admin inbox only — /my-feedback is read-only by design (keksdose #331) — and\n // every swipe through the same Undo as a tap (§4.5, §7.10).\n mobileSwipeActions: editable\n ? (row) => feedbackSwipePlan(swipe, row, { change: changeStatus, labels: statusLabels })\n : undefined,\n // The swipes as the desktop's last column (0.46, keksdose dev#598), less what the\n // status cell already offers.\n desktopSwipeActions: { omit: feedbackSwipeOmitShown, name: rowTitle, ...(commit === undefined ? {} : { commit }) },\n // The boxes, the select-all over what the filters keep, and the phone's long press —\n // in whose mode DataTable rests the swipes (§13.6).\n ...(selection ? { selection } : {}),\n };\n\n return {\n editable,\n title: mine ? pageLabels.myTitle : pageLabels.title,\n openId,\n setOpenId,\n rowHref,\n changeStatus,\n awaiting,\n setAwaiting,\n renderDate,\n detailProps,\n table,\n ...(bulk ? { bulk } : {}),\n };\n}\n\n/* ── The component ──────────────────────────────────────────────────────────── */\n\nexport interface FeedbackInboxProps<T extends FeedbackRecord> extends UseFeedbackInboxOptions<T> {\n /**\n * Above the table. Default: `<PageHeader title={title} className=\"mb-4 md:shrink-0\" />`.\n * A function gets the page's title (`feedbackPage.title` / `.myTitle`) — the app's own\n * header: `(title) => <PageHeader size=\"compact\" title={title} data-tour=\"feedback-header\" />`.\n * `null` for none.\n */\n header?: ReactNode | ((title: string) => ReactNode);\n /** Over the inbox's `DataTable` props — `edgeFade`, `locale`, `mobileExpandAsDialog`,\n * `className`, another `storageKey`. Merged last, so it wins. */\n table?: Partial<DataTableProps<T>>;\n /** Further `FloatingAction`s on the phone: after the awaiting toggle on the inbox, in a\n * group of their own on `/my-feedback`. */\n phoneActions?: ReactNode;\n /** On the page's frame — the fill-height column (`FILL_HEIGHT_CLASS`); the app's page\n * padding goes here. */\n className?: string;\n}\n\n/**\n * The feedback inbox (§13): `/feedback` and `/my-feedback` drawn whole from\n * {@link useFeedbackInbox} — the header, the table with its detail, phone cards and swipes,\n * and the phone's awaiting toggle, in the fill-height frame (page layout §1.3) — so a page\n * is its queries and this:\n *\n * ```tsx\n * export function FeedbackPage({ mine = false }: { mine?: boolean }) {\n * const { user, isAdmin } = useAuth();\n * const list = useQuery({ queryKey: mine ? keys.mine : keys.all, queryFn: mine ? api.mine : api.all });\n * const update = useFeedbackUpdate();\n * const reply = useFeedbackReply();\n * const rework = useFeedbackRework();\n * if (!mine && !isAdmin) return <Navigate to=\"/my-feedback\" replace />;\n * return (\n * <FeedbackInbox\n * rows={list.data ?? []} loading={list.isPending} mine={mine} canEdit={isAdmin} viewerId={user?.id}\n * update={update} reply={reply} rework={rework}\n * onUpload={api.upload} fetcher={api.file}\n * />\n * );\n * }\n * ```\n *\n * Every slot is {@link UseFeedbackInboxOptions}' or one of the four below. The redirect of\n * a non-admin, the routes and the queries stay the app's.\n */\nexport function FeedbackInbox<T extends FeedbackRecord>({\n header,\n table,\n phoneActions,\n className,\n ...options\n}: FeedbackInboxProps<T>) {\n const inbox = useFeedbackInbox<T>(options);\n const pageLabels = useFeedbackPageLabels(options.labels);\n const head =\n header === undefined ? (\n <PageHeader title={inbox.title} className=\"mb-4 md:shrink-0\" />\n ) : typeof header === \"function\" ? (\n header(inbox.title)\n ) : (\n header\n );\n // The phone's floating bulk bar takes the bottom of the screen while rows are selected,\n // where the toggle's group sits: the group steps aside until the selection empties.\n const selecting = (inbox.bulk?.rows.length ?? 0) > 0;\n return (\n // A plain element on the way to the table carries the fill (page layout §1.3).\n <div className={cn(FILL_HEIGHT_CLASS, className)} aria-busy={options.loading || undefined} data-feedback-inbox=\"\">\n {head}\n {inbox.bulk && <FeedbackInboxBulkBar bulk={inbox.bulk} commit={options.commit} labels={options.labels} />}\n <DataTable {...inbox.table} {...table} />\n {selecting ? null : inbox.editable ? (\n // The phone's floating action (keksdose feedback #58): only on the inbox — on\n // /my-feedback the author is not waiting on their own verdict.\n <FeedbackAwaitingToggle pressed={inbox.awaiting} onPressedChange={inbox.setAwaiting} labels={options.labels}>\n {phoneActions}\n </FeedbackAwaitingToggle>\n ) : (\n phoneActions && (\n <FloatingActionGroup aria-label={pageLabels.phoneActions} className=\"md:hidden\">\n {phoneActions}\n </FloatingActionGroup>\n )\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
  import type { AuthedFetcher } from "../hooks/use-authed-src.js";
3
3
  import type { PathPattern } from "../landing/routing.js";
4
4
  import { type LabelOverride } from "../i18n/kit-labels.js";
5
- import type { FeedbackNoteAttachment, FeedbackStatus } from "./feedback-inbox.js";
5
+ import type { FeedbackCategory, FeedbackNoteAttachment, FeedbackStatus } from "./feedback-inbox.js";
6
6
  import type { FeedbackContext, FeedbackRecord } from "./feedback-record.js";
7
7
  import type { FeedbackStatusChange } from "./feedback-status-undo.js";
8
8
  import type { CommitScope } from "../components/write-lock.js";
@@ -176,6 +176,10 @@ export interface FeedbackDetailPatch {
176
176
  * `PATCH /feedback/{id} {"public": next}` carries, alone, so an app can send it
177
177
  * through the same `onUpdate` mutation. */
178
178
  public?: boolean;
179
+ /** 0.57: the category (§3.4: the admin's, any time; never CRASH) — what the inbox's bulk
180
+ * bar sends, alone, through the same mutation (`FeedbackInbox`, §13.6). The detail
181
+ * itself never sends it. */
182
+ category?: FeedbackCategory;
179
183
  }
180
184
  /**
181
185
  * The page's update mutation (0.52): **TanStack Query's `mutateAsync`** — pass
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-row-detail.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { AuthedImage } from \"../components/authed-image\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { TextLink } from \"../components/text-link\";\nimport { toast } from \"../components/toast\";\nimport { Button } from \"../components/button\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport type { PathPattern } from \"../landing/routing\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport { DEFAULT_MAX_ATTACHMENT_BYTES } from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackNoteEditor,\n FeedbackProse,\n FeedbackStatusTransitions,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-inbox\";\nimport type { FeedbackNoteAttachment, FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_REWORKABLE_STATUSES,\n appendRework,\n isImageAttachment,\n replaceDescription,\n splitBodyAttachments,\n splitDescription,\n} from \"./feedback-record\";\nimport type { FeedbackContext, FeedbackRecord } from \"./feedback-record\";\nimport { diagnosticsEntries, FeedbackDiagnosticsList } from \"./feedback-diagnostics\";\nimport { runMutation } from \"./feedback-mutation\";\nimport { feedbackPage, feedbackPageLinkTarget } from \"./feedback-page-link\";\nimport { useFeedbackStatusUndo } from \"./feedback-status-undo\";\nimport type { FeedbackStatusChange } from \"./feedback-status-undo\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\nimport { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from \"./feedback-replies\";\nimport { FeedbackAttachmentDownload, plainFeedbackFetch } from \"./feedback-attachment-download\";\nimport { FeedbackPublicSwitch } from \"./feedback-public-switch\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * One report, opened — the panel a feedback table unfolds under its row (§4.4 of\n * docs/feedback-harmonization.md), lifted from keksdose's `FeedbackRow`\n * (`frontend/src/features/feedback/feedback-page.tsx:329-612`) with the contract's\n * changes applied.\n *\n * Until 0.27 each app wrote this panel itself out of the kit's shell\n * (`FeedbackDetail` / `FeedbackDetailSection` / `FeedbackNoteEditor`), and the three\n * copies had drifted in exactly the places a user notices:\n *\n * - **the description editor held the raw body** — keksdose `initial={fb.body}`\n * (`:420`), kastlan the same (`pages/feedback-page.tsx:226`) — so an author fixing a\n * typo could delete the `--- REWORK … ---` rounds and the `[screenshot]` file lines\n * with it, and the history was gone. Here the editor holds the ORIGINAL description\n * only ({@link splitDescription}) and saves through {@link replaceDescription}, which\n * puts every appended block and file line back unchanged;\n * - **kastlan's rework sent `status: \"OPEN\"`** from the client (`:189-195`) and offered it\n * on three statuses; keksdose sends the body alone and lets the server reopen the row\n * on five (live #331). The contract is keksdose's (§3.4) — {@link appendRework}, one\n * file, the server reopens;\n * - **kastlan showed only `screenshot_url`** (`:265-276`) and no downloads; keksdose the\n * screenshot, the other files and the rework pictures, a PDF as a download (dev #578);\n * - **the URL fell away on Kurvenschmiede's rows**, whose backfilled `context.url` is a\n * path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section\n * vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).\n *\n * The sections, in the contract's order: **Subject** (the category and the title, §8.1;\n * `showSubject={false}` leaves it out), **Description** (the body; the author's\n * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Replies** (0.38,\n * §9.7: the timeline, the admin's reply composer with its status picker and the nudge,\n * Rework — on a row without `replies`, a server before 0.38, the frozen\n * **Outcome** read-only in its place), **Comments** (0.38: the app's\n * `FeedbackCommentThread`, when passed), **Send for rework** (only while reworking) and\n * **Status** (with the **Visible to others** switch when `onPublicChange` is passed).\n *\n * The app keeps its data and its API: the row it renders, the PATCH (`onUpdate`), the\n * upload (`onUpload`), the authenticated GET (`fetcher`) and who is looking (`canEdit`,\n * `viewerId`). Every word is the kit's `feedbackDetail` namespace.\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/**\n * `feedbackDetail` — the row detail's words (§4.4). English: keksdose's `en.json`\n * (`feedback.*`, `common.*`, `more.copy_url`) with the contract's changes; the de-CH\n * canon (keksdose `de-CH.json`, ss never ß, \"Nacharbeit\" throughout — §7.14) on each key.\n */\nexport interface FeedbackDetailLabels {\n /** The first section's heading: what the report is called (feedback contract §8.1,\n * keksdose live #395 — with the row expanded and the table scrolled, nothing said\n * which entry was being answered). de-CH: \"Betreff\" */\n subject: string;\n /** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks\n * \"What happened? (optional)\"; the description, once filed, is headed as what it is\n * — and as the author's \"Edit description\" calls it. de-CH: \"Beschreibung\" */\n body: string;\n /** The author's button on that heading. de-CH: \"Bearbeiten\" */\n edit: string;\n /** Its accessible name — says WHAT is edited; contains the visible word.\n * de-CH: \"Beschreibung bearbeiten\" */\n editDescription: string;\n /** The editors' save button (description, outcome). de-CH: \"Speichern\" */\n save: string;\n /** Every editor's cancel button. de-CH: \"Abbrechen\" */\n cancel: string;\n /** Section 2's heading. de-CH: \"URL\" */\n url: string;\n /** The copy button beside the link — copies the FULL `context.url`.\n * de-CH: \"URL kopieren\" */\n copyUrl: string;\n /** The heading over `context.diagnostics`, between the URL and the attachment (keksdose\n * live #481). de-CH: \"Diagnose\" */\n diagnostics: string;\n /** Section 3's heading, and each picture's alt text. de-CH: \"Anhang\" */\n attachment: string;\n /** A PDF's / text file's download button, given the file's name.\n * de-CH: \"{{name}} herunterladen\" */\n download: (name: string) => string;\n /** The toast when that download failed. de-CH: \"Der Anhang konnte nicht\n * heruntergeladen werden.\" */\n downloadFailed: string;\n /** Section 4's heading on a row without `replies` (a server before 0.38), where the\n * frozen outcome is shown read-only. de-CH: \"Ergebnis\" */\n outcome: string;\n /** Section 4's heading since 0.38: the replies' timeline (§9.7). de-CH: \"Antworten\" */\n replies: string;\n /** On section 4's heading line once the row is settled, given the formatted\n * `resolved_at` date. de-CH: \"Erledigt am: {{date}}\" */\n resolvedAt: (date: string) => string;\n /** The button that opens section 5 on an answered row. de-CH: \"Nacharbeit\" */\n rework: string;\n /** Section 5's heading. de-CH: \"Zur Nacharbeit senden\" */\n reworkTitle: string;\n /** Section 5's send button. de-CH: \"Nacharbeit senden\" */\n reworkSend: string;\n /** The line above the rework note. de-CH: \"Was muss noch angepasst werden? Neue\n * Anforderungen oder Richtungswechsel.\" */\n reworkPlaceholder: string;\n /** The toast when the rework's file did not upload — and so nothing was sent. Names\n * no screenshot: it fires for a PDF or a log too (§7.14; keksdose's said\n * \"screenshot\"). de-CH: \"Die Datei konnte nicht hochgeladen werden. Die Nacharbeit\n * wurde nicht gesendet.\" */\n reworkUploadFailed: string;\n /** Section 6's heading. de-CH: \"Status\" */\n status: string;\n /** A `FeedbackNoteEditor`'s box, named when no `placeholder` line names it and the\n * caller gives no `label` — without it the box was read out as just \"edit text\"\n * (round 0.34, the showcase's axe check). de-CH: \"Notiz\" */\n note: string;\n}\n\n// Defined in ./feedback-row-detail-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DETAIL_LABELS };\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: LabelOverride<FeedbackDetailLabels>): FeedbackDetailLabels {\n return useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);\n}\n\n/* ── The page a report was filed on (§3.2, §4.4.2) ────────────────────────── */\n\n// The page as the reporter had it (path + query + hash) and the full URL \"Copy URL\"\n// copies were public too, as `feedbackPageHref` and `feedbackPageUrl`, until 0.53 (deprecated\n// in 0.52, API-4): `feedbackPage` (./feedback-page-link) is the one reading, and the kit\n// links only its path.\n\n/** The page's path alone — the compact text a URL column shows (§4.3), and the only part\n * of the page the kit links. */\nexport function feedbackPagePath(context: FeedbackContext | null | undefined): string | null {\n return feedbackPage(context)?.path ?? null;\n}\n\n/* ── Attachments (§3.5, §4.4.3) ───────────────────────────────────────────── */\n\n/**\n * Every file a report carries, in the order the detail shows them (§4.4.3): the\n * screenshot it was filed with, then its other files, then whatever the rework rounds\n * brought, in body order (keksdose dev #578, live #291). One list rather than three —\n * a reader looking for \"the picture\" should not have to know at which point in the\n * conversation it was taken.\n *\n * Each URL once: keksdose and Kurvenschmiede content-address their keys, so the same\n * screenshot pasted twice is one URL — shown twice it is the same picture twice (and,\n * keyed by URL, a React key clash keksdose had).\n */\nexport function feedbackAttachmentUrls(\n row: Pick<FeedbackRecord, \"screenshot_url\" | \"attachment_urls\" | \"body\"> & Partial<Pick<FeedbackRecord, \"replies\">>,\n): string[] {\n const urls = [\n ...(row.screenshot_url ? [row.screenshot_url] : []),\n // `?? []`: NULL on rows filed before the column, and no key at all from a server\n // that predates it (keksdose dev #578).\n ...(row.attachment_urls ?? []),\n ...splitBodyAttachments(row.body).urls,\n // 0.40 (§10.3): a rework entry's one file, in entry order — where the rework\n // pictures of the body were before.\n ...(row.replies ?? []).flatMap((reply) =>\n reply.kind === \"rework\" && reply.attachment_url ? [reply.attachment_url] : [],\n ),\n ];\n return [...new Set(urls)];\n}\n\n/* ── Who may do what (§3.4, §4.3, §4.4) ───────────────────────────────────── */\n\n/** What the person looking at a row may do with it — {@link feedbackRowAccess}. */\nexport interface FeedbackRowAccess {\n /** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */\n isAuthor: boolean;\n /** \"Edit description\": the author, while the row is OPEN, READY or IN_PROGRESS — on either\n * page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */\n canEditDescription: boolean;\n /** The admin on the inbox (`canEdit`): since 0.38 the reply composer (the outcome\n * itself is frozen, §9.7). */\n canEditOutcome: boolean;\n /** \"Rework\": the author, on an answered row ({@link FEEDBACK_REWORKABLE_STATUSES}).\n * Since 0.40 (§10.2 decision 6) the author's only — an admin sends a report back with\n * a reply whose status is `REWORK`; the server answers anyone else 403. */\n canRework: boolean;\n /** The status pills: the admin on the inbox; everyone else sees them read-only. */\n canChangeStatus: boolean;\n}\n\n/**\n * The detail's permission rules in one place — the same answers the server gives\n * (§3.4), so the panel never offers what the PATCH would refuse.\n *\n * `canEdit` is the app's \"admin on `/feedback`\": an admin on `/my-feedback` edits no\n * status and no outcome there (§4.3), but as the author still edits the description\n * and sends for rework. `viewerId` is the signed-in user's id.\n */\nexport function feedbackRowAccess(\n row: Pick<FeedbackRecord, \"user_id\" | \"status\">,\n { canEdit, viewerId }: { canEdit: boolean; viewerId?: number | null },\n): FeedbackRowAccess {\n // `user_id: null` is an erased author (keksdose live #275): nobody is that author,\n // least of all a viewer whose id happens to be missing too.\n const isAuthor = row.user_id !== null && viewerId !== undefined && viewerId !== null && viewerId === row.user_id;\n return {\n isAuthor,\n canEditDescription: isAuthor && FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status),\n canEditOutcome: canEdit,\n canRework: isAuthor && FEEDBACK_REWORKABLE_STATUSES.includes(row.status),\n canChangeStatus: canEdit,\n };\n}\n\n/* ── The PATCH ─────────────────────────────────────────────────────────────── */\n\n/** What the detail sends as `PATCH /feedback/{id}` (§3.4): the body (a description\n * edit, a rework append) or the outcome — and, through {@link useFeedbackStatusUndo}\n * when no `statusChange` is given, the status. Never two at once: a rework PATCH must\n * carry the body ALONE for the server to read it as an append (keksdose live #331). */\nexport interface FeedbackDetailPatch {\n id: number;\n body?: string;\n /** `null` clears it — a real edit. **Frozen since 0.38** (§9.7): server-kit 0.11\n * answers 422; the detail no longer sends it (a reply is written instead). */\n outcome?: string | null;\n status?: FeedbackStatus;\n /** 0.38.1: the **Visible to others** flip (§9.3) — what `onPublicChange`'s\n * `PATCH /feedback/{id} {\"public\": next}` carries, alone, so an app can send it\n * through the same `onUpdate` mutation. */\n public?: boolean;\n}\n\n/**\n * The page's update mutation (0.52): **TanStack Query's `mutateAsync`** — pass\n * `update.mutateAsync` (and `pending={update.isPending}`), the same function\n * `useFeedbackStatusUndo` takes. An editor closes when the promise resolves, which it does\n * for every call — also when a status pill was pressed while the save was in flight\n * (refactor audit 2026-10-30, FLW-3). A hand-written one resolves once the PATCH has\n * landed. A failure is the mutation's own to toast — the server's `detail`, else\n * `feedbackToast.updateFailed` (§4.4) — and leaves the editor open with its draft.\n */\nexport type FeedbackDetailUpdateAsync = (patch: FeedbackDetailPatch) => Promise<unknown>;\n\n/** `POST /feedback/attachments` (§3.3): one file up, its URL\n * (`/api/v1/feedback/attachments/<key>`) back. Reject on failure. */\nexport type FeedbackUpload = (file: File) => Promise<string>;\n\n/* ── Send for rework (§4.4.5) ─────────────────────────────────────────────── */\n\n/** 0.40 (§10.3): `POST /feedback/{id}/rework` — the trimmed note and the uploaded file's\n * URL. Return the promise: the section closes when it resolves and keeps the note when\n * it rejects (the toast is the app's: the server's `detail`, else\n * `feedbackToast.updateFailed`). */\nexport type FeedbackReworkSend = (note: string, attachmentUrl?: string) => void | Promise<unknown>;\n\nexport interface FeedbackReworkSectionProps {\n /** The row being sent back: its id, and the body the round is appended to. */\n row: Pick<FeedbackRecord, \"id\" | \"body\">;\n /** 0.40: the rework as an entry of its own — see {@link FeedbackReworkSend}. With it,\n * `onUpdate` is not called. */\n onRework?: FeedbackReworkSend;\n /** The PATCH — see {@link FeedbackDetailUpdateAsync}. Called with `{ id, body }` only,\n * and only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still\n * turns into an entry for one release. Optional since 0.40; pass one of the two. */\n onUpdate?: FeedbackDetailUpdateAsync;\n /** The PATCH is in flight (`update.isPending`). The upload adds its own. */\n pending?: boolean;\n /** The upload. Left out, the note goes without a file field. */\n onUpload?: FeedbackUpload;\n /** Snapshot the app view as a File — the kit's `captureAppScreenshot`, or the app's\n * own. A \"Capture screenshot\" button appears only with it; a rejection (no\n * `modern-screenshot`, a canvas that would not draw) toasts\n * `feedbackToast.captureFailed` and adds nothing. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The round landed — the server has the note and, on an answered row, reopened it. */\n onSent?: () => void;\n onCancel: () => void;\n /** Over the field's defaults: the contract's types and 10 MB (§3.5), the\n * `feedbackToast` refusals, the `feedbackAttachment` words. */\n attachment?: Partial<FeedbackNoteAttachment>;\n /** The send COMMITS — see `FeedbackNoteEditor`'s `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter sends an answered report\n * back with a note and at most one file, and the server reopens it (§3.4, keksdose\n * `submitRework`, `feedback-page.tsx:350`).\n *\n * - **The note is required** and trimmed: Send stays disabled on a blank box (a file\n * alone cannot be sent; keksdose `:577` dropped that click silently).\n * - **One file**, added, captured or pasted — the dialog's types and 10 MB. It is\n * uploaded FIRST and awaited: a body naming a file that never arrived would be a\n * rework whose evidence is missing, and the upload is the half that fails on its\n * own. If it fails nothing is sent and the toast says so; the note stays in the box.\n * - **0.40: an entry of its own** (§10.3) — with `onRework`, the note and the file's URL\n * go to `POST /feedback/{id}/rework` and the body is not touched. Without it, the\n * legacy append below, which server-kit 0.13 still converts (one minor).\n * - **The body alone goes in the PATCH** — {@link appendRework}'s strict extension of\n * the current body, which is how the server recognises the append and reopens the\n * row. Never a `status` (kastlan's client sent `OPEN`; an author may not write one,\n * so the server refused the whole PATCH, note and all — keksdose live #331).\n *\n * Exported on its own for an app that offers rework somewhere other than the row\n * detail. Kurvenschmiede's author verdict (\"That is it, done\") is NOT here: an answered\n * item stays answered unless its author sends it back (§7.2).\n */\nexport function FeedbackReworkSection({\n row,\n onRework,\n onUpdate,\n pending = false,\n onUpload,\n onCaptureScreenshot,\n onSent,\n onCancel,\n attachment,\n commit,\n labels: labelsProp,\n}: FeedbackReworkSectionProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n const toasts = useFeedbackToastLabels();\n const [uploading, setUploading] = useState(false);\n const [sending, setSending] = useState(false);\n // The body as it is NOW, not as it was when Send was pressed: the list can refresh\n // while the file uploads, and an append to a stale body is no longer a strict\n // extension of the stored one — the server would read it as an edit, and refuse it.\n const latestBody = useRef(row.body);\n useEffect(() => {\n latestBody.current = row.body;\n }, [row.body]);\n\n const capture = onCaptureScreenshot\n ? async () => {\n try {\n return await onCaptureScreenshot();\n } catch {\n toast.error(toasts.captureFailed);\n return null;\n }\n }\n : undefined;\n\n const send = async (value: string, file?: File | null) => {\n const note = value.trim();\n if (!note) return;\n let url: string | null = null;\n if (file && onUpload) {\n setUploading(true);\n try {\n url = await onUpload(file);\n } catch {\n toast.error(labels.reworkUploadFailed);\n return;\n } finally {\n setUploading(false);\n }\n }\n if (onRework) {\n // 0.40 (§10.3): the entry, not the body — the server checks the URL (a 422 outside\n // the app's attachment prefix).\n setSending(true);\n try {\n await onRework(note, url ?? undefined);\n } catch {\n return; // The app's toast; the note stays in the box.\n } finally {\n setSending(false);\n }\n onSent?.();\n return;\n }\n if (!onUpdate) return;\n let body: string | null;\n try {\n body = appendRework(latestBody.current, note, url);\n } catch {\n // The upload answered with something that is not an attachment URL of this\n // contract — written anyway it would sit in the body as prose no reader knows to\n // be a file. As far as the person is concerned, the file did not arrive.\n toast.error(labels.reworkUploadFailed);\n return;\n }\n if (body === null) return;\n runMutation(onUpdate, { id: row.id, body }, () => onSent?.());\n };\n\n return (\n <FeedbackDetailSection title={labels.reworkTitle}>\n <FeedbackNoteEditor\n key={row.id}\n initial=\"\"\n required\n commit={commit}\n pending={pending || uploading || sending}\n onSave={(value, file) => void send(value, file)}\n onCancel={onCancel}\n saveLabel={labels.reworkSend}\n cancelLabel={labels.cancel}\n placeholder={labels.reworkPlaceholder}\n attachment={\n onUpload\n ? {\n accept: FEEDBACK_ATTACHMENT_ACCEPT,\n maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,\n onError: (kind) => {\n toast.error(kind === \"type\" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);\n },\n onCaptureScreenshot: capture,\n ...attachment,\n }\n : undefined\n }\n />\n </FeedbackDetailSection>\n );\n}\n\n/* ── The detail (§4.4) ─────────────────────────────────────────────────────── */\n\nexport interface FeedbackRowDetailProps {\n /** The report — the list's own row, as `GET /feedback` / `GET /feedback/my` sent it. */\n row: FeedbackRecord;\n /** The admin on the inbox (`/feedback`): status pills and the outcome editor. `false`\n * on `/my-feedback`, for an admin too (§4.3). */\n canEdit: boolean;\n /** The signed-in user's id — who the AUTHOR is: the description editor and rework. */\n viewerId?: number | null;\n /** The page's PATCH — `update.mutateAsync` (see {@link FeedbackDetailUpdateAsync}). The\n * callback form, `update.mutate`, went in 0.53 (deprecated in 0.52). */\n onUpdate: FeedbackDetailUpdateAsync;\n /** `update.isPending`: the editors' save buttons wait for it. */\n pending?: boolean;\n /** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with\n * `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go\n * through it, since the attachment route wants the bearer token (§3.5). Left out,\n * the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /**\n * The undoable status change — what `useFeedbackStatusUndo(update.mutateAsync)` returns,\n * the SAME one the table's status cell and the swipes use (§4.5). Left out, the\n * detail builds one from `onUpdate`, which is equivalent as long as `onUpdate` is the\n * page's mutation (its observer outlives the row a status filter removes).\n */\n statusChange?: FeedbackStatusChange;\n /** For the rework's and (0.47, with `replyAttachments`) the reply composer's \"Capture screenshot\" — see\n * {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,\n * over the contract's defaults. */\n reworkAttachment?: Partial<FeedbackNoteAttachment>;\n /** How the `resolved_at` date reads — the app's date cell, as a string. Default\n * (0.52): the `<UiKitProvider formatDate>` when the app set one, else the kit's\n * `formatDate` in the provider's locale and time zone, `medium` style. */\n renderDate?: (iso: string) => string;\n /**\n * 0.52 (FLW-7): the app's one-shot routes, ON TOP of the kit's — `/join`,\n * `/register`, `/verify-email`, `/reset-password` (a string is that path and everything\n * under it, a RegExp is tested on the path, as `useLastVisitedPage`'s `exclude`). The\n * URL section shows such a page as text with Copy, never as a link: it acts on load,\n * and a report's page is the reporter's to write. Any other page links its PATH alone;\n * the query, where tokens and `?next=` targets travel, is text. keksdose:\n * `[\"/banks/callback\", \"/share-target\"]`. Keep it stable (module scope).\n */\n pageLinkExclude?: readonly PathPattern[];\n /**\n * The detail's saves COMMIT (description, outcome, rework, status): under a locked\n * `WriteLockProvider` each is `aria-disabled` with the lock's reason, and nothing\n * reaches `onUpdate`. Off by default — keksdose's shell lock is a read-only demo\n * BUDGET, and a person viewing one still owns their own reports.\n */\n commit?: CommitScope;\n /**\n * Open with the subject: the category badge and the title (feedback contract §8.1,\n * keksdose live #395). On by default; `false` for a page that already shows the title\n * beside the detail.\n */\n showSubject?: boolean;\n /**\n * 0.38: `POST /feedback/{id}/replies` (§9.7) — the body and the status picked\n * (`undefined`: \"No change\"). With `canEdit` and a row that carries `replies`, the\n * reply composer stands under the timeline. Return the promise: the box clears when it\n * resolves. A failure is the app's to toast — the server's `detail`, else\n * `feedbackToast.replyFailed` (0.38.1).\n *\n * 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the\n * composer takes one file as the rework note does — pasted into the box, captured or\n * picked — uploaded first, and its URL is the third argument (`attachment_url` in the\n * body; server-kit 0.18).\n */\n onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;\n /**\n * 0.47 (§12): the reply composer takes one file — through `onUpload`,\n * `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:\n * `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of\n * `(body, status)` does not grow a file field whose file its reply would drop (round\n * 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes\n * `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an\n * older server answers the field with a 422).\n */\n replyAttachments?: boolean;\n /** 0.38: the reply is in flight. */\n replying?: boolean;\n /**\n * 0.40 (§10.3): `POST /feedback/{id}/rework` — the author's rework as an entry of its\n * own (see {@link FeedbackReworkSend}). The **Rework** button shows only to the author\n * on an answered row, and only where the reply composer is not shown (§10.2 decision 6:\n * an admin answering a report sends it back with a reply whose status is `REWORK`).\n * Left out, the rework goes through `onUpdate` as the legacy body append — for one\n * minor, while the server converts it.\n */\n onRework?: FeedbackReworkSend;\n /** 0.38: the report's comments — the app's `FeedbackCommentThread` — in a section of\n * their own under the replies. A thread here draws the comments alone (0.38.1): the\n * Replies section above has the replies, so whatever `replies` the thread is handed\n * are dropped, and nothing is drawn twice. */\n comments?: ReactNode;\n /** 0.38: `PATCH /feedback/{id} {\"public\": next}` — draws the **Visible to others**\n * switch (`FeedbackPublicSwitch`) in the Status section, for the admin (`canEdit`)\n * and, within §9.3's window, the author. The page's own mutation fits:\n * `onPublicChange={(next) => update.mutate({ id: row.id, public: next })}` —\n * {@link FeedbackDetailPatch}'s `public` (0.38.1). */\n onPublicChange?: (next: boolean) => void;\n labels?: LabelOverride<FeedbackDetailLabels>;\n className?: string;\n}\n\n/**\n * The expanded row of a feedback table (§4.4): what was reported, where, with which\n * files, what the answer was, and where it stands — with the edits each viewer may\n * make. Hand it to `DataTable`'s `expandedRow`:\n *\n * ```tsx\n * const update = useMutation({ mutationFn: ({ id, ...patch }) => api.updateFeedback(id, patch),\n * onSuccess: invalidate, onError: (e) => toast.error(detailOf(e) ?? toastLabels.updateFailed) });\n * const changeStatus = useFeedbackStatusUndo(update.mutateAsync);\n * <DataTable\n * isExpanded={(row) => expandedId === row.id}\n * expandedRow={(row) => (\n * <FeedbackRowDetail row={row} canEdit={isAdmin && !mine} viewerId={user?.id}\n * onUpdate={update.mutateAsync} pending={update.isPending} statusChange={changeStatus}\n * onUpload={api.uploadFeedbackAttachment} fetcher={authedImageFetcher}\n * onCaptureScreenshot={captureAppScreenshot} />\n * )} … />\n * ```\n *\n * Mounted only while its row is expanded, so which editor is open lives here, and\n * collapsing the row closes it. Each editor is keyed by the row's id, so a draft is\n * never thrown away by a list refresh under it.\n */\nexport function FeedbackRowDetail({\n row,\n canEdit,\n viewerId,\n onUpdate,\n pending = false,\n onUpload,\n fetcher,\n statusChange,\n onCaptureScreenshot,\n reworkAttachment,\n renderDate,\n pageLinkExclude,\n commit,\n showSubject = true,\n onReply,\n replyAttachments = false,\n replying = false,\n onRework,\n comments,\n onPublicChange,\n labels: labelsProp,\n className,\n}: FeedbackRowDetailProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n // \"placeholder = the dialog's body label\" (§4.4.1): read from the dialog's own\n // namespace, so the two can never say different things.\n const dialogText = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS);\n const ownStatusChange = useFeedbackStatusUndo(onUpdate);\n const changeStatus = statusChange ?? ownStatusChange;\n const [editingBody, setEditingBody] = useState(false);\n const [reworking, setReworking] = useState(false);\n\n const access = feedbackRowAccess(row, { canEdit, viewerId });\n // The body as written, without its file lines (they are shown as files below) — the\n // rework rounds stay in the prose: they are what was said after the answer.\n const bodyText = splitBodyAttachments(row.body).text;\n const { description } = splitDescription(row.body);\n const page = feedbackPage(row.context);\n const pageLink = page && feedbackPageLinkTarget(page, pageLinkExclude);\n // What the app attached is for whoever handles the report and whoever sent it, never\n // for another viewer of a shared row (keksdose live #481).\n const diagnostics = canEdit || access.isAuthor ? diagnosticsEntries(row.context?.diagnostics) : [];\n // 0.47 (§12): with the entries, each entry's file is drawn inline in the Replies\n // timeline, so the Attachment section holds the report's own files.\n const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: undefined } : row);\n const fetchFile = fetcher ?? plainFeedbackFetch;\n // A date in a sentence (\"Resolved: …\"), so no weekday — through the kit's one day\n // ladder, which reaches the provider's `formatDate` (0.52, refactor audit 2026-10-30,\n // DUP-2).\n const day = useDayFormatter<string>(renderDate);\n const dateText = (iso: string) => day(iso);\n\n const saveDescription = (value: string) => {\n // Only a real change makes a round trip; clearing it is one (\"\" — the description\n // is optional, §7.4), and the rounds and file lines go back unchanged.\n if (value.trim() === description.trim()) {\n setEditingBody(false);\n return;\n }\n runMutation(onUpdate, { id: row.id, body: replaceDescription(row.body, value) }, () => setEditingBody(false));\n };\n\n // 0.38 (§9.7): the replies replace the outcome; a row without them is a server before\n // 0.38, whose frozen outcome is shown as it is, read-only.\n const replies = row.replies;\n const commentLabels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS);\n // 0.40 (§10.2 decision 6): whoever has the reply composer answers with it — REWORK is\n // one of its statuses — so the Rework button is for an author without it.\n const composerShown = Boolean(replies && access.canEditOutcome && onReply);\n const canRework = access.canRework && !composerShown;\n const showRework = canRework && reworking;\n\n return (\n <FeedbackDetail className={className}>\n {showSubject && (\n <FeedbackDetailSection title={labels.subject}>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm\" data-feedback-subject=\"\">\n <FeedbackCategoryBadge category={row.category} />\n <span className=\"min-w-0 font-medium text-primary [overflow-wrap:anywhere]\">{row.title}</span>\n </p>\n </FeedbackDetailSection>\n )}\n <FeedbackDetailSection\n title={labels.body}\n action={\n access.canEditDescription && !editingBody ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingBody(true);\n }}\n aria-label={labels.editDescription}\n >\n {labels.edit}\n </Button>\n ) : undefined\n }\n >\n {access.canEditDescription && editingBody ? (\n <FeedbackNoteEditor\n key={row.id}\n // The ORIGINAL description only — see the module note.\n initial={description}\n pending={pending}\n commit={commit}\n onSave={saveDescription}\n onCancel={() => setEditingBody(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={dialogText.bodyOptional ?? dialogText.body}\n />\n ) : (\n <FeedbackProse>{bodyText.trim() ? bodyText : undefined}</FeedbackProse>\n )}\n </FeedbackDetailSection>\n\n {page && (\n // The page it was filed on, with the full URL a click away (keksdose #278). Only\n // the path is a link; the query and hash, the reporter's to write, are text — and\n // a one-shot path is text too (FLW-7, 0.52 code review: see feedback-page-link).\n <FeedbackDetailSection title={labels.url}>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <code\n className=\"min-w-0 break-all text-xs text-secondary\"\n data-feedback-page=\"\"\n data-feedback-page-unlinked={pageLink ? undefined : \"\"}\n >\n {pageLink ? (\n <TextLink href={pageLink} tone=\"secondary\" underline=\"hover\" stopPropagation>\n {page.path}\n </TextLink>\n ) : (\n page.path\n )}\n {page.href.slice(page.path.length)}\n </code>\n <CopyButton size=\"xs\" className=\"shrink-0\" text={page.url} label={labels.copyUrl} stopPropagation />\n </div>\n </FeedbackDetailSection>\n )}\n\n {diagnostics.length > 0 && (\n <FeedbackDetailSection title={labels.diagnostics}>\n <FeedbackDiagnosticsList entries={diagnostics} />\n </FeedbackDetailSection>\n )}\n\n {files.length > 0 && (\n <FeedbackDetailSection title={labels.attachment}>\n <div className=\"flex min-w-0 flex-wrap items-start gap-2\">\n {files.map((url) =>\n isImageAttachment(url) ? (\n <AuthedImage\n key={url}\n src={url}\n alt={labels.attachment}\n fetcher={fetchFile}\n link\n stopPropagation\n // A broken attachment is simply not in the row (keksdose's\n // AuthedImage default) rather than a grey tile per failure.\n errorFallback={null}\n wrapperClassName=\"inline-block max-w-full\"\n className=\"max-h-64 max-w-full rounded border border-subtle\"\n />\n ) : (\n <FeedbackAttachmentDownload key={url} url={url} fetcher={fetchFile} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={replies ? labels.replies : labels.outcome}\n // When the row entered DONE / WONT_DO (keksdose feedback #96).\n action={\n row.resolved_at ? (\n <span className=\"text-xs text-muted\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n <div className=\"space-y-3\">\n {replies ? (\n <FeedbackReplyTimeline replies={replies} fetcher={fetchFile} />\n ) : (\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n )}\n {composerShown && replies && onReply && (\n <FeedbackReplyComposer\n key={row.id}\n status={row.status}\n onReply={onReply}\n pending={replying}\n onUpload={replyAttachments ? onUpload : undefined}\n onCaptureScreenshot={replyAttachments ? onCaptureScreenshot : undefined}\n attachment={replyAttachments ? reworkAttachment : undefined}\n suggestedStatus={feedbackReplyNudge(row)}\n commit={commit}\n />\n )}\n {/* No \"Open page\" button here since 0.46 (keksdose live #449): the URL section\n above already links the page, with the path on screen. */}\n {canRework && !reworking && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setReworking(true);\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden /> {labels.rework}\n </Button>\n </div>\n )}\n </div>\n </FeedbackDetailSection>\n\n {comments != null && (\n <FeedbackDetailSection title={commentLabels.comments}>\n <FeedbackRepliesDrawnAboveContext.Provider value>{comments}</FeedbackRepliesDrawnAboveContext.Provider>\n </FeedbackDetailSection>\n )}\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\n onRework={onRework}\n onUpdate={onUpdate}\n pending={pending}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n onSent={() => setReworking(false)}\n onCancel={() => setReworking(false)}\n attachment={reworkAttachment}\n commit={commit}\n labels={labelsProp}\n />\n )}\n\n <FeedbackDetailSection title={labels.status}>\n {/* Expanded: the whole enum in one click for the admin (keksdose feedback #421);\n everyone else the narrow set, read-only — a picture of the workflow. */}\n <FeedbackStatusTransitions\n status={row.status}\n statuses={\n access.canChangeStatus ? selectableFeedbackStatuses(row.status) : visibleFeedbackStatuses(row.status)\n }\n canEdit={access.canChangeStatus}\n commit={commit}\n onPick={(status) => changeStatus(row, status)}\n variant=\"pill\"\n />\n {onPublicChange && (\n <FeedbackPublicSwitch\n row={row}\n isAdmin={canEdit}\n viewerId={viewerId}\n onChange={onPublicChange}\n pending={pending}\n commit={commit}\n className=\"mt-3\"\n />\n )}\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-row-detail.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { AuthedImage } from \"../components/authed-image\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { TextLink } from \"../components/text-link\";\nimport { toast } from \"../components/toast\";\nimport { Button } from \"../components/button\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport type { PathPattern } from \"../landing/routing\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport { DEFAULT_MAX_ATTACHMENT_BYTES } from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackNoteEditor,\n FeedbackProse,\n FeedbackStatusTransitions,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-inbox\";\nimport type { FeedbackCategory, FeedbackNoteAttachment, FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_REWORKABLE_STATUSES,\n appendRework,\n isImageAttachment,\n replaceDescription,\n splitBodyAttachments,\n splitDescription,\n} from \"./feedback-record\";\nimport type { FeedbackContext, FeedbackRecord } from \"./feedback-record\";\nimport { diagnosticsEntries, FeedbackDiagnosticsList } from \"./feedback-diagnostics\";\nimport { runMutation } from \"./feedback-mutation\";\nimport { feedbackPage, feedbackPageLinkTarget } from \"./feedback-page-link\";\nimport { useFeedbackStatusUndo } from \"./feedback-status-undo\";\nimport type { FeedbackStatusChange } from \"./feedback-status-undo\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\nimport { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from \"./feedback-replies\";\nimport { FeedbackAttachmentDownload, plainFeedbackFetch } from \"./feedback-attachment-download\";\nimport { FeedbackPublicSwitch } from \"./feedback-public-switch\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * One report, opened — the panel a feedback table unfolds under its row (§4.4 of\n * docs/feedback-harmonization.md), lifted from keksdose's `FeedbackRow`\n * (`frontend/src/features/feedback/feedback-page.tsx:329-612`) with the contract's\n * changes applied.\n *\n * Until 0.27 each app wrote this panel itself out of the kit's shell\n * (`FeedbackDetail` / `FeedbackDetailSection` / `FeedbackNoteEditor`), and the three\n * copies had drifted in exactly the places a user notices:\n *\n * - **the description editor held the raw body** — keksdose `initial={fb.body}`\n * (`:420`), kastlan the same (`pages/feedback-page.tsx:226`) — so an author fixing a\n * typo could delete the `--- REWORK … ---` rounds and the `[screenshot]` file lines\n * with it, and the history was gone. Here the editor holds the ORIGINAL description\n * only ({@link splitDescription}) and saves through {@link replaceDescription}, which\n * puts every appended block and file line back unchanged;\n * - **kastlan's rework sent `status: \"OPEN\"`** from the client (`:189-195`) and offered it\n * on three statuses; keksdose sends the body alone and lets the server reopen the row\n * on five (live #331). The contract is keksdose's (§3.4) — {@link appendRework}, one\n * file, the server reopens;\n * - **kastlan showed only `screenshot_url`** (`:265-276`) and no downloads; keksdose the\n * screenshot, the other files and the rework pictures, a PDF as a download (dev #578);\n * - **the URL fell away on Kurvenschmiede's rows**, whose backfilled `context.url` is a\n * path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section\n * vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).\n *\n * The sections, in the contract's order: **Subject** (the category and the title, §8.1;\n * `showSubject={false}` leaves it out), **Description** (the body; the author's\n * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Replies** (0.38,\n * §9.7: the timeline, the admin's reply composer with its status picker and the nudge,\n * Rework — on a row without `replies`, a server before 0.38, the frozen\n * **Outcome** read-only in its place), **Comments** (0.38: the app's\n * `FeedbackCommentThread`, when passed), **Send for rework** (only while reworking) and\n * **Status** (with the **Visible to others** switch when `onPublicChange` is passed).\n *\n * The app keeps its data and its API: the row it renders, the PATCH (`onUpdate`), the\n * upload (`onUpload`), the authenticated GET (`fetcher`) and who is looking (`canEdit`,\n * `viewerId`). Every word is the kit's `feedbackDetail` namespace.\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/**\n * `feedbackDetail` — the row detail's words (§4.4). English: keksdose's `en.json`\n * (`feedback.*`, `common.*`, `more.copy_url`) with the contract's changes; the de-CH\n * canon (keksdose `de-CH.json`, ss never ß, \"Nacharbeit\" throughout — §7.14) on each key.\n */\nexport interface FeedbackDetailLabels {\n /** The first section's heading: what the report is called (feedback contract §8.1,\n * keksdose live #395 — with the row expanded and the table scrolled, nothing said\n * which entry was being answered). de-CH: \"Betreff\" */\n subject: string;\n /** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks\n * \"What happened? (optional)\"; the description, once filed, is headed as what it is\n * — and as the author's \"Edit description\" calls it. de-CH: \"Beschreibung\" */\n body: string;\n /** The author's button on that heading. de-CH: \"Bearbeiten\" */\n edit: string;\n /** Its accessible name — says WHAT is edited; contains the visible word.\n * de-CH: \"Beschreibung bearbeiten\" */\n editDescription: string;\n /** The editors' save button (description, outcome). de-CH: \"Speichern\" */\n save: string;\n /** Every editor's cancel button. de-CH: \"Abbrechen\" */\n cancel: string;\n /** Section 2's heading. de-CH: \"URL\" */\n url: string;\n /** The copy button beside the link — copies the FULL `context.url`.\n * de-CH: \"URL kopieren\" */\n copyUrl: string;\n /** The heading over `context.diagnostics`, between the URL and the attachment (keksdose\n * live #481). de-CH: \"Diagnose\" */\n diagnostics: string;\n /** Section 3's heading, and each picture's alt text. de-CH: \"Anhang\" */\n attachment: string;\n /** A PDF's / text file's download button, given the file's name.\n * de-CH: \"{{name}} herunterladen\" */\n download: (name: string) => string;\n /** The toast when that download failed. de-CH: \"Der Anhang konnte nicht\n * heruntergeladen werden.\" */\n downloadFailed: string;\n /** Section 4's heading on a row without `replies` (a server before 0.38), where the\n * frozen outcome is shown read-only. de-CH: \"Ergebnis\" */\n outcome: string;\n /** Section 4's heading since 0.38: the replies' timeline (§9.7). de-CH: \"Antworten\" */\n replies: string;\n /** On section 4's heading line once the row is settled, given the formatted\n * `resolved_at` date. de-CH: \"Erledigt am: {{date}}\" */\n resolvedAt: (date: string) => string;\n /** The button that opens section 5 on an answered row. de-CH: \"Nacharbeit\" */\n rework: string;\n /** Section 5's heading. de-CH: \"Zur Nacharbeit senden\" */\n reworkTitle: string;\n /** Section 5's send button. de-CH: \"Nacharbeit senden\" */\n reworkSend: string;\n /** The line above the rework note. de-CH: \"Was muss noch angepasst werden? Neue\n * Anforderungen oder Richtungswechsel.\" */\n reworkPlaceholder: string;\n /** The toast when the rework's file did not upload — and so nothing was sent. Names\n * no screenshot: it fires for a PDF or a log too (§7.14; keksdose's said\n * \"screenshot\"). de-CH: \"Die Datei konnte nicht hochgeladen werden. Die Nacharbeit\n * wurde nicht gesendet.\" */\n reworkUploadFailed: string;\n /** Section 6's heading. de-CH: \"Status\" */\n status: string;\n /** A `FeedbackNoteEditor`'s box, named when no `placeholder` line names it and the\n * caller gives no `label` — without it the box was read out as just \"edit text\"\n * (round 0.34, the showcase's axe check). de-CH: \"Notiz\" */\n note: string;\n}\n\n// Defined in ./feedback-row-detail-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DETAIL_LABELS };\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: LabelOverride<FeedbackDetailLabels>): FeedbackDetailLabels {\n return useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);\n}\n\n/* ── The page a report was filed on (§3.2, §4.4.2) ────────────────────────── */\n\n// The page as the reporter had it (path + query + hash) and the full URL \"Copy URL\"\n// copies were public too, as `feedbackPageHref` and `feedbackPageUrl`, until 0.53 (deprecated\n// in 0.52, API-4): `feedbackPage` (./feedback-page-link) is the one reading, and the kit\n// links only its path.\n\n/** The page's path alone — the compact text a URL column shows (§4.3), and the only part\n * of the page the kit links. */\nexport function feedbackPagePath(context: FeedbackContext | null | undefined): string | null {\n return feedbackPage(context)?.path ?? null;\n}\n\n/* ── Attachments (§3.5, §4.4.3) ───────────────────────────────────────────── */\n\n/**\n * Every file a report carries, in the order the detail shows them (§4.4.3): the\n * screenshot it was filed with, then its other files, then whatever the rework rounds\n * brought, in body order (keksdose dev #578, live #291). One list rather than three —\n * a reader looking for \"the picture\" should not have to know at which point in the\n * conversation it was taken.\n *\n * Each URL once: keksdose and Kurvenschmiede content-address their keys, so the same\n * screenshot pasted twice is one URL — shown twice it is the same picture twice (and,\n * keyed by URL, a React key clash keksdose had).\n */\nexport function feedbackAttachmentUrls(\n row: Pick<FeedbackRecord, \"screenshot_url\" | \"attachment_urls\" | \"body\"> & Partial<Pick<FeedbackRecord, \"replies\">>,\n): string[] {\n const urls = [\n ...(row.screenshot_url ? [row.screenshot_url] : []),\n // `?? []`: NULL on rows filed before the column, and no key at all from a server\n // that predates it (keksdose dev #578).\n ...(row.attachment_urls ?? []),\n ...splitBodyAttachments(row.body).urls,\n // 0.40 (§10.3): a rework entry's one file, in entry order — where the rework\n // pictures of the body were before.\n ...(row.replies ?? []).flatMap((reply) =>\n reply.kind === \"rework\" && reply.attachment_url ? [reply.attachment_url] : [],\n ),\n ];\n return [...new Set(urls)];\n}\n\n/* ── Who may do what (§3.4, §4.3, §4.4) ───────────────────────────────────── */\n\n/** What the person looking at a row may do with it — {@link feedbackRowAccess}. */\nexport interface FeedbackRowAccess {\n /** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */\n isAuthor: boolean;\n /** \"Edit description\": the author, while the row is OPEN, READY or IN_PROGRESS — on either\n * page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */\n canEditDescription: boolean;\n /** The admin on the inbox (`canEdit`): since 0.38 the reply composer (the outcome\n * itself is frozen, §9.7). */\n canEditOutcome: boolean;\n /** \"Rework\": the author, on an answered row ({@link FEEDBACK_REWORKABLE_STATUSES}).\n * Since 0.40 (§10.2 decision 6) the author's only — an admin sends a report back with\n * a reply whose status is `REWORK`; the server answers anyone else 403. */\n canRework: boolean;\n /** The status pills: the admin on the inbox; everyone else sees them read-only. */\n canChangeStatus: boolean;\n}\n\n/**\n * The detail's permission rules in one place — the same answers the server gives\n * (§3.4), so the panel never offers what the PATCH would refuse.\n *\n * `canEdit` is the app's \"admin on `/feedback`\": an admin on `/my-feedback` edits no\n * status and no outcome there (§4.3), but as the author still edits the description\n * and sends for rework. `viewerId` is the signed-in user's id.\n */\nexport function feedbackRowAccess(\n row: Pick<FeedbackRecord, \"user_id\" | \"status\">,\n { canEdit, viewerId }: { canEdit: boolean; viewerId?: number | null },\n): FeedbackRowAccess {\n // `user_id: null` is an erased author (keksdose live #275): nobody is that author,\n // least of all a viewer whose id happens to be missing too.\n const isAuthor = row.user_id !== null && viewerId !== undefined && viewerId !== null && viewerId === row.user_id;\n return {\n isAuthor,\n canEditDescription: isAuthor && FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status),\n canEditOutcome: canEdit,\n canRework: isAuthor && FEEDBACK_REWORKABLE_STATUSES.includes(row.status),\n canChangeStatus: canEdit,\n };\n}\n\n/* ── The PATCH ─────────────────────────────────────────────────────────────── */\n\n/** What the detail sends as `PATCH /feedback/{id}` (§3.4): the body (a description\n * edit, a rework append) or the outcome — and, through {@link useFeedbackStatusUndo}\n * when no `statusChange` is given, the status. Never two at once: a rework PATCH must\n * carry the body ALONE for the server to read it as an append (keksdose live #331). */\nexport interface FeedbackDetailPatch {\n id: number;\n body?: string;\n /** `null` clears it — a real edit. **Frozen since 0.38** (§9.7): server-kit 0.11\n * answers 422; the detail no longer sends it (a reply is written instead). */\n outcome?: string | null;\n status?: FeedbackStatus;\n /** 0.38.1: the **Visible to others** flip (§9.3) — what `onPublicChange`'s\n * `PATCH /feedback/{id} {\"public\": next}` carries, alone, so an app can send it\n * through the same `onUpdate` mutation. */\n public?: boolean;\n /** 0.57: the category (§3.4: the admin's, any time; never CRASH) — what the inbox's bulk\n * bar sends, alone, through the same mutation (`FeedbackInbox`, §13.6). The detail\n * itself never sends it. */\n category?: FeedbackCategory;\n}\n\n/**\n * The page's update mutation (0.52): **TanStack Query's `mutateAsync`** — pass\n * `update.mutateAsync` (and `pending={update.isPending}`), the same function\n * `useFeedbackStatusUndo` takes. An editor closes when the promise resolves, which it does\n * for every call — also when a status pill was pressed while the save was in flight\n * (refactor audit 2026-10-30, FLW-3). A hand-written one resolves once the PATCH has\n * landed. A failure is the mutation's own to toast — the server's `detail`, else\n * `feedbackToast.updateFailed` (§4.4) — and leaves the editor open with its draft.\n */\nexport type FeedbackDetailUpdateAsync = (patch: FeedbackDetailPatch) => Promise<unknown>;\n\n/** `POST /feedback/attachments` (§3.3): one file up, its URL\n * (`/api/v1/feedback/attachments/<key>`) back. Reject on failure. */\nexport type FeedbackUpload = (file: File) => Promise<string>;\n\n/* ── Send for rework (§4.4.5) ─────────────────────────────────────────────── */\n\n/** 0.40 (§10.3): `POST /feedback/{id}/rework` — the trimmed note and the uploaded file's\n * URL. Return the promise: the section closes when it resolves and keeps the note when\n * it rejects (the toast is the app's: the server's `detail`, else\n * `feedbackToast.updateFailed`). */\nexport type FeedbackReworkSend = (note: string, attachmentUrl?: string) => void | Promise<unknown>;\n\nexport interface FeedbackReworkSectionProps {\n /** The row being sent back: its id, and the body the round is appended to. */\n row: Pick<FeedbackRecord, \"id\" | \"body\">;\n /** 0.40: the rework as an entry of its own — see {@link FeedbackReworkSend}. With it,\n * `onUpdate` is not called. */\n onRework?: FeedbackReworkSend;\n /** The PATCH — see {@link FeedbackDetailUpdateAsync}. Called with `{ id, body }` only,\n * and only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still\n * turns into an entry for one release. Optional since 0.40; pass one of the two. */\n onUpdate?: FeedbackDetailUpdateAsync;\n /** The PATCH is in flight (`update.isPending`). The upload adds its own. */\n pending?: boolean;\n /** The upload. Left out, the note goes without a file field. */\n onUpload?: FeedbackUpload;\n /** Snapshot the app view as a File — the kit's `captureAppScreenshot`, or the app's\n * own. A \"Capture screenshot\" button appears only with it; a rejection (no\n * `modern-screenshot`, a canvas that would not draw) toasts\n * `feedbackToast.captureFailed` and adds nothing. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The round landed — the server has the note and, on an answered row, reopened it. */\n onSent?: () => void;\n onCancel: () => void;\n /** Over the field's defaults: the contract's types and 10 MB (§3.5), the\n * `feedbackToast` refusals, the `feedbackAttachment` words. */\n attachment?: Partial<FeedbackNoteAttachment>;\n /** The send COMMITS — see `FeedbackNoteEditor`'s `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter sends an answered report\n * back with a note and at most one file, and the server reopens it (§3.4, keksdose\n * `submitRework`, `feedback-page.tsx:350`).\n *\n * - **The note is required** and trimmed: Send stays disabled on a blank box (a file\n * alone cannot be sent; keksdose `:577` dropped that click silently).\n * - **One file**, added, captured or pasted — the dialog's types and 10 MB. It is\n * uploaded FIRST and awaited: a body naming a file that never arrived would be a\n * rework whose evidence is missing, and the upload is the half that fails on its\n * own. If it fails nothing is sent and the toast says so; the note stays in the box.\n * - **0.40: an entry of its own** (§10.3) — with `onRework`, the note and the file's URL\n * go to `POST /feedback/{id}/rework` and the body is not touched. Without it, the\n * legacy append below, which server-kit 0.13 still converts (one minor).\n * - **The body alone goes in the PATCH** — {@link appendRework}'s strict extension of\n * the current body, which is how the server recognises the append and reopens the\n * row. Never a `status` (kastlan's client sent `OPEN`; an author may not write one,\n * so the server refused the whole PATCH, note and all — keksdose live #331).\n *\n * Exported on its own for an app that offers rework somewhere other than the row\n * detail. Kurvenschmiede's author verdict (\"That is it, done\") is NOT here: an answered\n * item stays answered unless its author sends it back (§7.2).\n */\nexport function FeedbackReworkSection({\n row,\n onRework,\n onUpdate,\n pending = false,\n onUpload,\n onCaptureScreenshot,\n onSent,\n onCancel,\n attachment,\n commit,\n labels: labelsProp,\n}: FeedbackReworkSectionProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n const toasts = useFeedbackToastLabels();\n const [uploading, setUploading] = useState(false);\n const [sending, setSending] = useState(false);\n // The body as it is NOW, not as it was when Send was pressed: the list can refresh\n // while the file uploads, and an append to a stale body is no longer a strict\n // extension of the stored one — the server would read it as an edit, and refuse it.\n const latestBody = useRef(row.body);\n useEffect(() => {\n latestBody.current = row.body;\n }, [row.body]);\n\n const capture = onCaptureScreenshot\n ? async () => {\n try {\n return await onCaptureScreenshot();\n } catch {\n toast.error(toasts.captureFailed);\n return null;\n }\n }\n : undefined;\n\n const send = async (value: string, file?: File | null) => {\n const note = value.trim();\n if (!note) return;\n let url: string | null = null;\n if (file && onUpload) {\n setUploading(true);\n try {\n url = await onUpload(file);\n } catch {\n toast.error(labels.reworkUploadFailed);\n return;\n } finally {\n setUploading(false);\n }\n }\n if (onRework) {\n // 0.40 (§10.3): the entry, not the body — the server checks the URL (a 422 outside\n // the app's attachment prefix).\n setSending(true);\n try {\n await onRework(note, url ?? undefined);\n } catch {\n return; // The app's toast; the note stays in the box.\n } finally {\n setSending(false);\n }\n onSent?.();\n return;\n }\n if (!onUpdate) return;\n let body: string | null;\n try {\n body = appendRework(latestBody.current, note, url);\n } catch {\n // The upload answered with something that is not an attachment URL of this\n // contract — written anyway it would sit in the body as prose no reader knows to\n // be a file. As far as the person is concerned, the file did not arrive.\n toast.error(labels.reworkUploadFailed);\n return;\n }\n if (body === null) return;\n runMutation(onUpdate, { id: row.id, body }, () => onSent?.());\n };\n\n return (\n <FeedbackDetailSection title={labels.reworkTitle}>\n <FeedbackNoteEditor\n key={row.id}\n initial=\"\"\n required\n commit={commit}\n pending={pending || uploading || sending}\n onSave={(value, file) => void send(value, file)}\n onCancel={onCancel}\n saveLabel={labels.reworkSend}\n cancelLabel={labels.cancel}\n placeholder={labels.reworkPlaceholder}\n attachment={\n onUpload\n ? {\n accept: FEEDBACK_ATTACHMENT_ACCEPT,\n maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,\n onError: (kind) => {\n toast.error(kind === \"type\" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);\n },\n onCaptureScreenshot: capture,\n ...attachment,\n }\n : undefined\n }\n />\n </FeedbackDetailSection>\n );\n}\n\n/* ── The detail (§4.4) ─────────────────────────────────────────────────────── */\n\nexport interface FeedbackRowDetailProps {\n /** The report — the list's own row, as `GET /feedback` / `GET /feedback/my` sent it. */\n row: FeedbackRecord;\n /** The admin on the inbox (`/feedback`): status pills and the outcome editor. `false`\n * on `/my-feedback`, for an admin too (§4.3). */\n canEdit: boolean;\n /** The signed-in user's id — who the AUTHOR is: the description editor and rework. */\n viewerId?: number | null;\n /** The page's PATCH — `update.mutateAsync` (see {@link FeedbackDetailUpdateAsync}). The\n * callback form, `update.mutate`, went in 0.53 (deprecated in 0.52). */\n onUpdate: FeedbackDetailUpdateAsync;\n /** `update.isPending`: the editors' save buttons wait for it. */\n pending?: boolean;\n /** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with\n * `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go\n * through it, since the attachment route wants the bearer token (§3.5). Left out,\n * the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /**\n * The undoable status change — what `useFeedbackStatusUndo(update.mutateAsync)` returns,\n * the SAME one the table's status cell and the swipes use (§4.5). Left out, the\n * detail builds one from `onUpdate`, which is equivalent as long as `onUpdate` is the\n * page's mutation (its observer outlives the row a status filter removes).\n */\n statusChange?: FeedbackStatusChange;\n /** For the rework's and (0.47, with `replyAttachments`) the reply composer's \"Capture screenshot\" — see\n * {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,\n * over the contract's defaults. */\n reworkAttachment?: Partial<FeedbackNoteAttachment>;\n /** How the `resolved_at` date reads — the app's date cell, as a string. Default\n * (0.52): the `<UiKitProvider formatDate>` when the app set one, else the kit's\n * `formatDate` in the provider's locale and time zone, `medium` style. */\n renderDate?: (iso: string) => string;\n /**\n * 0.52 (FLW-7): the app's one-shot routes, ON TOP of the kit's — `/join`,\n * `/register`, `/verify-email`, `/reset-password` (a string is that path and everything\n * under it, a RegExp is tested on the path, as `useLastVisitedPage`'s `exclude`). The\n * URL section shows such a page as text with Copy, never as a link: it acts on load,\n * and a report's page is the reporter's to write. Any other page links its PATH alone;\n * the query, where tokens and `?next=` targets travel, is text. keksdose:\n * `[\"/banks/callback\", \"/share-target\"]`. Keep it stable (module scope).\n */\n pageLinkExclude?: readonly PathPattern[];\n /**\n * The detail's saves COMMIT (description, outcome, rework, status): under a locked\n * `WriteLockProvider` each is `aria-disabled` with the lock's reason, and nothing\n * reaches `onUpdate`. Off by default — keksdose's shell lock is a read-only demo\n * BUDGET, and a person viewing one still owns their own reports.\n */\n commit?: CommitScope;\n /**\n * Open with the subject: the category badge and the title (feedback contract §8.1,\n * keksdose live #395). On by default; `false` for a page that already shows the title\n * beside the detail.\n */\n showSubject?: boolean;\n /**\n * 0.38: `POST /feedback/{id}/replies` (§9.7) — the body and the status picked\n * (`undefined`: \"No change\"). With `canEdit` and a row that carries `replies`, the\n * reply composer stands under the timeline. Return the promise: the box clears when it\n * resolves. A failure is the app's to toast — the server's `detail`, else\n * `feedbackToast.replyFailed` (0.38.1).\n *\n * 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the\n * composer takes one file as the rework note does — pasted into the box, captured or\n * picked — uploaded first, and its URL is the third argument (`attachment_url` in the\n * body; server-kit 0.18).\n */\n onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;\n /**\n * 0.47 (§12): the reply composer takes one file — through `onUpload`,\n * `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:\n * `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of\n * `(body, status)` does not grow a file field whose file its reply would drop (round\n * 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes\n * `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an\n * older server answers the field with a 422).\n */\n replyAttachments?: boolean;\n /** 0.38: the reply is in flight. */\n replying?: boolean;\n /**\n * 0.40 (§10.3): `POST /feedback/{id}/rework` — the author's rework as an entry of its\n * own (see {@link FeedbackReworkSend}). The **Rework** button shows only to the author\n * on an answered row, and only where the reply composer is not shown (§10.2 decision 6:\n * an admin answering a report sends it back with a reply whose status is `REWORK`).\n * Left out, the rework goes through `onUpdate` as the legacy body append — for one\n * minor, while the server converts it.\n */\n onRework?: FeedbackReworkSend;\n /** 0.38: the report's comments — the app's `FeedbackCommentThread` — in a section of\n * their own under the replies. A thread here draws the comments alone (0.38.1): the\n * Replies section above has the replies, so whatever `replies` the thread is handed\n * are dropped, and nothing is drawn twice. */\n comments?: ReactNode;\n /** 0.38: `PATCH /feedback/{id} {\"public\": next}` — draws the **Visible to others**\n * switch (`FeedbackPublicSwitch`) in the Status section, for the admin (`canEdit`)\n * and, within §9.3's window, the author. The page's own mutation fits:\n * `onPublicChange={(next) => update.mutate({ id: row.id, public: next })}` —\n * {@link FeedbackDetailPatch}'s `public` (0.38.1). */\n onPublicChange?: (next: boolean) => void;\n labels?: LabelOverride<FeedbackDetailLabels>;\n className?: string;\n}\n\n/**\n * The expanded row of a feedback table (§4.4): what was reported, where, with which\n * files, what the answer was, and where it stands — with the edits each viewer may\n * make. Hand it to `DataTable`'s `expandedRow`:\n *\n * ```tsx\n * const update = useMutation({ mutationFn: ({ id, ...patch }) => api.updateFeedback(id, patch),\n * onSuccess: invalidate, onError: (e) => toast.error(detailOf(e) ?? toastLabels.updateFailed) });\n * const changeStatus = useFeedbackStatusUndo(update.mutateAsync);\n * <DataTable\n * isExpanded={(row) => expandedId === row.id}\n * expandedRow={(row) => (\n * <FeedbackRowDetail row={row} canEdit={isAdmin && !mine} viewerId={user?.id}\n * onUpdate={update.mutateAsync} pending={update.isPending} statusChange={changeStatus}\n * onUpload={api.uploadFeedbackAttachment} fetcher={authedImageFetcher}\n * onCaptureScreenshot={captureAppScreenshot} />\n * )} … />\n * ```\n *\n * Mounted only while its row is expanded, so which editor is open lives here, and\n * collapsing the row closes it. Each editor is keyed by the row's id, so a draft is\n * never thrown away by a list refresh under it.\n */\nexport function FeedbackRowDetail({\n row,\n canEdit,\n viewerId,\n onUpdate,\n pending = false,\n onUpload,\n fetcher,\n statusChange,\n onCaptureScreenshot,\n reworkAttachment,\n renderDate,\n pageLinkExclude,\n commit,\n showSubject = true,\n onReply,\n replyAttachments = false,\n replying = false,\n onRework,\n comments,\n onPublicChange,\n labels: labelsProp,\n className,\n}: FeedbackRowDetailProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n // \"placeholder = the dialog's body label\" (§4.4.1): read from the dialog's own\n // namespace, so the two can never say different things.\n const dialogText = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS);\n const ownStatusChange = useFeedbackStatusUndo(onUpdate);\n const changeStatus = statusChange ?? ownStatusChange;\n const [editingBody, setEditingBody] = useState(false);\n const [reworking, setReworking] = useState(false);\n\n const access = feedbackRowAccess(row, { canEdit, viewerId });\n // The body as written, without its file lines (they are shown as files below) — the\n // rework rounds stay in the prose: they are what was said after the answer.\n const bodyText = splitBodyAttachments(row.body).text;\n const { description } = splitDescription(row.body);\n const page = feedbackPage(row.context);\n const pageLink = page && feedbackPageLinkTarget(page, pageLinkExclude);\n // What the app attached is for whoever handles the report and whoever sent it, never\n // for another viewer of a shared row (keksdose live #481).\n const diagnostics = canEdit || access.isAuthor ? diagnosticsEntries(row.context?.diagnostics) : [];\n // 0.47 (§12): with the entries, each entry's file is drawn inline in the Replies\n // timeline, so the Attachment section holds the report's own files.\n const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: undefined } : row);\n const fetchFile = fetcher ?? plainFeedbackFetch;\n // A date in a sentence (\"Resolved: …\"), so no weekday — through the kit's one day\n // ladder, which reaches the provider's `formatDate` (0.52, refactor audit 2026-10-30,\n // DUP-2).\n const day = useDayFormatter<string>(renderDate);\n const dateText = (iso: string) => day(iso);\n\n const saveDescription = (value: string) => {\n // Only a real change makes a round trip; clearing it is one (\"\" — the description\n // is optional, §7.4), and the rounds and file lines go back unchanged.\n if (value.trim() === description.trim()) {\n setEditingBody(false);\n return;\n }\n runMutation(onUpdate, { id: row.id, body: replaceDescription(row.body, value) }, () => setEditingBody(false));\n };\n\n // 0.38 (§9.7): the replies replace the outcome; a row without them is a server before\n // 0.38, whose frozen outcome is shown as it is, read-only.\n const replies = row.replies;\n const commentLabels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS);\n // 0.40 (§10.2 decision 6): whoever has the reply composer answers with it — REWORK is\n // one of its statuses — so the Rework button is for an author without it.\n const composerShown = Boolean(replies && access.canEditOutcome && onReply);\n const canRework = access.canRework && !composerShown;\n const showRework = canRework && reworking;\n\n return (\n <FeedbackDetail className={className}>\n {showSubject && (\n <FeedbackDetailSection title={labels.subject}>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm\" data-feedback-subject=\"\">\n <FeedbackCategoryBadge category={row.category} />\n <span className=\"min-w-0 font-medium text-primary [overflow-wrap:anywhere]\">{row.title}</span>\n </p>\n </FeedbackDetailSection>\n )}\n <FeedbackDetailSection\n title={labels.body}\n action={\n access.canEditDescription && !editingBody ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingBody(true);\n }}\n aria-label={labels.editDescription}\n >\n {labels.edit}\n </Button>\n ) : undefined\n }\n >\n {access.canEditDescription && editingBody ? (\n <FeedbackNoteEditor\n key={row.id}\n // The ORIGINAL description only — see the module note.\n initial={description}\n pending={pending}\n commit={commit}\n onSave={saveDescription}\n onCancel={() => setEditingBody(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={dialogText.bodyOptional ?? dialogText.body}\n />\n ) : (\n <FeedbackProse>{bodyText.trim() ? bodyText : undefined}</FeedbackProse>\n )}\n </FeedbackDetailSection>\n\n {page && (\n // The page it was filed on, with the full URL a click away (keksdose #278). Only\n // the path is a link; the query and hash, the reporter's to write, are text — and\n // a one-shot path is text too (FLW-7, 0.52 code review: see feedback-page-link).\n <FeedbackDetailSection title={labels.url}>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <code\n className=\"min-w-0 break-all text-xs text-secondary\"\n data-feedback-page=\"\"\n data-feedback-page-unlinked={pageLink ? undefined : \"\"}\n >\n {pageLink ? (\n <TextLink href={pageLink} tone=\"secondary\" underline=\"hover\" stopPropagation>\n {page.path}\n </TextLink>\n ) : (\n page.path\n )}\n {page.href.slice(page.path.length)}\n </code>\n <CopyButton size=\"xs\" className=\"shrink-0\" text={page.url} label={labels.copyUrl} stopPropagation />\n </div>\n </FeedbackDetailSection>\n )}\n\n {diagnostics.length > 0 && (\n <FeedbackDetailSection title={labels.diagnostics}>\n <FeedbackDiagnosticsList entries={diagnostics} />\n </FeedbackDetailSection>\n )}\n\n {files.length > 0 && (\n <FeedbackDetailSection title={labels.attachment}>\n <div className=\"flex min-w-0 flex-wrap items-start gap-2\">\n {files.map((url) =>\n isImageAttachment(url) ? (\n <AuthedImage\n key={url}\n src={url}\n alt={labels.attachment}\n fetcher={fetchFile}\n link\n stopPropagation\n // A broken attachment is simply not in the row (keksdose's\n // AuthedImage default) rather than a grey tile per failure.\n errorFallback={null}\n wrapperClassName=\"inline-block max-w-full\"\n className=\"max-h-64 max-w-full rounded border border-subtle\"\n />\n ) : (\n <FeedbackAttachmentDownload key={url} url={url} fetcher={fetchFile} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={replies ? labels.replies : labels.outcome}\n // When the row entered DONE / WONT_DO (keksdose feedback #96).\n action={\n row.resolved_at ? (\n <span className=\"text-xs text-muted\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n <div className=\"space-y-3\">\n {replies ? (\n <FeedbackReplyTimeline replies={replies} fetcher={fetchFile} />\n ) : (\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n )}\n {composerShown && replies && onReply && (\n <FeedbackReplyComposer\n key={row.id}\n status={row.status}\n onReply={onReply}\n pending={replying}\n onUpload={replyAttachments ? onUpload : undefined}\n onCaptureScreenshot={replyAttachments ? onCaptureScreenshot : undefined}\n attachment={replyAttachments ? reworkAttachment : undefined}\n suggestedStatus={feedbackReplyNudge(row)}\n commit={commit}\n />\n )}\n {/* No \"Open page\" button here since 0.46 (keksdose live #449): the URL section\n above already links the page, with the path on screen. */}\n {canRework && !reworking && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setReworking(true);\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden /> {labels.rework}\n </Button>\n </div>\n )}\n </div>\n </FeedbackDetailSection>\n\n {comments != null && (\n <FeedbackDetailSection title={commentLabels.comments}>\n <FeedbackRepliesDrawnAboveContext.Provider value>{comments}</FeedbackRepliesDrawnAboveContext.Provider>\n </FeedbackDetailSection>\n )}\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\n onRework={onRework}\n onUpdate={onUpdate}\n pending={pending}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n onSent={() => setReworking(false)}\n onCancel={() => setReworking(false)}\n attachment={reworkAttachment}\n commit={commit}\n labels={labelsProp}\n />\n )}\n\n <FeedbackDetailSection title={labels.status}>\n {/* Expanded: the whole enum in one click for the admin (keksdose feedback #421);\n everyone else the narrow set, read-only — a picture of the workflow. */}\n <FeedbackStatusTransitions\n status={row.status}\n statuses={\n access.canChangeStatus ? selectableFeedbackStatuses(row.status) : visibleFeedbackStatuses(row.status)\n }\n canEdit={access.canChangeStatus}\n commit={commit}\n onPick={(status) => changeStatus(row, status)}\n variant=\"pill\"\n />\n {onPublicChange && (\n <FeedbackPublicSwitch\n row={row}\n isAdmin={canEdit}\n viewerId={viewerId}\n onChange={onPublicChange}\n pending={pending}\n commit={commit}\n className=\"mt-3\"\n />\n )}\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
@@ -18,7 +18,16 @@ const DEFAULT_FEEDBACK_PAGE_LABELS = {
18
18
  environment: (environment) => String(environment ?? "").toUpperCase(),
19
19
  reworkChip: (count) => count === 1 ? "Rework" : `Rework \xD7${count}`,
20
20
  awaitingFilter: "Only what is waiting for you",
21
- phoneActions: "Feedback actions"
21
+ phoneActions: "Feedback actions",
22
+ bulkStatus: "Set status",
23
+ bulkCategory: "Set category",
24
+ bulkVisibility: "Visibility",
25
+ bulkPublicOn: "Visible to others",
26
+ bulkPublicOff: "Not visible to others",
27
+ bulkChanged: (count, value) => count === 1 ? `Set 1 report to \u201C${value}\u201D` : `Set ${count} reports to \u201C${value}\u201D`,
28
+ bulkFailed: (count) => count === 1 ? "1 report could not be changed and stays selected." : `${count} reports could not be changed and stay selected.`,
29
+ bulkRestored: (count) => count === 1 ? "1 report is back as it was" : `${count} reports are back as they were`,
30
+ bulkRestoreFailed: (count) => count === 1 ? "1 report could not be put back." : `${count} reports could not be put back.`
22
31
  };
23
32
  export {
24
33
  DEFAULT_FEEDBACK_PAGE_LABELS
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-table-labels.ts"],"sourcesContent":["// The English words of ./feedback-table, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./feedback-table re-exports them.\n\nimport type { FeedbackPageLabels } from \"./feedback-table\";\n\nexport const DEFAULT_FEEDBACK_PAGE_LABELS: FeedbackPageLabels = {\n title: \"Feedback inbox\",\n myTitle: \"My feedback\",\n columnId: \"#\",\n columnDate: \"Date\",\n columnCategory: \"Category\",\n columnSubject: \"Subject\",\n columnUser: \"User\",\n columnEmail: \"Email\",\n columnUrl: \"URL\",\n columnStatus: \"Status\",\n columnResolved: \"Resolved\",\n openPage: \"Open page\",\n empty: \"None\",\n deletedUser: \"<deleted user>\",\n userFallback: (id) => `user #${id}`,\n environment: (environment) => String(environment ?? \"\").toUpperCase(),\n reworkChip: (count) => (count === 1 ? \"Rework\" : `Rework ×${count}`),\n awaitingFilter: \"Only what is waiting for you\",\n phoneActions: \"Feedback actions\",\n};\n"],"mappings":";AAMO,MAAM,+BAAmD;AAAA,EAC9D,OAAO;AAAA,EACP,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc,CAAC,OAAO,SAAS,EAAE;AAAA,EACjC,aAAa,CAAC,gBAAgB,OAAO,eAAe,EAAE,EAAE,YAAY;AAAA,EACpE,YAAY,CAAC,UAAW,UAAU,IAAI,WAAW,cAAW,KAAK;AAAA,EACjE,gBAAgB;AAAA,EAChB,cAAc;AAChB;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-table-labels.ts"],"sourcesContent":["// The English words of ./feedback-table, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./feedback-table re-exports them.\n\nimport type { FeedbackPageLabels } from \"./feedback-table\";\n\nexport const DEFAULT_FEEDBACK_PAGE_LABELS: FeedbackPageLabels = {\n title: \"Feedback inbox\",\n myTitle: \"My feedback\",\n columnId: \"#\",\n columnDate: \"Date\",\n columnCategory: \"Category\",\n columnSubject: \"Subject\",\n columnUser: \"User\",\n columnEmail: \"Email\",\n columnUrl: \"URL\",\n columnStatus: \"Status\",\n columnResolved: \"Resolved\",\n openPage: \"Open page\",\n empty: \"None\",\n deletedUser: \"<deleted user>\",\n userFallback: (id) => `user #${id}`,\n environment: (environment) => String(environment ?? \"\").toUpperCase(),\n reworkChip: (count) => (count === 1 ? \"Rework\" : `Rework ×${count}`),\n awaitingFilter: \"Only what is waiting for you\",\n phoneActions: \"Feedback actions\",\n bulkStatus: \"Set status\",\n bulkCategory: \"Set category\",\n bulkVisibility: \"Visibility\",\n bulkPublicOn: \"Visible to others\",\n bulkPublicOff: \"Not visible to others\",\n bulkChanged: (count, value) => (count === 1 ? `Set 1 report to “${value}”` : `Set ${count} reports to “${value}”`),\n bulkFailed: (count) =>\n count === 1\n ? \"1 report could not be changed and stays selected.\"\n : `${count} reports could not be changed and stay selected.`,\n bulkRestored: (count) => (count === 1 ? \"1 report is back as it was\" : `${count} reports are back as they were`),\n bulkRestoreFailed: (count) =>\n count === 1 ? \"1 report could not be put back.\" : `${count} reports could not be put back.`,\n};\n"],"mappings":";AAMO,MAAM,+BAAmD;AAAA,EAC9D,OAAO;AAAA,EACP,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc,CAAC,OAAO,SAAS,EAAE;AAAA,EACjC,aAAa,CAAC,gBAAgB,OAAO,eAAe,EAAE,EAAE,YAAY;AAAA,EACpE,YAAY,CAAC,UAAW,UAAU,IAAI,WAAW,cAAW,KAAK;AAAA,EACjE,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,eAAe;AAAA,EACf,aAAa,CAAC,OAAO,UAAW,UAAU,IAAI,yBAAoB,KAAK,WAAM,OAAO,KAAK,qBAAgB,KAAK;AAAA,EAC9G,YAAY,CAAC,UACX,UAAU,IACN,sDACA,GAAG,KAAK;AAAA,EACd,cAAc,CAAC,UAAW,UAAU,IAAI,+BAA+B,GAAG,KAAK;AAAA,EAC/E,mBAAmB,CAAC,UAClB,UAAU,IAAI,oCAAoC,GAAG,KAAK;AAC9D;","names":[]}
@@ -91,6 +91,32 @@ export interface FeedbackPageLabels {
91
91
  awaitingFilter: string;
92
92
  /** The name of the phone's floating group that holds it. de-CH: "Feedback-Aktionen" */
93
93
  phoneActions: string;
94
+ /** 0.57 (keksdose dev #663), the inbox's bulk bar: the button that sets the selected
95
+ * reports' status, and the name of the list it opens. de-CH: "Status setzen" */
96
+ bulkStatus: string;
97
+ /** The same for the category (never CRASH: a report is not re-labelled a crash, §3.4).
98
+ * de-CH: "Kategorie setzen" */
99
+ bulkCategory: string;
100
+ /** The same for **Visible to others** (§9.3), with `publicSwitch`.
101
+ * de-CH: "Sichtbarkeit" */
102
+ bulkVisibility: string;
103
+ /** Its two choices: `public` on… de-CH: "Für andere sichtbar" */
104
+ bulkPublicOn: string;
105
+ /** …and off. de-CH: "Nicht für andere sichtbar" */
106
+ bulkPublicOff: string;
107
+ /** The Undo toast of a bulk change that landed — `count` reports changed, `value` the
108
+ * status, category or visibility they were set to (its label).
109
+ * de-CH: "{{count}} Meldungen auf „{{value}}“ gesetzt" */
110
+ bulkChanged: (count: number, value: string) => string;
111
+ /** The reports a bulk change could not change — they stay selected, to try again.
112
+ * de-CH: "{{count}} Meldungen konnten nicht geändert werden und bleiben ausgewählt." */
113
+ bulkFailed: (count: number) => string;
114
+ /** The bulk change's Undo landed: `count` reports are back as they were.
115
+ * de-CH: "{{count}} Meldungen zurückgesetzt" */
116
+ bulkRestored: (count: number) => string;
117
+ /** Reports the Undo could not put back. de-CH: "{{count}} Meldungen konnten nicht
118
+ * zurückgesetzt werden." */
119
+ bulkRestoreFailed: (count: number) => string;
94
120
  }
95
121
  export { DEFAULT_FEEDBACK_PAGE_LABELS };
96
122
  /** `feedbackPage` resolved: English, then `<UiKitProvider labels>`, then `labels`. The