@eifi1/ui-kit 0.39.1 → 0.40.1
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.
- package/README.md +8 -7
- package/dist/billing/billing-overview.d.ts +10 -0
- package/dist/components/autocomplete.js +5 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/combobox-core.d.ts +14 -1
- package/dist/components/combobox-core.js +4 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +9 -3
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +5 -0
- package/dist/components/data-table-labels.js +2 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -1
- package/dist/components/data-table-mobile.js +162 -15
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table.d.ts +39 -12
- package/dist/components/data-table.js +74 -13
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +10 -3
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/picker-sheet.d.ts +18 -1
- package/dist/components/picker-sheet.js +20 -2
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/tabs.d.ts +26 -2
- package/dist/components/tabs.js +41 -2
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +2 -1
- package/dist/components/translation-review-parts.js +1 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/ui-labels.js +3 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-comment-thread.d.ts +8 -1
- package/dist/feedback/feedback-comment-thread.js +26 -1
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +2 -1
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu-labels.js +3 -1
- package/dist/feedback/feedback-menu-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +31 -24
- package/dist/feedback/feedback-menu.js +16 -7
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-public-labels.d.ts +3 -2
- package/dist/feedback/feedback-public-labels.js +1 -1
- package/dist/feedback/feedback-public-labels.js.map +1 -1
- package/dist/feedback/feedback-public-list.d.ts +4 -1
- package/dist/feedback/feedback-public-list.js +67 -55
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +16 -3
- package/dist/feedback/feedback-record.d.ts +20 -3
- package/dist/feedback/feedback-record.js +15 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +13 -2
- package/dist/feedback/feedback-replies.js +67 -33
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-rework-entry-chip.d.ts +9 -0
- package/dist/feedback/feedback-rework-entry-chip.js +13 -0
- package/dist/feedback/feedback-rework-entry-chip.js.map +1 -0
- package/dist/feedback/feedback-row-detail.d.ts +31 -8
- package/dist/feedback/feedback-row-detail.js +31 -7
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-table-labels.js +1 -1
- package/dist/feedback/feedback-table-labels.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -2
- package/dist/feedback/feedback-table.js +2 -2
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +3 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.d.ts +9 -6
- package/dist/feedback/feedback-vocabulary.js +16 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +30 -0
- package/dist/hooks/use-long-press.js +77 -0
- package/dist/hooks/use-long-press.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +37 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/german.js +13 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +12 -5
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +13 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +13 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +13 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +13 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +13 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/shell/app-shell.js +9 -0
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/fill-height.d.ts +38 -0
- package/dist/shell/fill-height.js +12 -0
- package/dist/shell/fill-height.js.map +1 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -1
- package/package.json +1 -1
- package/src/billing/billing-overview.ts +10 -0
- package/src/components/autocomplete.tsx +5 -1
- package/src/components/combobox-core.tsx +20 -1
- package/src/components/combobox.tsx +9 -3
- package/src/components/data-table-labels.ts +7 -0
- package/src/components/data-table-mobile.tsx +193 -9
- package/src/components/data-table.tsx +141 -24
- package/src/components/inline-entity-combobox.tsx +12 -4
- package/src/components/picker-sheet.tsx +38 -1
- package/src/components/tabs.tsx +73 -3
- package/src/components/translation-review-parts.tsx +3 -2
- package/src/components/ui-labels.ts +2 -0
- package/src/feedback/feedback-comment-thread.tsx +39 -1
- package/src/feedback/feedback-labels.ts +3 -1
- package/src/feedback/feedback-menu-labels.ts +2 -0
- package/src/feedback/feedback-menu.tsx +48 -30
- package/src/feedback/feedback-public-labels.ts +4 -3
- package/src/feedback/feedback-public-list.tsx +15 -2
- package/src/feedback/feedback-public-types.ts +17 -3
- package/src/feedback/feedback-record.ts +34 -3
- package/src/feedback/feedback-replies.tsx +100 -31
- package/src/feedback/feedback-rework-entry-chip.tsx +18 -0
- package/src/feedback/feedback-row-detail.tsx +65 -12
- package/src/feedback/feedback-table-labels.ts +1 -1
- package/src/feedback/feedback-table.tsx +7 -4
- package/src/feedback/feedback-thread.tsx +5 -1
- package/src/feedback/feedback-vocabulary.ts +27 -6
- package/src/hooks/use-long-press.ts +126 -0
- package/src/hooks/use-overlay-history.ts +74 -3
- package/src/i18n/german.ts +10 -2
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/es.ts +10 -2
- package/src/i18n/locales/fr.ts +10 -2
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +10 -2
- package/src/i18n/locales/zh.ts +10 -2
- package/src/i18n/shared-labels.ts +1 -1
- package/src/shell/app-shell.tsx +9 -0
- package/src/shell/fill-height.tsx +45 -0
- package/src/shell.ts +3 -0
|
@@ -179,11 +179,12 @@ export const FEEDBACK_PICKABLE_CATEGORIES: readonly FeedbackCategory[] = Object.
|
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* The statuses a report can be sent back for **rework** from (§3.4): everything the team
|
|
182
|
-
* has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO
|
|
183
|
-
*
|
|
184
|
-
* `READY` and `IN_PROGRESS` there is nothing to send back yet.
|
|
182
|
+
* has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO` — and,
|
|
183
|
+
* since 0.40, `REWORK` itself (§10.2 decision 4: a second rework adds another entry). On
|
|
184
|
+
* `OPEN`, `READY` and `IN_PROGRESS` there is nothing to send back yet.
|
|
185
185
|
*/
|
|
186
186
|
export const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[] = Object.freeze([
|
|
187
|
+
"REWORK",
|
|
187
188
|
"IN_EVALUATION",
|
|
188
189
|
"NEEDS_LIVE_TEST",
|
|
189
190
|
"POSTPONED",
|
|
@@ -191,6 +192,18 @@ export const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[] = Object.fr
|
|
|
191
192
|
"WONT_DO",
|
|
192
193
|
] as const);
|
|
193
194
|
|
|
195
|
+
/**
|
|
196
|
+
* The loops' queue (0.40, §10.2 decision 4; server-kit `QUEUE_STATUSES`): released for
|
|
197
|
+
* work (`READY`), sent back and released again (`REWORK`), and being worked
|
|
198
|
+
* (`IN_PROGRESS`). Kurvenschmiede's loop adds `OPEN` (decision 9) — its own difference,
|
|
199
|
+
* not this list's.
|
|
200
|
+
*/
|
|
201
|
+
export const FEEDBACK_QUEUE_STATUSES: readonly FeedbackStatus[] = Object.freeze([
|
|
202
|
+
"READY",
|
|
203
|
+
"REWORK",
|
|
204
|
+
"IN_PROGRESS",
|
|
205
|
+
] as const);
|
|
206
|
+
|
|
194
207
|
/**
|
|
195
208
|
* The statuses in which the AUTHOR may still edit title, body and category (§3.4,
|
|
196
209
|
* keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN`, `READY` and `IN_PROGRESS`.
|
|
@@ -320,6 +333,24 @@ export function reworkCount(body: string): number {
|
|
|
320
333
|
return n;
|
|
321
334
|
}
|
|
322
335
|
|
|
336
|
+
/**
|
|
337
|
+
* How many times a report has been sent back, since 0.40 (§10.3): its `kind: "rework"`
|
|
338
|
+
* entries when the row carries `replies`, else {@link reworkCount} off the body (a server
|
|
339
|
+
* before server-kit 0.13, whose reworks are still `--- REWORK … ---` blocks). What the
|
|
340
|
+
* Rework chip counts.
|
|
341
|
+
*/
|
|
342
|
+
export function feedbackReworkCount(
|
|
343
|
+
row: { body?: string | null; replies?: readonly Pick<FeedbackReply, "kind">[] | null },
|
|
344
|
+
): number {
|
|
345
|
+
if (row.replies) {
|
|
346
|
+
const entries = row.replies.filter((reply) => reply.kind === "rework").length;
|
|
347
|
+
// A row migrated by an app that answers `replies` but not yet `kind` keeps its body's
|
|
348
|
+
// count: no entry is a rework there, and the blocks are still in the body.
|
|
349
|
+
if (entries > 0) return entries;
|
|
350
|
+
}
|
|
351
|
+
return reworkCount(row.body ?? "");
|
|
352
|
+
}
|
|
353
|
+
|
|
323
354
|
/**
|
|
324
355
|
* A body split into the prose to render and the files to show beside it, in body order.
|
|
325
356
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { BellOff, Paperclip } from "lucide-react";
|
|
3
4
|
import { Select } from "../components/select";
|
|
4
5
|
import type { CommitScope } from "../components/write-lock";
|
|
5
6
|
import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
@@ -8,9 +9,11 @@ import { formatDate } from "../lib/format";
|
|
|
8
9
|
import { FeedbackStatusBadge, selectableFeedbackStatuses, type FeedbackStatus } from "./feedback-inbox";
|
|
9
10
|
import { useFeedbackStatusLabels } from "./feedback-labels";
|
|
10
11
|
import type { FeedbackReply } from "./feedback-public-types";
|
|
11
|
-
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX } from "./feedback-record";
|
|
12
|
+
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX, attachmentName } from "./feedback-record";
|
|
12
13
|
import { ChatComposer } from "./feedback-thread";
|
|
13
14
|
import { DEFAULT_FEEDBACK_REPLIES_LABELS } from "./feedback-thread-parts-labels";
|
|
15
|
+
import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip";
|
|
16
|
+
import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* The admin's answers as a list (docs/feedback-harmonization.md §9.7, decision 5): each
|
|
@@ -79,23 +82,53 @@ export interface FeedbackReplyTimelineProps {
|
|
|
79
82
|
renderDate?: (iso: string) => string;
|
|
80
83
|
/** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */
|
|
81
84
|
redact?: boolean;
|
|
85
|
+
/** 0.40: how a rework entry's file (`attachment_url`) is drawn under its text. Default:
|
|
86
|
+
* its name beside a paperclip — the file itself is in the row detail's Attachment
|
|
87
|
+
* section, fetched with the app's credentials. */
|
|
88
|
+
renderAttachment?: (url: string) => ReactNode;
|
|
82
89
|
labels?: LabelOverride<FeedbackRepliesLabels>;
|
|
83
90
|
className?: string;
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
/** The status an entry moved the report to — "→ Done", with where from in the admin view.
|
|
94
|
+
* Nothing on an entry with no `status_to`: a status-less reply, or a rework migrated out
|
|
95
|
+
* of the body, whose move was never recorded (0.40, §10.3). */
|
|
96
|
+
function EntryStatus({ reply, statusChanged }: { reply: FeedbackReply; statusChanged: string }) {
|
|
97
|
+
if (!reply.status_to) return null;
|
|
98
|
+
return (
|
|
99
|
+
<span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
|
|
100
|
+
<span className="sr-only">{statusChanged}</span>
|
|
101
|
+
{reply.status_from && reply.status_from !== reply.status_to && (
|
|
102
|
+
<>
|
|
103
|
+
<FeedbackStatusBadge status={reply.status_from} />
|
|
104
|
+
<span aria-hidden>→</span>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
{!reply.status_from && <span aria-hidden>→</span>}
|
|
108
|
+
<FeedbackStatusBadge status={reply.status_to} />
|
|
109
|
+
</span>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
/**
|
|
87
114
|
* The replies, oldest first, down a rule: each with its author (`null` → "Admin"), its
|
|
88
115
|
* time, the status it set ("→ Done"; the admin view adds where from) and its text. No
|
|
89
116
|
* edit and no delete (§9.7): a correction is a new reply.
|
|
117
|
+
*
|
|
118
|
+
* 0.40 (§10.3): an author's **rework** entry (`kind: "rework"`) stands among them as the
|
|
119
|
+
* author's — their name (`null` → "A user", never "Admin"), the **Rework** chip, the
|
|
120
|
+
* status it moved the report to (none on a migrated one) and its file.
|
|
90
121
|
*/
|
|
91
122
|
export function FeedbackReplyTimeline({
|
|
92
123
|
replies,
|
|
93
124
|
renderDate,
|
|
94
125
|
redact = true,
|
|
126
|
+
renderAttachment,
|
|
95
127
|
labels: labelsProp,
|
|
96
128
|
className,
|
|
97
129
|
}: FeedbackReplyTimelineProps) {
|
|
98
130
|
const labels = useFeedbackRepliesLabels(labelsProp);
|
|
131
|
+
const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
|
|
99
132
|
const locale = useKitLocale();
|
|
100
133
|
const priv = redact ? "" : undefined;
|
|
101
134
|
if (replies.length === 0) {
|
|
@@ -105,40 +138,72 @@ export function FeedbackReplyTimeline({
|
|
|
105
138
|
renderDate ? renderDate(iso) : formatDate(iso, { dateStyle: "medium", timeStyle: "short" }, { locale });
|
|
106
139
|
return (
|
|
107
140
|
<ol aria-label={labels.replies} className={cn("space-y-3 border-s-2 border-subtle ps-3", className)}>
|
|
108
|
-
{replies.map((reply) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<span className="
|
|
119
|
-
|
|
120
|
-
{reply.status_from && reply.status_from !== reply.status_to && (
|
|
121
|
-
<>
|
|
122
|
-
<FeedbackStatusBadge status={reply.status_from} />
|
|
123
|
-
<span aria-hidden>→</span>
|
|
124
|
-
</>
|
|
125
|
-
)}
|
|
126
|
-
{!reply.status_from && <span aria-hidden>→</span>}
|
|
127
|
-
<FeedbackStatusBadge status={reply.status_to} />
|
|
141
|
+
{replies.map((reply) => {
|
|
142
|
+
const rework = reply.kind === "rework";
|
|
143
|
+
return (
|
|
144
|
+
<li
|
|
145
|
+
key={reply.id}
|
|
146
|
+
data-feedback-reply={reply.id}
|
|
147
|
+
data-feedback-entry-kind={rework ? "rework" : undefined}
|
|
148
|
+
className="min-w-0 text-sm"
|
|
149
|
+
>
|
|
150
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted">
|
|
151
|
+
<span data-private={priv} className="font-medium text-secondary">
|
|
152
|
+
{reply.author_name ?? (rework ? anonymous : labels.admin)}
|
|
128
153
|
</span>
|
|
154
|
+
{rework && <FeedbackReworkEntryChip />}
|
|
155
|
+
<time dateTime={reply.created_at} className="tabular-nums">
|
|
156
|
+
{dateText(reply.created_at)}
|
|
157
|
+
</time>
|
|
158
|
+
<EntryStatus reply={reply} statusChanged={labels.statusChanged} />
|
|
159
|
+
</div>
|
|
160
|
+
<p data-private={priv} dir="auto" className="mt-0.5 whitespace-pre-wrap break-words text-start text-primary">
|
|
161
|
+
{reply.body}
|
|
162
|
+
</p>
|
|
163
|
+
{rework && reply.attachment_url && (
|
|
164
|
+
<div className="mt-1 min-w-0" data-private={priv} data-feedback-entry-file="">
|
|
165
|
+
{renderAttachment ? (
|
|
166
|
+
renderAttachment(reply.attachment_url)
|
|
167
|
+
) : (
|
|
168
|
+
<span className="inline-flex min-w-0 max-w-full items-center gap-1 text-xs text-secondary">
|
|
169
|
+
<Paperclip className="size-3.5 shrink-0" aria-hidden />
|
|
170
|
+
<span className="min-w-0 break-all">{attachmentName(reply.attachment_url)}</span>
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
129
174
|
)}
|
|
130
|
-
</
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</p>
|
|
134
|
-
</li>
|
|
135
|
-
))}
|
|
175
|
+
</li>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
136
178
|
</ol>
|
|
137
179
|
);
|
|
138
180
|
}
|
|
139
181
|
|
|
140
182
|
/* ── The composer ─────────────────────────────────────────────────────────── */
|
|
141
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The status Select and Reply on one line, one height (keksdose live #417, Android, dark).
|
|
186
|
+
*
|
|
187
|
+
* A field and a button of the same padding are not the same height everywhere: on a touch
|
|
188
|
+
* screen the field's type grows to 16 px (FIELD_TOUCH_TEXT, so iOS does not zoom) and its
|
|
189
|
+
* box with it, while the button keeps its 14 px line — and at Large the button takes its
|
|
190
|
+
* 48 px touch floor, which the field has not. So the composer's action row stretches its
|
|
191
|
+
* two halves to one height and Reply fills it (Button's `stretch`, which is the kit's
|
|
192
|
+
* answer for a button beside a taller field). Scoped to this composer through the marks
|
|
193
|
+
* ChatComposer puts on its row: a host slot with an attachment preview must not drag the
|
|
194
|
+
* send button to the preview's 80 px.
|
|
195
|
+
*
|
|
196
|
+
* On a touch screen the "Ctrl+Enter to send" hint has no keyboard to speak of, and beside
|
|
197
|
+
* a field-height select it pushed Reply onto a line of its own on a phone — it is
|
|
198
|
+
* visually hidden there (still read in order). At Large the button's 48 px floor is
|
|
199
|
+
* taller than any field, here as in every form row of the kit.
|
|
200
|
+
*/
|
|
201
|
+
const REPLY_ROW_ALIGN = cn(
|
|
202
|
+
"[&_[data-chat-composer-actions]]:items-stretch",
|
|
203
|
+
"[&_[data-chat-composer-send]]:self-stretch",
|
|
204
|
+
"pointer-coarse:[&_[data-chat-composer-hint]]:sr-only",
|
|
205
|
+
);
|
|
206
|
+
|
|
142
207
|
export interface FeedbackReplyComposerProps {
|
|
143
208
|
/** The report's status now — what "No change" keeps. */
|
|
144
209
|
status: FeedbackStatus;
|
|
@@ -161,7 +226,9 @@ export interface FeedbackReplyComposerProps {
|
|
|
161
226
|
|
|
162
227
|
/**
|
|
163
228
|
* The admin's reply box (§9.7): the text, the **status** to set with it ("No change"
|
|
164
|
-
* first, then every status — {@link selectableFeedbackStatuses}
|
|
229
|
+
* first, then every status — {@link selectableFeedbackStatuses}, `REWORK` among them
|
|
230
|
+
* since 0.40: how an admin sends any report back, their own included, §10.2 decision 6),
|
|
231
|
+
* and **Reply**. A
|
|
165
232
|
* {@link ChatComposer} underneath: Ctrl/⌘+Enter sends, the count runs to 8 000, a draft
|
|
166
233
|
* typed while a reply is in flight stays.
|
|
167
234
|
*
|
|
@@ -196,7 +263,7 @@ export function FeedbackReplyComposer({
|
|
|
196
263
|
};
|
|
197
264
|
|
|
198
265
|
return (
|
|
199
|
-
<div className={cn("space-y-2", className)} data-feedback-reply-composer="">
|
|
266
|
+
<div className={cn("space-y-2", REPLY_ROW_ALIGN, className)} data-feedback-reply-composer="">
|
|
200
267
|
{suggestedStatus && (
|
|
201
268
|
<p className="flex items-start gap-1.5 text-xs text-secondary" data-feedback-nudge={suggestedStatus}>
|
|
202
269
|
<BellOff className="mt-0.5 size-3.5 shrink-0 text-warning" aria-hidden />
|
|
@@ -211,8 +278,10 @@ export function FeedbackReplyComposer({
|
|
|
211
278
|
showCount
|
|
212
279
|
labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.reply }}
|
|
213
280
|
attachmentSlot={
|
|
281
|
+
// The kit's standard Select at field height (#417: "not invent new things where
|
|
282
|
+
// there are usable ones already") — `size="sm"` was a 28 px box beside the
|
|
283
|
+
// 38 px Reply. See REPLY_ROW_ALIGN for how the two keep one height.
|
|
214
284
|
<Select
|
|
215
|
-
size="sm"
|
|
216
285
|
aria-label={labels.status}
|
|
217
286
|
value={picked}
|
|
218
287
|
disabled={pending}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Chip } from "../components/chip";
|
|
2
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
3
|
+
import { DEFAULT_FEEDBACK_PAGE_LABELS } from "./feedback-table-labels";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The **Rework** chip on an author's rework entry (0.40, docs/feedback-harmonization.md
|
|
7
|
+
* §10.3) — the table's own `feedbackPage.reworkChip(1)` and tone, so the entry and the
|
|
8
|
+
* row's chip say the same. Drawn by `FeedbackReplyTimeline` and `FeedbackCommentThread`.
|
|
9
|
+
* Internal: not re-exported.
|
|
10
|
+
*/
|
|
11
|
+
export function FeedbackReworkEntryChip({ className }: { className?: string }) {
|
|
12
|
+
const pageLabels = useKitLabels("feedbackPage", DEFAULT_FEEDBACK_PAGE_LABELS);
|
|
13
|
+
return (
|
|
14
|
+
<Chip size="sm" tone="warning" caps className={className} data-feedback-entry-rework="">
|
|
15
|
+
{pageLabels.reworkChip(1)}
|
|
16
|
+
</Chip>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -237,7 +237,8 @@ export function feedbackPageUrl(context: FeedbackContext | null | undefined): st
|
|
|
237
237
|
* keyed by URL, a React key clash keksdose had).
|
|
238
238
|
*/
|
|
239
239
|
export function feedbackAttachmentUrls(
|
|
240
|
-
row: Pick<FeedbackRecord, "screenshot_url" | "attachment_urls" | "body"
|
|
240
|
+
row: Pick<FeedbackRecord, "screenshot_url" | "attachment_urls" | "body"> &
|
|
241
|
+
Partial<Pick<FeedbackRecord, "replies">>,
|
|
241
242
|
): string[] {
|
|
242
243
|
const urls = [
|
|
243
244
|
...(row.screenshot_url ? [row.screenshot_url] : []),
|
|
@@ -245,6 +246,11 @@ export function feedbackAttachmentUrls(
|
|
|
245
246
|
// that predates it (keksdose dev #578).
|
|
246
247
|
...(row.attachment_urls ?? []),
|
|
247
248
|
...splitBodyAttachments(row.body).urls,
|
|
249
|
+
// 0.40 (§10.3): a rework entry's one file, in entry order — where the rework
|
|
250
|
+
// pictures of the body were before.
|
|
251
|
+
...(row.replies ?? []).flatMap((reply) =>
|
|
252
|
+
reply.kind === "rework" && reply.attachment_url ? [reply.attachment_url] : [],
|
|
253
|
+
),
|
|
248
254
|
];
|
|
249
255
|
return [...new Set(urls)];
|
|
250
256
|
}
|
|
@@ -330,8 +336,9 @@ export interface FeedbackRowAccess {
|
|
|
330
336
|
/** The admin on the inbox (`canEdit`): since 0.38 the reply composer (the outcome
|
|
331
337
|
* itself is frozen, §9.7). */
|
|
332
338
|
canEditOutcome: boolean;
|
|
333
|
-
/** "Rework": the
|
|
334
|
-
*
|
|
339
|
+
/** "Rework": the author, on an answered row ({@link FEEDBACK_REWORKABLE_STATUSES}).
|
|
340
|
+
* Since 0.40 (§10.2 decision 6) the author's only — an admin sends a report back with
|
|
341
|
+
* a reply whose status is `REWORK`; the server answers anyone else 403. */
|
|
335
342
|
canRework: boolean;
|
|
336
343
|
/** The status pills: the admin on the inbox; everyone else sees them read-only. */
|
|
337
344
|
canChangeStatus: boolean;
|
|
@@ -356,7 +363,7 @@ export function feedbackRowAccess(
|
|
|
356
363
|
isAuthor,
|
|
357
364
|
canEditDescription: isAuthor && FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status),
|
|
358
365
|
canEditOutcome: canEdit,
|
|
359
|
-
canRework:
|
|
366
|
+
canRework: isAuthor && FEEDBACK_REWORKABLE_STATUSES.includes(row.status),
|
|
360
367
|
canChangeStatus: canEdit,
|
|
361
368
|
};
|
|
362
369
|
}
|
|
@@ -396,11 +403,22 @@ export type FeedbackUpload = (file: File) => Promise<string>;
|
|
|
396
403
|
|
|
397
404
|
/* ── Send for rework (§4.4.5) ─────────────────────────────────────────────── */
|
|
398
405
|
|
|
406
|
+
/** 0.40 (§10.3): `POST /feedback/{id}/rework` — the trimmed note and the uploaded file's
|
|
407
|
+
* URL. Return the promise: the section closes when it resolves and keeps the note when
|
|
408
|
+
* it rejects (the toast is the app's: the server's `detail`, else
|
|
409
|
+
* `feedbackToast.updateFailed`). */
|
|
410
|
+
export type FeedbackReworkSend = (note: string, attachmentUrl?: string) => void | Promise<unknown>;
|
|
411
|
+
|
|
399
412
|
export interface FeedbackReworkSectionProps {
|
|
400
413
|
/** The row being sent back: its id, and the body the round is appended to. */
|
|
401
414
|
row: Pick<FeedbackRecord, "id" | "body">;
|
|
402
|
-
/**
|
|
403
|
-
onUpdate
|
|
415
|
+
/** 0.40: the rework as an entry of its own — see {@link FeedbackReworkSend}. With it,
|
|
416
|
+
* `onUpdate` is not called. */
|
|
417
|
+
onRework?: FeedbackReworkSend;
|
|
418
|
+
/** The PATCH — see {@link FeedbackDetailUpdate}. Called with `{ id, body }` only, and
|
|
419
|
+
* only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still
|
|
420
|
+
* turns into an entry for one release. Optional since 0.40; pass one of the two. */
|
|
421
|
+
onUpdate?: FeedbackDetailUpdate;
|
|
404
422
|
/** The PATCH is in flight (`update.isPending`). The upload adds its own. */
|
|
405
423
|
pending?: boolean;
|
|
406
424
|
/** The upload. Left out, the note goes without a file field. */
|
|
@@ -422,7 +440,7 @@ export interface FeedbackReworkSectionProps {
|
|
|
422
440
|
}
|
|
423
441
|
|
|
424
442
|
/**
|
|
425
|
-
* The rework editor (§4.4.5): the submitter
|
|
443
|
+
* The rework editor (§4.4.5): the submitter sends an answered report
|
|
426
444
|
* back with a note and at most one file, and the server reopens it (§3.4, keksdose
|
|
427
445
|
* `submitRework`, `feedback-page.tsx:350`).
|
|
428
446
|
*
|
|
@@ -432,6 +450,9 @@ export interface FeedbackReworkSectionProps {
|
|
|
432
450
|
* uploaded FIRST and awaited: a body naming a file that never arrived would be a
|
|
433
451
|
* rework whose evidence is missing, and the upload is the half that fails on its
|
|
434
452
|
* own. If it fails nothing is sent and the toast says so; the note stays in the box.
|
|
453
|
+
* - **0.40: an entry of its own** (§10.3) — with `onRework`, the note and the file's URL
|
|
454
|
+
* go to `POST /feedback/{id}/rework` and the body is not touched. Without it, the
|
|
455
|
+
* legacy append below, which server-kit 0.13 still converts (one minor).
|
|
435
456
|
* - **The body alone goes in the PATCH** — {@link appendRework}'s strict extension of
|
|
436
457
|
* the current body, which is how the server recognises the append and reopens the
|
|
437
458
|
* row. Never a `status` (kastlan's client sent `OPEN`; an author may not write one,
|
|
@@ -443,6 +464,7 @@ export interface FeedbackReworkSectionProps {
|
|
|
443
464
|
*/
|
|
444
465
|
export function FeedbackReworkSection({
|
|
445
466
|
row,
|
|
467
|
+
onRework,
|
|
446
468
|
onUpdate,
|
|
447
469
|
pending = false,
|
|
448
470
|
onUpload,
|
|
@@ -456,6 +478,7 @@ export function FeedbackReworkSection({
|
|
|
456
478
|
const labels = useFeedbackDetailLabels(labelsProp);
|
|
457
479
|
const toasts = useFeedbackToastLabels();
|
|
458
480
|
const [uploading, setUploading] = useState(false);
|
|
481
|
+
const [sending, setSending] = useState(false);
|
|
459
482
|
// The body as it is NOW, not as it was when Send was pressed: the list can refresh
|
|
460
483
|
// while the file uploads, and an append to a stale body is no longer a strict
|
|
461
484
|
// extension of the stored one — the server would read it as an edit, and refuse it.
|
|
@@ -490,6 +513,21 @@ export function FeedbackReworkSection({
|
|
|
490
513
|
setUploading(false);
|
|
491
514
|
}
|
|
492
515
|
}
|
|
516
|
+
if (onRework) {
|
|
517
|
+
// 0.40 (§10.3): the entry, not the body — the server checks the URL (a 422 outside
|
|
518
|
+
// the app's attachment prefix).
|
|
519
|
+
setSending(true);
|
|
520
|
+
try {
|
|
521
|
+
await onRework(note, url ?? undefined);
|
|
522
|
+
} catch {
|
|
523
|
+
return; // The app's toast; the note stays in the box.
|
|
524
|
+
} finally {
|
|
525
|
+
setSending(false);
|
|
526
|
+
}
|
|
527
|
+
onSent?.();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (!onUpdate) return;
|
|
493
531
|
let body: string | null;
|
|
494
532
|
try {
|
|
495
533
|
body = appendRework(latestBody.current, note, url);
|
|
@@ -511,7 +549,7 @@ export function FeedbackReworkSection({
|
|
|
511
549
|
initial=""
|
|
512
550
|
required
|
|
513
551
|
commit={commit}
|
|
514
|
-
pending={pending || uploading}
|
|
552
|
+
pending={pending || uploading || sending}
|
|
515
553
|
onSave={(value, file) => void send(value, file)}
|
|
516
554
|
onCancel={onCancel}
|
|
517
555
|
saveLabel={labels.reworkSend}
|
|
@@ -593,6 +631,15 @@ export interface FeedbackRowDetailProps {
|
|
|
593
631
|
onReply?: (body: string, status?: FeedbackStatus) => void | Promise<unknown>;
|
|
594
632
|
/** 0.38: the reply is in flight. */
|
|
595
633
|
replying?: boolean;
|
|
634
|
+
/**
|
|
635
|
+
* 0.40 (§10.3): `POST /feedback/{id}/rework` — the author's rework as an entry of its
|
|
636
|
+
* own (see {@link FeedbackReworkSend}). The **Rework** button shows only to the author
|
|
637
|
+
* on an answered row, and only where the reply composer is not shown (§10.2 decision 6:
|
|
638
|
+
* an admin answering a report sends it back with a reply whose status is `REWORK`).
|
|
639
|
+
* Left out, the rework goes through `onUpdate` as the legacy body append — for one
|
|
640
|
+
* minor, while the server converts it.
|
|
641
|
+
*/
|
|
642
|
+
onRework?: FeedbackReworkSend;
|
|
596
643
|
/** 0.38: the report's comments — the app's `FeedbackCommentThread` — in a section of
|
|
597
644
|
* their own under the replies. A thread here draws the comments alone (0.38.1): the
|
|
598
645
|
* Replies section above has the replies, so whatever `replies` the thread is handed
|
|
@@ -647,6 +694,7 @@ export function FeedbackRowDetail({
|
|
|
647
694
|
showSubject = true,
|
|
648
695
|
onReply,
|
|
649
696
|
replying = false,
|
|
697
|
+
onRework,
|
|
650
698
|
comments,
|
|
651
699
|
onPublicChange,
|
|
652
700
|
labels: labelsProp,
|
|
@@ -687,7 +735,11 @@ export function FeedbackRowDetail({
|
|
|
687
735
|
// 0.38, whose frozen outcome is shown as it is, read-only.
|
|
688
736
|
const replies = row.replies;
|
|
689
737
|
const commentLabels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS);
|
|
690
|
-
|
|
738
|
+
// 0.40 (§10.2 decision 6): whoever has the reply composer answers with it — REWORK is
|
|
739
|
+
// one of its statuses — so the Rework button is for an author without it.
|
|
740
|
+
const composerShown = Boolean(replies && access.canEditOutcome && onReply);
|
|
741
|
+
const canRework = access.canRework && !composerShown;
|
|
742
|
+
const showRework = canRework && reworking;
|
|
691
743
|
|
|
692
744
|
return (
|
|
693
745
|
<FeedbackDetail className={className}>
|
|
@@ -791,7 +843,7 @@ export function FeedbackRowDetail({
|
|
|
791
843
|
) : (
|
|
792
844
|
<FeedbackProse>{row.outcome || undefined}</FeedbackProse>
|
|
793
845
|
)}
|
|
794
|
-
{
|
|
846
|
+
{composerShown && replies && onReply && (
|
|
795
847
|
<FeedbackReplyComposer
|
|
796
848
|
key={row.id}
|
|
797
849
|
status={row.status}
|
|
@@ -801,9 +853,9 @@ export function FeedbackRowDetail({
|
|
|
801
853
|
commit={commit}
|
|
802
854
|
/>
|
|
803
855
|
)}
|
|
804
|
-
{((
|
|
856
|
+
{((canRework && !reworking) || pageHref) && (
|
|
805
857
|
<div className="flex flex-wrap gap-2">
|
|
806
|
-
{
|
|
858
|
+
{canRework && !reworking && (
|
|
807
859
|
<Button
|
|
808
860
|
type="button"
|
|
809
861
|
variant="secondary"
|
|
@@ -836,6 +888,7 @@ export function FeedbackRowDetail({
|
|
|
836
888
|
{showRework && (
|
|
837
889
|
<FeedbackReworkSection
|
|
838
890
|
row={row}
|
|
891
|
+
onRework={onRework}
|
|
839
892
|
onUpdate={onUpdate}
|
|
840
893
|
pending={pending}
|
|
841
894
|
onUpload={onUpload}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type FeedbackCategoryLabels,
|
|
29
29
|
type FeedbackStatusLabels,
|
|
30
30
|
} from "./feedback-labels";
|
|
31
|
-
import { FEEDBACK_AWAITING_STATUSES,
|
|
31
|
+
import { FEEDBACK_AWAITING_STATUSES, feedbackReworkCount, type FeedbackContext, type FeedbackRecord } from "./feedback-record";
|
|
32
32
|
import type { FeedbackStatusChange } from "./feedback-status-undo";
|
|
33
33
|
import { feedbackPageHref, feedbackPagePath } from "./feedback-row-detail";
|
|
34
34
|
import type { CommitScope } from "../components/write-lock";
|
|
@@ -70,7 +70,8 @@ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
|
|
|
70
70
|
* `de-CH.json`, ss never ß).
|
|
71
71
|
*/
|
|
72
72
|
export interface FeedbackPageLabels {
|
|
73
|
-
/** The admin inbox's page title, `/feedback
|
|
73
|
+
/** The admin inbox's page title, `/feedback` — the menu's "Feedback inbox" (0.40,
|
|
74
|
+
* §10.2 decision 11; "Feedback" before). de-CH: "Feedback-Eingang" */
|
|
74
75
|
title: string;
|
|
75
76
|
/** The user's own list, `/my-feedback`. de-CH: "Mein Feedback" */
|
|
76
77
|
myTitle: string;
|
|
@@ -268,7 +269,9 @@ function instant(value: string | null | undefined): number | null {
|
|
|
268
269
|
/* ── The subject: title + environment chip + rework chip ───────────────────── */
|
|
269
270
|
|
|
270
271
|
export interface FeedbackSubjectProps {
|
|
271
|
-
|
|
272
|
+
/** `replies` (0.40): the chip counts the rework entries, else the body's blocks —
|
|
273
|
+
* {@link feedbackReworkCount}. */
|
|
274
|
+
row: Pick<FeedbackRecord, "title" | "body" | "context"> & Partial<Pick<FeedbackRecord, "replies">>;
|
|
272
275
|
/**
|
|
273
276
|
* `false` (default): one line — the title truncates and the chips stay whole, as a
|
|
274
277
|
* table cell wants. `true`: the title wraps and the chips flow after its last word, as
|
|
@@ -292,7 +295,7 @@ export interface FeedbackSubjectProps {
|
|
|
292
295
|
export function FeedbackSubject({ row, wrap = false, labels: labelsProp, className }: FeedbackSubjectProps) {
|
|
293
296
|
const labels = useFeedbackPageLabels(labelsProp);
|
|
294
297
|
const env = feedbackEnvironment(row.context);
|
|
295
|
-
const rounds =
|
|
298
|
+
const rounds = feedbackReworkCount(row);
|
|
296
299
|
const chipClass = wrap ? "ms-1.5 align-middle" : "shrink-0";
|
|
297
300
|
const chips = (
|
|
298
301
|
<>
|
|
@@ -852,7 +852,9 @@ export function ChatComposer({
|
|
|
852
852
|
}
|
|
853
853
|
}}
|
|
854
854
|
/>
|
|
855
|
-
|
|
855
|
+
{/* The `data-chat-composer-*` marks are for a host that lines its slot up with the
|
|
856
|
+
send button — FeedbackReplyComposer's status Select (#417). */}
|
|
857
|
+
<div data-chat-composer-actions="" className="flex flex-wrap items-end justify-between gap-2">
|
|
856
858
|
{config || slot != null ? (
|
|
857
859
|
<div className="flex min-w-0 flex-wrap items-end gap-2">
|
|
858
860
|
{config && (
|
|
@@ -878,11 +880,13 @@ export function ChatComposer({
|
|
|
878
880
|
<div className="ms-auto flex items-center gap-2">
|
|
879
881
|
<span
|
|
880
882
|
id={hintId}
|
|
883
|
+
data-chat-composer-hint=""
|
|
881
884
|
className={size === "sm" ? "sr-only" : "text-xs text-placeholder"}
|
|
882
885
|
>
|
|
883
886
|
{sendOn === "enter" ? labels.sendHintEnter : labels.sendHint(modifier)}
|
|
884
887
|
</span>
|
|
885
888
|
<Button
|
|
889
|
+
data-chat-composer-send=""
|
|
886
890
|
onClick={send}
|
|
887
891
|
disabled={!canSend}
|
|
888
892
|
commit={commit}
|
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
MoreHorizontal,
|
|
12
12
|
OctagonAlert,
|
|
13
13
|
PauseCircle,
|
|
14
|
+
RotateCcw,
|
|
14
15
|
Wrench,
|
|
15
16
|
} from "lucide-react";
|
|
16
17
|
import type { LucideIcon } from "lucide-react";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* The feedback **vocabulary**: the
|
|
20
|
+
* The feedback **vocabulary**: the nine statuses and five categories, the glyph and tone
|
|
20
21
|
* of each, their orders, and the policy for how a report moves from status to status.
|
|
21
22
|
*
|
|
22
23
|
* Data and plain functions, no components. Until 0.34 this lived at the top of
|
|
@@ -27,16 +28,18 @@ import type { LucideIcon } from "lucide-react";
|
|
|
27
28
|
* feedback-inbox.tsx re-exports every name here, so nothing an app imports has moved.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
|
-
/** The
|
|
31
|
+
/** The nine states a report can be in.
|
|
31
32
|
*
|
|
32
33
|
* Shared value for value across the apps on purpose: it is what lets one habit,
|
|
33
34
|
* and one agent prompt, work on any repo. Five are a chain and three sit off
|
|
34
35
|
* it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31
|
|
35
36
|
* (feedback contract §8.2): OPEN is "filed, not yet triaged", READY "released for
|
|
36
|
-
* implementation", and nobody works an OPEN row.
|
|
37
|
+
* implementation", and nobody works an OPEN row. `REWORK` joined in 0.40 (§10.2
|
|
38
|
+
* decisions 2, 4): "sent back, released for work again" — a READY that came back. */
|
|
37
39
|
export type FeedbackStatus =
|
|
38
40
|
| "OPEN"
|
|
39
41
|
| "READY"
|
|
42
|
+
| "REWORK"
|
|
40
43
|
| "IN_PROGRESS"
|
|
41
44
|
| "IN_EVALUATION"
|
|
42
45
|
| "NEEDS_LIVE_TEST"
|
|
@@ -76,6 +79,16 @@ export const FEEDBACK_STATUS_META: Record<
|
|
|
76
79
|
activeBg: "bg-brand-soft",
|
|
77
80
|
activeText: "text-brand",
|
|
78
81
|
},
|
|
82
|
+
// Sent back by its author or an admin and released for work again (0.40, feedback
|
|
83
|
+
// contract §10.2 decision 4): the Rework button's `RotateCcw`, in the danger tint so the
|
|
84
|
+
// queue shows reworks apart from new reports. Not IN_PROGRESS's warning tint — that is
|
|
85
|
+
// where a REWORK row moves next — and WONT_DO, the only other red, sits at the far end
|
|
86
|
+
// with `Ban`.
|
|
87
|
+
REWORK: {
|
|
88
|
+
icon: RotateCcw,
|
|
89
|
+
activeBg: "bg-danger-soft",
|
|
90
|
+
activeText: "text-danger",
|
|
91
|
+
},
|
|
79
92
|
IN_PROGRESS: {
|
|
80
93
|
icon: Wrench,
|
|
81
94
|
activeBg: "bg-warning-soft",
|
|
@@ -142,8 +155,11 @@ const OFF_CHAIN: FeedbackStatus[] = ["NEEDS_LIVE_TEST", "POSTPONED", "WONT_DO"];
|
|
|
142
155
|
const FORWARD_CHAIN: FeedbackStatus[] = ["OPEN", "READY", "IN_PROGRESS", "IN_EVALUATION", "DONE"];
|
|
143
156
|
|
|
144
157
|
/** The next status along the chain, or null at the end (or off it, as `WONT_DO`
|
|
145
|
-
* is). What a swipe-to-advance gesture commits.
|
|
158
|
+
* is). What a swipe-to-advance gesture commits. `REWORK` (0.40) is not in the chain —
|
|
159
|
+
* "advance" from READY still goes to IN_PROGRESS — but it leads there too: a reworked
|
|
160
|
+
* row is READY's twin. */
|
|
146
161
|
export function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {
|
|
162
|
+
if (current === "REWORK") return "IN_PROGRESS";
|
|
147
163
|
const at = FORWARD_CHAIN.indexOf(current);
|
|
148
164
|
return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;
|
|
149
165
|
}
|
|
@@ -152,7 +168,7 @@ export function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | nu
|
|
|
152
168
|
* The steps that make sense from where a row currently stands.
|
|
153
169
|
*
|
|
154
170
|
* For the compact control in a table cell: it is a glanceable triage affordance,
|
|
155
|
-
* and a row of
|
|
171
|
+
* and a row of nine icons in a cell is noise. One step forward, one step back
|
|
156
172
|
* (so an item can be sent back for rework), and any of the three off-chain
|
|
157
173
|
* verdicts — which is what makes those usable as verdicts at all.
|
|
158
174
|
*
|
|
@@ -165,7 +181,12 @@ export function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | nu
|
|
|
165
181
|
export function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {
|
|
166
182
|
const set = new Set<FeedbackStatus>([current]);
|
|
167
183
|
const at = FORWARD_CHAIN.indexOf(current);
|
|
168
|
-
if (
|
|
184
|
+
if (current === "REWORK") {
|
|
185
|
+
// READY's twin (0.40, §10.2 decision 4): the step that picks the work up, and the
|
|
186
|
+
// verdicts a released row can still get.
|
|
187
|
+
set.add("IN_PROGRESS");
|
|
188
|
+
for (const off of OFF_CHAIN) set.add(off);
|
|
189
|
+
} else if (at !== -1) {
|
|
169
190
|
if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);
|
|
170
191
|
if (at > 0) set.add(FORWARD_CHAIN[at - 1]);
|
|
171
192
|
for (const off of OFF_CHAIN) set.add(off);
|