@eifi1/ui-kit 0.39.0 → 0.40.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.
- package/README.md +8 -7
- package/dist/billing/billing-overview.d.ts +12 -2
- package/dist/billing/pay-page.js +1 -1
- package/dist/billing/pay-page.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +1 -1
- 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.js +1 -0
- 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/pay/pay.js +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 +12 -2
- package/src/billing/pay-page.ts +1 -1
- 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 +4 -1
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Inbox, MessageSquare, Users } from "lucide-react";
|
|
1
|
+
import { Inbox, MessageSquare, User, Users } from "lucide-react";
|
|
2
2
|
import type { UserAvatarBadge } from "../components/user-avatar";
|
|
3
3
|
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
4
4
|
import { TopBarActionMenu, type TopBarMenuEntry } from "../shell/topbar-action-menu";
|
|
@@ -13,21 +13,26 @@ import { DEFAULT_FEEDBACK_MENU_LABELS } from "./feedback-menu-labels";
|
|
|
13
13
|
/**
|
|
14
14
|
* `feedbackMenu` — the top-bar feedback menu's own words (0.27.0). The four category rows
|
|
15
15
|
* are `feedbackCategory`'s, shared with the badges and the dialog's picker. de-CH canon
|
|
16
|
-
* (keksdose `de-CH.json
|
|
17
|
-
* `
|
|
16
|
+
* (keksdose `de-CH.json`, §10.6): `trigger` "Feedback senden", `discussion`
|
|
17
|
+
* "Feedback-Diskussion", `myFeedback` "Mein Feedback", `inbox` "Feedback-Eingang".
|
|
18
18
|
*/
|
|
19
19
|
export interface FeedbackMenuLabels {
|
|
20
20
|
/** The trigger's accessible name, and the menu's. keksdose `feedback.send`. */
|
|
21
21
|
trigger: string;
|
|
22
|
-
/** The link to the
|
|
23
|
-
* `feedback.my_title`. */
|
|
22
|
+
/** The link to the viewer's own reports — everyone's, an admin's too since 0.40
|
|
23
|
+
* (§10.2 decision 3). keksdose `feedback.my_title`. */
|
|
24
24
|
myFeedback: string;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
|
|
25
|
+
/** 0.40: the link to the public list, {@link FeedbackMenuProps.publicHref} (§10.2
|
|
26
|
+
* decision 3; "All feedback" before). de-CH "Feedback-Diskussion". */
|
|
27
|
+
discussion: string;
|
|
28
|
+
/** 0.40: the admin's link to the triage list, {@link FeedbackMenuProps.inboxHref}
|
|
29
|
+
* ("View feedback" before). de-CH "Feedback-Eingang". */
|
|
30
|
+
inbox: string;
|
|
31
|
+
/** @deprecated 0.40 — use {@link inbox}; removed in 0.41. Unused: the inbox row reads
|
|
32
|
+
* `inbox` (feedback §10.6). */
|
|
33
|
+
viewFeedback?: string;
|
|
34
|
+
/** @deprecated 0.40 — use {@link discussion}; removed in 0.41. Unused: the public row
|
|
35
|
+
* reads `discussion` (feedback §10.6). */
|
|
31
36
|
allFeedback?: string;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -38,8 +43,8 @@ export interface FeedbackMenuProps {
|
|
|
38
43
|
/** A category row was chosen — open the submit dialog on it (`useFeedbackSubmit`'s
|
|
39
44
|
* `open`). Never called with `CRASH`: there is no row for it. */
|
|
40
45
|
onFile: (category: FeedbackCategory) => void;
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
46
|
+
/** Adds the **Feedback inbox** row → {@link inboxHref} under "My feedback" (0.40, §10.2
|
|
47
|
+
* decision 11; until 0.39 it replaced it). The admin check is the app's (kastlan's is
|
|
43
48
|
* company-scoped). Default `false`. */
|
|
44
49
|
isAdmin?: boolean;
|
|
45
50
|
/**
|
|
@@ -50,16 +55,16 @@ export interface FeedbackMenuProps {
|
|
|
50
55
|
* `all-feedback`, `my-feedback`, `view-feedback`).
|
|
51
56
|
*/
|
|
52
57
|
extraEntries?: TopBarMenuEntry[];
|
|
53
|
-
/** The
|
|
54
|
-
* (§2.3). */
|
|
58
|
+
/** The viewer's own reports, linked for everyone (an admin too since 0.40). Default
|
|
59
|
+
* `"/my-feedback"` (§2.3). */
|
|
55
60
|
myFeedbackHref?: string;
|
|
56
61
|
/** The admin inbox, linked for an admin. Default `"/feedback"` (§2.3). */
|
|
57
62
|
inboxHref?: string;
|
|
58
63
|
/**
|
|
59
64
|
* The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,
|
|
60
|
-
*
|
|
61
|
-
* everyone — the admin too, who reads what the others read
|
|
62
|
-
* `"/all-feedback"
|
|
65
|
+
* a **Feedback discussion** row (`Users`, key `all-feedback`; "All feedback" until 0.40)
|
|
66
|
+
* first of the list links, for everyone — the admin too, who reads what the others read
|
|
67
|
+
* there. keksdose: `"/all-feedback"`. Leave it out where the app has no
|
|
63
68
|
* public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a
|
|
64
69
|
* demo — §9.2 decisions 7, 8).
|
|
65
70
|
*/
|
|
@@ -83,17 +88,19 @@ export interface FeedbackMenuProps {
|
|
|
83
88
|
* the category badges' own glyphs), each calling `onFile(category)`;
|
|
84
89
|
* 3. a divider, always;
|
|
85
90
|
* 4. the app's `extraEntries`;
|
|
86
|
-
* 5.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
91
|
+
* 5. the list links (0.40, §10.2 decision 11), in this order: **Feedback discussion**
|
|
92
|
+
* (`Users`, key `all-feedback`) → `publicHref`, when passed; **My feedback** (`User`,
|
|
93
|
+
* key `my-feedback`) → `/my-feedback`, for everyone, admins included; **Feedback
|
|
94
|
+
* inbox** (`Inbox`, key `view-feedback`) → `/feedback`, for an admin. An admin with
|
|
95
|
+
* public parts sees three, a member two; without them an admin two and anyone else
|
|
96
|
+
* one. Until 0.39 an admin had the inbox in place of their own list (feedback #331);
|
|
97
|
+
* Marcel's kk #419 gave them both.
|
|
90
98
|
*
|
|
91
99
|
* WHY A PRESET. Three apps had drawn three menus on the same primitive and they disagreed:
|
|
92
100
|
* kastlan headed it "New submission", Kurvenschmiede "Send feedback" with a single link
|
|
93
101
|
* that changed its words by role, keksdose had no heading and one link by role. keksdose
|
|
94
|
-
* is the source (§2.4, §7.11): no heading,
|
|
95
|
-
*
|
|
96
|
-
* words cannot drift again. Where the menu sits in the bar stays each app's.
|
|
102
|
+
* is the source (§2.4, §7.11): no heading, the list links by role — and the kit draws
|
|
103
|
+
* it, so the row order, the icons and the words cannot drift again. Where the menu sits in the bar stays each app's.
|
|
97
104
|
*
|
|
98
105
|
* `CRASH` has no row and never will: crash reports are filed by the error boundary with
|
|
99
106
|
* the stack and a fingerprint (keksdose feedback #160), and a hand-filed "crash" would sort
|
|
@@ -136,17 +143,28 @@ export function FeedbackMenu({
|
|
|
136
143
|
key: "all-feedback",
|
|
137
144
|
to: publicHref,
|
|
138
145
|
icon: <Users className="size-4" />,
|
|
139
|
-
label: labels.
|
|
146
|
+
label: labels.discussion,
|
|
140
147
|
} satisfies TopBarMenuEntry,
|
|
141
148
|
]
|
|
142
149
|
: []),
|
|
143
150
|
{
|
|
144
151
|
kind: "link",
|
|
145
|
-
key:
|
|
146
|
-
to:
|
|
147
|
-
icon: <
|
|
148
|
-
label:
|
|
152
|
+
key: "my-feedback",
|
|
153
|
+
to: myFeedbackHref,
|
|
154
|
+
icon: <User className="size-4" />,
|
|
155
|
+
label: labels.myFeedback,
|
|
149
156
|
},
|
|
157
|
+
...(isAdmin
|
|
158
|
+
? [
|
|
159
|
+
{
|
|
160
|
+
kind: "link",
|
|
161
|
+
key: "view-feedback",
|
|
162
|
+
to: inboxHref,
|
|
163
|
+
icon: <Inbox className="size-4" />,
|
|
164
|
+
label: labels.inbox,
|
|
165
|
+
} satisfies TopBarMenuEntry,
|
|
166
|
+
]
|
|
167
|
+
: []),
|
|
150
168
|
];
|
|
151
169
|
return (
|
|
152
170
|
<TopBarActionMenu
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are
|
|
11
11
|
* `feedbackVote`'s, the thread's `feedbackComments`'.
|
|
12
12
|
*
|
|
13
|
-
* de-CH canon (§9.2, §9.5): `title` "
|
|
13
|
+
* de-CH canon (§9.2, §9.5, §10.6): `title` "Feedback-Diskussion", `anonymous` "Eine Person".
|
|
14
14
|
*/
|
|
15
15
|
export interface FeedbackPublicLabels {
|
|
16
|
-
/** The page's heading — keksdose's `/all-feedback`, the menu's "
|
|
16
|
+
/** The page's heading — keksdose's `/all-feedback`, the menu's "Feedback discussion"
|
|
17
|
+
* (0.40, §10.2 decision 11; "All feedback" before). de-CH "Feedback-Diskussion". */
|
|
17
18
|
title: string;
|
|
18
19
|
/** The sort toggle's accessible name. */
|
|
19
20
|
sort: string;
|
|
@@ -57,7 +58,7 @@ export interface FeedbackPublicLabels {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels = {
|
|
60
|
-
title: "
|
|
61
|
+
title: "Feedback discussion",
|
|
61
62
|
sort: "Sort",
|
|
62
63
|
sortNewest: "Newest",
|
|
63
64
|
sortVotes: "Most wanted",
|
|
@@ -455,7 +455,8 @@ export function FeedbackPublicDetail({
|
|
|
455
455
|
/>
|
|
456
456
|
</div>
|
|
457
457
|
<FeedbackDetailSection title={labels.body}>
|
|
458
|
-
{/* Rework rounds stay in it — the author's own words (§9.4)
|
|
458
|
+
{/* Rework rounds stay in it — the author's own words (§9.4) — on a server before
|
|
459
|
+
server-kit 0.13; since then each is an entry in the conversation (§10.3). */}
|
|
459
460
|
<FeedbackProse>{row.body}</FeedbackProse>
|
|
460
461
|
</FeedbackDetailSection>
|
|
461
462
|
<FeedbackDetailSection title={labels.conversation}>
|
|
@@ -464,6 +465,8 @@ export function FeedbackPublicDetail({
|
|
|
464
465
|
comments={comments ?? []}
|
|
465
466
|
loading={comments === undefined}
|
|
466
467
|
isAdmin={isAdmin}
|
|
468
|
+
// 0.40 (§10.3): the author's rework entries are theirs — at the reading end.
|
|
469
|
+
isAuthor={row.mine}
|
|
467
470
|
onComment={onComment}
|
|
468
471
|
onHide={onHide}
|
|
469
472
|
onDelete={onDelete}
|
|
@@ -11,14 +11,28 @@ import type { FeedbackCategory, FeedbackStatus } from "./feedback-vocabulary";
|
|
|
11
11
|
* account, a loop's reply). The client never builds one from an id.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/** The two kinds of entry in a report's timeline (0.40, §10.2 decision 7): the admin's
|
|
15
|
+
* `reply`, and the author's `rework` — the report sent back with a note. */
|
|
16
|
+
export type FeedbackEntryKind = "reply" | "rework";
|
|
17
|
+
|
|
18
|
+
/** One answer of the admin's — `FeedbackReplyResponse`, §9.7 — or, since 0.40, one
|
|
19
|
+
* rework of the author's (§10.3, `kind: "rework"`). Oldest first. */
|
|
15
20
|
export interface FeedbackReply {
|
|
16
21
|
id: number;
|
|
17
22
|
/** `null`: a loop's reply, a migrated `outcome`, or an erased admin — drawn as the
|
|
18
|
-
* app's word for its admin ("Admin").
|
|
23
|
+
* app's word for its admin ("Admin"). On a rework entry `null` is an erased or
|
|
24
|
+
* unattributed author, drawn as "A user" (`feedbackPublic.anonymous`), never as the
|
|
25
|
+
* admin word. */
|
|
19
26
|
author_name: string | null;
|
|
20
27
|
body: string;
|
|
21
|
-
/**
|
|
28
|
+
/** 0.40 (§10.3): `"reply"` (absent: a server before server-kit 0.13, all replies) or
|
|
29
|
+
* `"rework"`, the author's entry. */
|
|
30
|
+
kind?: FeedbackEntryKind;
|
|
31
|
+
/** 0.40: a rework entry's one file (`/api/v1/feedback/attachments/<key>`) — the admin
|
|
32
|
+
* and author views only; `null` or absent otherwise (never in the public view, §9.10). */
|
|
33
|
+
attachment_url?: string | null;
|
|
34
|
+
/** The status this reply moved the report to; `null` = no status change — and on a
|
|
35
|
+
* rework migrated out of the body (§10.4), whose move was never recorded. */
|
|
22
36
|
status_to: FeedbackStatus | null;
|
|
23
37
|
/** Where it moved from — the admin view only. */
|
|
24
38
|
status_from?: FeedbackStatus | null;
|
|
@@ -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
|
+
}
|