@eifi1/ui-kit 0.46.0 → 0.47.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 +12 -11
- package/dist/admin/user-roster.d.ts +6 -2
- package/dist/admin/user-roster.js +7 -3
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +22 -5
- package/dist/components/data-table-row-actions.js +14 -5
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table.d.ts +16 -1
- package/dist/components/data-table.js +20 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/row-actions.d.ts +37 -5
- package/dist/components/row-actions.js +22 -5
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/tabs.d.ts +24 -16
- package/dist/components/tabs.js +37 -31
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review.js +3 -1
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui-labels.js +1 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js +2 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-download.d.ts +19 -0
- package/dist/feedback/feedback-attachment-download.js +59 -0
- package/dist/feedback/feedback-attachment-download.js.map +1 -0
- package/dist/feedback/feedback-comment-thread.d.ts +17 -6
- package/dist/feedback/feedback-comment-thread.js +21 -2
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-entry-file.d.ts +61 -0
- package/dist/feedback/feedback-entry-file.js +76 -0
- package/dist/feedback/feedback-entry-file.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +18 -5
- package/dist/feedback/feedback-public-list.js +8 -0
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +7 -2
- package/dist/feedback/feedback-replies.d.ts +26 -9
- package/dist/feedback/feedback-replies.js +20 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +0 -1
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +23 -10
- package/dist/feedback/feedback-row-detail.js +10 -54
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/german.js +1 -6
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -6
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +1 -6
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +1 -6
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +1 -6
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +1 -6
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +1 -6
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +0 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/dist/notifications/device-zone-report.d.ts +7 -0
- package/dist/notifications/device-zone-report.js +8 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/feedback-notification-types.d.ts +14 -1
- package/dist/notifications/feedback-notification-types.js +5 -1
- package/dist/notifications/feedback-notification-types.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +1 -1
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +12 -3
- package/dist/notifications/notification-inbox.js +2 -2
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +0 -3
- package/dist/notifications/notifications-labels.d.ts +2 -13
- package/dist/notifications/notifications-labels.js +0 -4
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +3 -8
- package/dist/notifications/quiet-hours-setting.js +8 -3
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/offline/local-first.d.ts +9 -0
- package/dist/offline/local-first.js +14 -0
- package/dist/offline/local-first.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +2 -1
- package/dist/offline.js.map +1 -1
- package/dist/sync/db.d.ts +17 -0
- package/dist/sync/db.js +10 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.d.ts +11 -9
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/hooks.d.ts +5 -0
- package/dist/sync/hooks.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +5 -2
- package/dist/sync.js.map +1 -1
- package/dist/theme/theme-store.d.ts +4 -4
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/package.json +2 -2
- package/src/admin/user-roster.tsx +12 -3
- package/src/components/data-table-row-actions.tsx +33 -9
- package/src/components/data-table.tsx +46 -3
- package/src/components/row-actions.tsx +73 -10
- package/src/components/tabs.tsx +66 -44
- package/src/components/translation-review.tsx +3 -1
- package/src/components/ui-labels.ts +1 -1
- package/src/data-table.ts +3 -0
- package/src/feedback/feedback-attachment-download.tsx +77 -0
- package/src/feedback/feedback-comment-thread.tsx +42 -7
- package/src/feedback/feedback-entry-file.tsx +150 -0
- package/src/feedback/feedback-public-list.tsx +25 -4
- package/src/feedback/feedback-public-types.ts +7 -2
- package/src/feedback/feedback-replies.tsx +46 -19
- package/src/feedback/feedback-row-detail-labels.ts +0 -1
- package/src/feedback/feedback-row-detail.tsx +35 -86
- package/src/feedback.ts +1 -0
- package/src/i18n/german.ts +1 -6
- package/src/i18n/locales/es.ts +1 -6
- package/src/i18n/locales/fr.ts +1 -6
- package/src/i18n/locales/hu.ts +1 -6
- package/src/i18n/locales/it.ts +1 -6
- package/src/i18n/locales/zh.ts +1 -6
- package/src/i18n/review.ts +0 -1
- package/src/index.ts +19 -5
- package/src/notifications/device-zone-report.ts +14 -1
- package/src/notifications/feedback-notification-types.ts +19 -1
- package/src/notifications/notification-bell.tsx +1 -1
- package/src/notifications/notification-inbox.tsx +15 -5
- package/src/notifications/notification-types.ts +0 -3
- package/src/notifications/notifications-labels.ts +2 -17
- package/src/notifications/quiet-hours-setting.tsx +16 -11
- package/src/offline/local-first.ts +24 -0
- package/src/offline.ts +1 -1
- package/src/sync/db.ts +22 -0
- package/src/sync/engine.ts +11 -9
- package/src/sync/hooks.ts +5 -0
- package/src/sync.ts +2 -2
- package/src/theme/theme-store.ts +13 -5
- package/dist/notifications/time-zone-select.d.ts +0 -53
- package/dist/notifications/time-zone-select.js +0 -76
- package/dist/notifications/time-zone-select.js.map +0 -1
- package/src/notifications/time-zone-select.tsx +0 -129
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { CommitScope } from "../components/write-lock.js";
|
|
3
|
+
import type { AuthedFetcher } from "../hooks/use-authed-src.js";
|
|
3
4
|
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
4
5
|
import { type FeedbackStatus } from "./feedback-inbox.js";
|
|
5
6
|
import type { FeedbackReply } from "./feedback-public-types.js";
|
|
7
|
+
import { type FeedbackEntryUploadOptions } from "./feedback-entry-file.js";
|
|
6
8
|
import { DEFAULT_FEEDBACK_REPLIES_LABELS } from "./feedback-thread-parts-labels.js";
|
|
7
9
|
/**
|
|
8
10
|
* The admin's answers as a list (docs/feedback-harmonization.md §9.7, decision 5): each
|
|
@@ -57,10 +59,14 @@ export interface FeedbackReplyTimelineProps {
|
|
|
57
59
|
renderDate?: (iso: string) => string;
|
|
58
60
|
/** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */
|
|
59
61
|
redact?: boolean;
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
/** How an entry's file (`attachment_url` — a rework's since 0.40, an admin reply's
|
|
63
|
+
* since 0.47) is drawn under its text. Default (0.47, §12): {@link FeedbackEntryFile} —
|
|
64
|
+
* a picture inline as a thumbnail that opens full size, any other file a download —
|
|
65
|
+
* through `fetcher`. */
|
|
63
66
|
renderAttachment?: (url: string) => ReactNode;
|
|
67
|
+
/** 0.47: the app's authenticated GET (`AuthedImage`'s) for the entries' files. Left
|
|
68
|
+
* out, the browser's own fetch with same-origin cookies. */
|
|
69
|
+
fetcher?: AuthedFetcher | null;
|
|
64
70
|
labels?: LabelOverride<FeedbackRepliesLabels>;
|
|
65
71
|
className?: string;
|
|
66
72
|
}
|
|
@@ -72,17 +78,21 @@ export interface FeedbackReplyTimelineProps {
|
|
|
72
78
|
* 0.40 (§10.3): an author's **rework** entry (`kind: "rework"`) stands among them as the
|
|
73
79
|
* author's — their name (`null` → "A user", never "Admin"), the **Rework** chip, the
|
|
74
80
|
* status it moved the report to (none on a migrated one) and its file.
|
|
81
|
+
*
|
|
82
|
+
* 0.47 (§12): any entry's file — a rework's, an admin reply's — is drawn under its text,
|
|
83
|
+
* a picture INLINE ({@link FeedbackEntryFile}).
|
|
75
84
|
*/
|
|
76
|
-
export declare function FeedbackReplyTimeline({ replies, renderDate, redact, renderAttachment, labels: labelsProp, className, }: FeedbackReplyTimelineProps): import("react").JSX.Element;
|
|
77
|
-
export interface FeedbackReplyComposerProps {
|
|
85
|
+
export declare function FeedbackReplyTimeline({ replies, renderDate, redact, renderAttachment, fetcher, labels: labelsProp, className, }: FeedbackReplyTimelineProps): import("react").JSX.Element;
|
|
86
|
+
export interface FeedbackReplyComposerProps extends FeedbackEntryUploadOptions {
|
|
78
87
|
/** The report's status now — what "No change" keeps. */
|
|
79
88
|
status: FeedbackStatus;
|
|
80
89
|
/**
|
|
81
90
|
* `POST /feedback/{id}/replies` — the body (1–{@link FEEDBACK_REPLY_MAX} after
|
|
82
|
-
* trimming)
|
|
83
|
-
* the
|
|
91
|
+
* trimming), the status picked, `undefined` for "No change", and (0.47, §12) the URL
|
|
92
|
+
* of the one file uploaded with it, `undefined` for none. Return the promise: the box
|
|
93
|
+
* clears when it resolves, and keeps everything on a rejection.
|
|
84
94
|
*/
|
|
85
|
-
onReply: (body: string, status?: FeedbackStatus) => void | Promise<unknown>;
|
|
95
|
+
onReply: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;
|
|
86
96
|
/** The reply is in flight. */
|
|
87
97
|
pending?: boolean;
|
|
88
98
|
/** Preselect this status and say the author has not been told (§9.7, the nudge) —
|
|
@@ -104,5 +114,12 @@ export interface FeedbackReplyComposerProps {
|
|
|
104
114
|
* With `suggestedStatus` the line "The author has not been told. Add a reply." stands
|
|
105
115
|
* above it and that status is picked, so the reply that answers it records the status
|
|
106
116
|
* and the nudge goes away.
|
|
117
|
+
*
|
|
118
|
+
* 0.47 (§12, keksdose live #447): with `onUpload`, one file as the rework note takes it —
|
|
119
|
+
* a screenshot pasted into the box, a capture (`onCaptureScreenshot`), or a pick; the
|
|
120
|
+
* contract's types and 10 MB. It is uploaded FIRST; a failed upload toasts and sends
|
|
121
|
+
* nothing, and the draft and the file stay. `onUpload` here is the composer's own opt-in
|
|
122
|
+
* (new in 0.47, so no page passed it before); `FeedbackRowDetail` hands it on only with
|
|
123
|
+
* `replyAttachments`, since its `onUpload` is the rework note's too.
|
|
107
124
|
*/
|
|
108
|
-
export declare function FeedbackReplyComposer({ status, onReply, pending, suggestedStatus, commit, labels: labelsProp, className, }: FeedbackReplyComposerProps): import("react").JSX.Element;
|
|
125
|
+
export declare function FeedbackReplyComposer({ status, onReply, pending, suggestedStatus, commit, onUpload, onCaptureScreenshot, attachment, labels: labelsProp, className, }: FeedbackReplyComposerProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { BellOff
|
|
4
|
+
import { BellOff } from "lucide-react";
|
|
5
5
|
import { Select } from "../components/select.js";
|
|
6
6
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
7
7
|
import { cn } from "../lib/cn.js";
|
|
8
8
|
import { formatDate } from "../lib/format.js";
|
|
9
9
|
import { FeedbackStatusBadge, selectableFeedbackStatuses } from "./feedback-inbox.js";
|
|
10
10
|
import { useFeedbackStatusLabels } from "./feedback-labels.js";
|
|
11
|
-
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX
|
|
11
|
+
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX } from "./feedback-record.js";
|
|
12
|
+
import { FeedbackEntryFile, useFeedbackEntryUpload } from "./feedback-entry-file.js";
|
|
12
13
|
import { ChatComposer } from "./feedback-thread.js";
|
|
13
14
|
import { DEFAULT_FEEDBACK_REPLIES_LABELS } from "./feedback-thread-parts-labels.js";
|
|
14
15
|
import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip.js";
|
|
@@ -37,6 +38,7 @@ function FeedbackReplyTimeline({
|
|
|
37
38
|
renderDate,
|
|
38
39
|
redact = true,
|
|
39
40
|
renderAttachment,
|
|
41
|
+
fetcher,
|
|
40
42
|
labels: labelsProp,
|
|
41
43
|
className
|
|
42
44
|
}) {
|
|
@@ -64,10 +66,7 @@ function FeedbackReplyTimeline({
|
|
|
64
66
|
/* @__PURE__ */ jsx(EntryStatus, { reply, statusChanged: labels.statusChanged })
|
|
65
67
|
] }),
|
|
66
68
|
/* @__PURE__ */ jsx("p", { "data-private": priv, dir: "auto", className: "mt-0.5 whitespace-pre-wrap break-words text-start text-primary", children: reply.body }),
|
|
67
|
-
|
|
68
|
-
/* @__PURE__ */ jsx(Paperclip, { className: "size-3.5 shrink-0", "aria-hidden": true }),
|
|
69
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-all", children: attachmentName(reply.attachment_url) })
|
|
70
|
-
] }) })
|
|
69
|
+
reply.attachment_url && /* @__PURE__ */ jsx("div", { className: "mt-1 min-w-0", "data-private": priv, "data-feedback-entry-attachment": "", children: renderAttachment ? renderAttachment(reply.attachment_url) : /* @__PURE__ */ jsx(FeedbackEntryFile, { url: reply.attachment_url, fetcher }) })
|
|
71
70
|
]
|
|
72
71
|
},
|
|
73
72
|
reply.id
|
|
@@ -85,19 +84,29 @@ function FeedbackReplyComposer({
|
|
|
85
84
|
pending = false,
|
|
86
85
|
suggestedStatus,
|
|
87
86
|
commit,
|
|
87
|
+
onUpload,
|
|
88
|
+
onCaptureScreenshot,
|
|
89
|
+
attachment,
|
|
88
90
|
labels: labelsProp,
|
|
89
91
|
className
|
|
90
92
|
}) {
|
|
91
93
|
const labels = useFeedbackRepliesLabels(labelsProp);
|
|
94
|
+
const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({
|
|
95
|
+
onUpload,
|
|
96
|
+
onCaptureScreenshot,
|
|
97
|
+
attachment
|
|
98
|
+
});
|
|
92
99
|
const statusNames = useFeedbackStatusLabels();
|
|
93
100
|
const [pick, setPick] = useState({
|
|
94
101
|
under: suggestedStatus,
|
|
95
102
|
value: suggestedStatus ?? ""
|
|
96
103
|
});
|
|
97
104
|
const picked = pick.under === suggestedStatus ? pick.value : suggestedStatus ?? "";
|
|
98
|
-
const send = (body) => {
|
|
105
|
+
const send = async (body, file) => {
|
|
99
106
|
const sent = picked;
|
|
100
|
-
|
|
107
|
+
const url = await upload(file);
|
|
108
|
+
const replied = url === void 0 ? onReply(body, sent || void 0) : onReply(body, sent || void 0, url);
|
|
109
|
+
return Promise.resolve(replied).then(() => {
|
|
101
110
|
setPick((current) => current.value === sent ? { under: suggestedStatus, value: "" } : current);
|
|
102
111
|
});
|
|
103
112
|
};
|
|
@@ -110,7 +119,8 @@ function FeedbackReplyComposer({
|
|
|
110
119
|
ChatComposer,
|
|
111
120
|
{
|
|
112
121
|
onSend: send,
|
|
113
|
-
pending,
|
|
122
|
+
pending: pending || uploading,
|
|
123
|
+
attachment: composerAttachment,
|
|
114
124
|
commit,
|
|
115
125
|
maxLength: FEEDBACK_REPLY_MAX,
|
|
116
126
|
showCount: true,
|
|
@@ -124,7 +134,7 @@ function FeedbackReplyComposer({
|
|
|
124
134
|
{
|
|
125
135
|
"aria-label": labels.status,
|
|
126
136
|
value: picked,
|
|
127
|
-
disabled: pending,
|
|
137
|
+
disabled: pending || uploading,
|
|
128
138
|
onChange: (event) => setPick({ under: suggestedStatus, value: event.target.value }),
|
|
129
139
|
"data-feedback-reply-status-pick": "",
|
|
130
140
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-replies.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { BellOff, Paperclip } from \"lucide-react\";\nimport { Select } from \"../components/select\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate } from \"../lib/format\";\nimport { FeedbackStatusBadge, selectableFeedbackStatuses, type FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackStatusLabels } from \"./feedback-labels\";\nimport type { FeedbackReply } from \"./feedback-public-types\";\nimport { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX, attachmentName } from \"./feedback-record\";\nimport { ChatComposer } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_REPLIES_LABELS } from \"./feedback-thread-parts-labels\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\n\n/**\n * The admin's answers as a list (docs/feedback-harmonization.md §9.7, decision 5): each\n * reply its own row — who, when, what, and the status it moved the report to — where\n * 0.37 had one `outcome` field that every answer overwrote. {@link FeedbackReplyTimeline}\n * draws them; {@link FeedbackReplyComposer} writes the next one, with the status picker\n * beside it (decision 13: a reply notifies the author, a status alone does not, so the\n * status belongs with the words).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackReplies` namespace of `<UiKitProvider labels>`. */\nexport interface FeedbackRepliesLabels {\n /** The timeline's accessible name. de-CH: \"Antworten\" */\n replies: string;\n /** No reply yet. de-CH: \"Noch keine Antwort\" */\n empty: string;\n /** A reply without an author — a loop's, a migrated outcome, an erased admin (§9.7).\n * The app's word for its admin; never \"the team\". de-CH: \"Admin\" */\n admin: string;\n /** Read before a reply's status chip (\"→ Done\"). de-CH: \"Status geändert auf\" */\n statusChanged: string;\n /** The reply box's accessible name. de-CH: \"Antwort schreiben\" */\n field: string;\n /** de-CH: \"Antwort schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Antworten\" */\n reply: string;\n /** The status picker's accessible name. de-CH: \"Status mit der Antwort\" */\n status: string;\n /** The picker's first option: the reply changes no status. de-CH: \"Keine Änderung\" */\n noChange: string;\n /** Above the composer when a decided status was set without a reply (§9.7, B2).\n * de-CH: \"Die meldende Person wurde nicht benachrichtigt. Schreiben Sie eine Antwort.\" */\n nudge: string;\n}\n\nexport { DEFAULT_FEEDBACK_REPLIES_LABELS };\n\n/** `feedbackReplies` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackRepliesLabels(labels?: LabelOverride<FeedbackRepliesLabels>): FeedbackRepliesLabels {\n return useKitLabels(\"feedbackReplies\", DEFAULT_FEEDBACK_REPLIES_LABELS, labels);\n}\n\n/**\n * The status a row detail should nudge toward (§9.7, B2), or `undefined`: the row's\n * status when it is decided ({@link FEEDBACK_DECIDED_STATUSES}) and no reply carries it\n * as its `status_to` — a status-only move from the table, a swipe or the pills told the\n * author nothing. `undefined` too without `replies` (a server before 0.38).\n */\nexport function feedbackReplyNudge(row: {\n status: FeedbackStatus;\n replies?: readonly Pick<FeedbackReply, \"status_to\">[] | null;\n}): FeedbackStatus | undefined {\n if (!row.replies || !FEEDBACK_DECIDED_STATUSES.includes(row.status)) return undefined;\n return row.replies.some((reply) => reply.status_to === row.status) ? undefined : row.status;\n}\n\n/* ── The timeline ─────────────────────────────────────────────────────────── */\n\nexport interface FeedbackReplyTimelineProps {\n /** Oldest first, as the server sends them. */\n replies: readonly FeedbackReply[];\n /** How a reply's time reads. Default: the date and time in the provider's locale. */\n renderDate?: (iso: string) => string;\n /** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */\n redact?: boolean;\n /** 0.40: how a rework entry's file (`attachment_url`) is drawn under its text. Default:\n * its name beside a paperclip — the file itself is in the row detail's Attachment\n * section, fetched with the app's credentials. */\n renderAttachment?: (url: string) => ReactNode;\n labels?: LabelOverride<FeedbackRepliesLabels>;\n className?: string;\n}\n\n/** The status an entry moved the report to — \"→ Done\", with where from in the admin view.\n * Nothing on an entry with no `status_to`: a status-less reply, or a rework migrated out\n * of the body, whose move was never recorded (0.40, §10.3). */\nfunction EntryStatus({ reply, statusChanged }: { reply: FeedbackReply; statusChanged: string }) {\n if (!reply.status_to) return null;\n return (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{statusChanged}</span>\n {reply.status_from && reply.status_from !== reply.status_to && (\n <>\n <FeedbackStatusBadge status={reply.status_from} />\n <span aria-hidden>→</span>\n </>\n )}\n {!reply.status_from && <span aria-hidden>→</span>}\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n );\n}\n\n/**\n * The replies, oldest first, down a rule: each with its author (`null` → \"Admin\"), its\n * time, the status it set (\"→ Done\"; the admin view adds where from) and its text. No\n * edit and no delete (§9.7): a correction is a new reply.\n *\n * 0.40 (§10.3): an author's **rework** entry (`kind: \"rework\"`) stands among them as the\n * author's — their name (`null` → \"A user\", never \"Admin\"), the **Rework** chip, the\n * status it moved the report to (none on a migrated one) and its file.\n */\nexport function FeedbackReplyTimeline({\n replies,\n renderDate,\n redact = true,\n renderAttachment,\n labels: labelsProp,\n className,\n}: FeedbackReplyTimelineProps) {\n const labels = useFeedbackRepliesLabels(labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const locale = useKitLocale();\n const priv = redact ? \"\" : undefined;\n if (replies.length === 0) {\n return <p className={cn(\"text-sm text-placeholder\", className)}>{labels.empty}</p>;\n }\n const dateText = (iso: string) =>\n renderDate ? renderDate(iso) : formatDate(iso, { dateStyle: \"medium\", timeStyle: \"short\" }, { locale });\n return (\n <ol aria-label={labels.replies} className={cn(\"space-y-3 border-s-2 border-subtle ps-3\", className)}>\n {replies.map((reply) => {\n const rework = reply.kind === \"rework\";\n return (\n <li\n key={reply.id}\n data-feedback-reply={reply.id}\n data-feedback-entry-kind={rework ? \"rework\" : undefined}\n className=\"min-w-0 text-sm\"\n >\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted\">\n <span data-private={priv} className=\"font-medium text-secondary\">\n {reply.author_name ?? (rework ? anonymous : labels.admin)}\n </span>\n {rework && <FeedbackReworkEntryChip />}\n <time dateTime={reply.created_at} className=\"tabular-nums\">\n {dateText(reply.created_at)}\n </time>\n <EntryStatus reply={reply} statusChanged={labels.statusChanged} />\n </div>\n <p data-private={priv} dir=\"auto\" className=\"mt-0.5 whitespace-pre-wrap break-words text-start text-primary\">\n {reply.body}\n </p>\n {rework && reply.attachment_url && (\n <div className=\"mt-1 min-w-0\" data-private={priv} data-feedback-entry-file=\"\">\n {renderAttachment ? (\n renderAttachment(reply.attachment_url)\n ) : (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-1 text-xs text-secondary\">\n <Paperclip className=\"size-3.5 shrink-0\" aria-hidden />\n <span className=\"min-w-0 break-all\">{attachmentName(reply.attachment_url)}</span>\n </span>\n )}\n </div>\n )}\n </li>\n );\n })}\n </ol>\n );\n}\n\n/* ── The composer ─────────────────────────────────────────────────────────── */\n\n/**\n * The status Select and Reply on one line, one height (keksdose live #417, Android, dark).\n *\n * A field and a button of the same padding are not the same height everywhere: on a touch\n * screen the field's type grows to 16 px (FIELD_TOUCH_TEXT, so iOS does not zoom) and its\n * box with it, while the button keeps its 14 px line — and at Large the button takes its\n * 48 px touch floor, which the field has not. So the composer's action row stretches its\n * two halves to one height and Reply fills it (Button's `stretch`, which is the kit's\n * answer for a button beside a taller field). Scoped to this composer through the marks\n * ChatComposer puts on its row: a host slot with an attachment preview must not drag the\n * send button to the preview's 80 px.\n *\n * On a touch screen the \"Ctrl+Enter to send\" hint has no keyboard to speak of, and beside\n * a field-height select it pushed Reply onto a line of its own on a phone — it is\n * visually hidden there (still read in order). At Large the button's 48 px floor is\n * taller than any field, here as in every form row of the kit.\n */\nconst REPLY_ROW_ALIGN = cn(\n \"[&_[data-chat-composer-actions]]:items-stretch\",\n \"[&_[data-chat-composer-send]]:self-stretch\",\n \"pointer-coarse:[&_[data-chat-composer-hint]]:sr-only\",\n);\n\nexport interface FeedbackReplyComposerProps {\n /** The report's status now — what \"No change\" keeps. */\n status: FeedbackStatus;\n /**\n * `POST /feedback/{id}/replies` — the body (1–{@link FEEDBACK_REPLY_MAX} after\n * trimming) and the status picked, `undefined` for \"No change\". Return the promise:\n * the box clears when it resolves, and keeps everything on a rejection.\n */\n onReply: (body: string, status?: FeedbackStatus) => void | Promise<unknown>;\n /** The reply is in flight. */\n pending?: boolean;\n /** Preselect this status and say the author has not been told (§9.7, the nudge) —\n * {@link feedbackReplyNudge}. */\n suggestedStatus?: FeedbackStatus;\n /** Reply COMMITS — see ChatComposer's `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackRepliesLabels>;\n className?: string;\n}\n\n/**\n * The admin's reply box (§9.7): the text, the **status** to set with it (\"No change\"\n * first, then every status — {@link selectableFeedbackStatuses}, `REWORK` among them\n * since 0.40: how an admin sends any report back, their own included, §10.2 decision 6),\n * and **Reply**. A\n * {@link ChatComposer} underneath: Ctrl/⌘+Enter sends, the count runs to 8 000, a draft\n * typed while a reply is in flight stays.\n *\n * With `suggestedStatus` the line \"The author has not been told. Add a reply.\" stands\n * above it and that status is picked, so the reply that answers it records the status\n * and the nudge goes away.\n */\nexport function FeedbackReplyComposer({\n status,\n onReply,\n pending = false,\n suggestedStatus,\n commit,\n labels: labelsProp,\n className,\n}: FeedbackReplyComposerProps) {\n const labels = useFeedbackRepliesLabels(labelsProp);\n const statusNames = useFeedbackStatusLabels();\n // The pick, valid for the suggestion it was made under: a new suggestion (another\n // status set from the table) picks itself again.\n const [pick, setPick] = useState<{ under: FeedbackStatus | undefined; value: FeedbackStatus | \"\" }>({\n under: suggestedStatus,\n value: suggestedStatus ?? \"\",\n });\n const picked = pick.under === suggestedStatus ? pick.value : (suggestedStatus ?? \"\");\n\n const send = (body: string) => {\n const sent = picked;\n return Promise.resolve(onReply(body, sent || undefined)).then(() => {\n setPick((current) => (current.value === sent ? { under: suggestedStatus, value: \"\" } : current));\n });\n };\n\n return (\n <div className={cn(\"space-y-2\", REPLY_ROW_ALIGN, className)} data-feedback-reply-composer=\"\">\n {suggestedStatus && (\n <p className=\"flex items-start gap-1.5 text-xs text-secondary\" data-feedback-nudge={suggestedStatus}>\n <BellOff className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n <span>{labels.nudge}</span>\n </p>\n )}\n <ChatComposer\n onSend={send}\n pending={pending}\n commit={commit}\n maxLength={FEEDBACK_REPLY_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.reply }}\n attachmentSlot={\n // The kit's standard Select at field height (#417: \"not invent new things where\n // there are usable ones already\") — `size=\"sm\"` was a 28 px box beside the\n // 38 px Reply. See REPLY_ROW_ALIGN for how the two keep one height.\n <Select\n aria-label={labels.status}\n value={picked}\n disabled={pending}\n onChange={(event) =>\n setPick({ under: suggestedStatus, value: event.target.value as FeedbackStatus | \"\" })\n }\n data-feedback-reply-status-pick=\"\"\n >\n <option value=\"\">{labels.noChange}</option>\n {selectableFeedbackStatuses(status).map((value) => (\n <option key={value} value={value}>\n {statusNames[value]}\n </option>\n ))}\n </Select>\n }\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-replies.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { BellOff } from \"lucide-react\";\nimport { Select } from \"../components/select\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate } from \"../lib/format\";\nimport { FeedbackStatusBadge, selectableFeedbackStatuses, type FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackStatusLabels } from \"./feedback-labels\";\nimport type { FeedbackReply } from \"./feedback-public-types\";\nimport { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX } from \"./feedback-record\";\nimport { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { ChatComposer } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_REPLIES_LABELS } from \"./feedback-thread-parts-labels\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\n\n/**\n * The admin's answers as a list (docs/feedback-harmonization.md §9.7, decision 5): each\n * reply its own row — who, when, what, and the status it moved the report to — where\n * 0.37 had one `outcome` field that every answer overwrote. {@link FeedbackReplyTimeline}\n * draws them; {@link FeedbackReplyComposer} writes the next one, with the status picker\n * beside it (decision 13: a reply notifies the author, a status alone does not, so the\n * status belongs with the words).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackReplies` namespace of `<UiKitProvider labels>`. */\nexport interface FeedbackRepliesLabels {\n /** The timeline's accessible name. de-CH: \"Antworten\" */\n replies: string;\n /** No reply yet. de-CH: \"Noch keine Antwort\" */\n empty: string;\n /** A reply without an author — a loop's, a migrated outcome, an erased admin (§9.7).\n * The app's word for its admin; never \"the team\". de-CH: \"Admin\" */\n admin: string;\n /** Read before a reply's status chip (\"→ Done\"). de-CH: \"Status geändert auf\" */\n statusChanged: string;\n /** The reply box's accessible name. de-CH: \"Antwort schreiben\" */\n field: string;\n /** de-CH: \"Antwort schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Antworten\" */\n reply: string;\n /** The status picker's accessible name. de-CH: \"Status mit der Antwort\" */\n status: string;\n /** The picker's first option: the reply changes no status. de-CH: \"Keine Änderung\" */\n noChange: string;\n /** Above the composer when a decided status was set without a reply (§9.7, B2).\n * de-CH: \"Die meldende Person wurde nicht benachrichtigt. Schreiben Sie eine Antwort.\" */\n nudge: string;\n}\n\nexport { DEFAULT_FEEDBACK_REPLIES_LABELS };\n\n/** `feedbackReplies` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackRepliesLabels(labels?: LabelOverride<FeedbackRepliesLabels>): FeedbackRepliesLabels {\n return useKitLabels(\"feedbackReplies\", DEFAULT_FEEDBACK_REPLIES_LABELS, labels);\n}\n\n/**\n * The status a row detail should nudge toward (§9.7, B2), or `undefined`: the row's\n * status when it is decided ({@link FEEDBACK_DECIDED_STATUSES}) and no reply carries it\n * as its `status_to` — a status-only move from the table, a swipe or the pills told the\n * author nothing. `undefined` too without `replies` (a server before 0.38).\n */\nexport function feedbackReplyNudge(row: {\n status: FeedbackStatus;\n replies?: readonly Pick<FeedbackReply, \"status_to\">[] | null;\n}): FeedbackStatus | undefined {\n if (!row.replies || !FEEDBACK_DECIDED_STATUSES.includes(row.status)) return undefined;\n return row.replies.some((reply) => reply.status_to === row.status) ? undefined : row.status;\n}\n\n/* ── The timeline ─────────────────────────────────────────────────────────── */\n\nexport interface FeedbackReplyTimelineProps {\n /** Oldest first, as the server sends them. */\n replies: readonly FeedbackReply[];\n /** How a reply's time reads. Default: the date and time in the provider's locale. */\n renderDate?: (iso: string) => string;\n /** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */\n redact?: boolean;\n /** How an entry's file (`attachment_url` — a rework's since 0.40, an admin reply's\n * since 0.47) is drawn under its text. Default (0.47, §12): {@link FeedbackEntryFile} —\n * a picture inline as a thumbnail that opens full size, any other file a download —\n * through `fetcher`. */\n renderAttachment?: (url: string) => ReactNode;\n /** 0.47: the app's authenticated GET (`AuthedImage`'s) for the entries' files. Left\n * out, the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n labels?: LabelOverride<FeedbackRepliesLabels>;\n className?: string;\n}\n\n/** The status an entry moved the report to — \"→ Done\", with where from in the admin view.\n * Nothing on an entry with no `status_to`: a status-less reply, or a rework migrated out\n * of the body, whose move was never recorded (0.40, §10.3). */\nfunction EntryStatus({ reply, statusChanged }: { reply: FeedbackReply; statusChanged: string }) {\n if (!reply.status_to) return null;\n return (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{statusChanged}</span>\n {reply.status_from && reply.status_from !== reply.status_to && (\n <>\n <FeedbackStatusBadge status={reply.status_from} />\n <span aria-hidden>→</span>\n </>\n )}\n {!reply.status_from && <span aria-hidden>→</span>}\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n );\n}\n\n/**\n * The replies, oldest first, down a rule: each with its author (`null` → \"Admin\"), its\n * time, the status it set (\"→ Done\"; the admin view adds where from) and its text. No\n * edit and no delete (§9.7): a correction is a new reply.\n *\n * 0.40 (§10.3): an author's **rework** entry (`kind: \"rework\"`) stands among them as the\n * author's — their name (`null` → \"A user\", never \"Admin\"), the **Rework** chip, the\n * status it moved the report to (none on a migrated one) and its file.\n *\n * 0.47 (§12): any entry's file — a rework's, an admin reply's — is drawn under its text,\n * a picture INLINE ({@link FeedbackEntryFile}).\n */\nexport function FeedbackReplyTimeline({\n replies,\n renderDate,\n redact = true,\n renderAttachment,\n fetcher,\n labels: labelsProp,\n className,\n}: FeedbackReplyTimelineProps) {\n const labels = useFeedbackRepliesLabels(labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const locale = useKitLocale();\n const priv = redact ? \"\" : undefined;\n if (replies.length === 0) {\n return <p className={cn(\"text-sm text-placeholder\", className)}>{labels.empty}</p>;\n }\n const dateText = (iso: string) =>\n renderDate ? renderDate(iso) : formatDate(iso, { dateStyle: \"medium\", timeStyle: \"short\" }, { locale });\n return (\n <ol aria-label={labels.replies} className={cn(\"space-y-3 border-s-2 border-subtle ps-3\", className)}>\n {replies.map((reply) => {\n const rework = reply.kind === \"rework\";\n return (\n <li\n key={reply.id}\n data-feedback-reply={reply.id}\n data-feedback-entry-kind={rework ? \"rework\" : undefined}\n className=\"min-w-0 text-sm\"\n >\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted\">\n <span data-private={priv} className=\"font-medium text-secondary\">\n {reply.author_name ?? (rework ? anonymous : labels.admin)}\n </span>\n {rework && <FeedbackReworkEntryChip />}\n <time dateTime={reply.created_at} className=\"tabular-nums\">\n {dateText(reply.created_at)}\n </time>\n <EntryStatus reply={reply} statusChanged={labels.statusChanged} />\n </div>\n <p data-private={priv} dir=\"auto\" className=\"mt-0.5 whitespace-pre-wrap break-words text-start text-primary\">\n {reply.body}\n </p>\n {reply.attachment_url && (\n <div className=\"mt-1 min-w-0\" data-private={priv} data-feedback-entry-attachment=\"\">\n {renderAttachment ? (\n renderAttachment(reply.attachment_url)\n ) : (\n <FeedbackEntryFile url={reply.attachment_url} fetcher={fetcher} />\n )}\n </div>\n )}\n </li>\n );\n })}\n </ol>\n );\n}\n\n/* ── The composer ─────────────────────────────────────────────────────────── */\n\n/**\n * The status Select and Reply on one line, one height (keksdose live #417, Android, dark).\n *\n * A field and a button of the same padding are not the same height everywhere: on a touch\n * screen the field's type grows to 16 px (FIELD_TOUCH_TEXT, so iOS does not zoom) and its\n * box with it, while the button keeps its 14 px line — and at Large the button takes its\n * 48 px touch floor, which the field has not. So the composer's action row stretches its\n * two halves to one height and Reply fills it (Button's `stretch`, which is the kit's\n * answer for a button beside a taller field). Scoped to this composer through the marks\n * ChatComposer puts on its row: a host slot with an attachment preview must not drag the\n * send button to the preview's 80 px.\n *\n * On a touch screen the \"Ctrl+Enter to send\" hint has no keyboard to speak of, and beside\n * a field-height select it pushed Reply onto a line of its own on a phone — it is\n * visually hidden there (still read in order). At Large the button's 48 px floor is\n * taller than any field, here as in every form row of the kit.\n */\nconst REPLY_ROW_ALIGN = cn(\n \"[&_[data-chat-composer-actions]]:items-stretch\",\n \"[&_[data-chat-composer-send]]:self-stretch\",\n \"pointer-coarse:[&_[data-chat-composer-hint]]:sr-only\",\n);\n\nexport interface FeedbackReplyComposerProps extends FeedbackEntryUploadOptions {\n /** The report's status now — what \"No change\" keeps. */\n status: FeedbackStatus;\n /**\n * `POST /feedback/{id}/replies` — the body (1–{@link FEEDBACK_REPLY_MAX} after\n * trimming), the status picked, `undefined` for \"No change\", and (0.47, §12) the URL\n * of the one file uploaded with it, `undefined` for none. Return the promise: the box\n * clears when it resolves, and keeps everything on a rejection.\n */\n onReply: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;\n /** The reply is in flight. */\n pending?: boolean;\n /** Preselect this status and say the author has not been told (§9.7, the nudge) —\n * {@link feedbackReplyNudge}. */\n suggestedStatus?: FeedbackStatus;\n /** Reply COMMITS — see ChatComposer's `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackRepliesLabels>;\n className?: string;\n}\n\n/**\n * The admin's reply box (§9.7): the text, the **status** to set with it (\"No change\"\n * first, then every status — {@link selectableFeedbackStatuses}, `REWORK` among them\n * since 0.40: how an admin sends any report back, their own included, §10.2 decision 6),\n * and **Reply**. A\n * {@link ChatComposer} underneath: Ctrl/⌘+Enter sends, the count runs to 8 000, a draft\n * typed while a reply is in flight stays.\n *\n * With `suggestedStatus` the line \"The author has not been told. Add a reply.\" stands\n * above it and that status is picked, so the reply that answers it records the status\n * and the nudge goes away.\n *\n * 0.47 (§12, keksdose live #447): with `onUpload`, one file as the rework note takes it —\n * a screenshot pasted into the box, a capture (`onCaptureScreenshot`), or a pick; the\n * contract's types and 10 MB. It is uploaded FIRST; a failed upload toasts and sends\n * nothing, and the draft and the file stay. `onUpload` here is the composer's own opt-in\n * (new in 0.47, so no page passed it before); `FeedbackRowDetail` hands it on only with\n * `replyAttachments`, since its `onUpload` is the rework note's too.\n */\nexport function FeedbackReplyComposer({\n status,\n onReply,\n pending = false,\n suggestedStatus,\n commit,\n onUpload,\n onCaptureScreenshot,\n attachment,\n labels: labelsProp,\n className,\n}: FeedbackReplyComposerProps) {\n const labels = useFeedbackRepliesLabels(labelsProp);\n const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({\n onUpload,\n onCaptureScreenshot,\n attachment,\n });\n const statusNames = useFeedbackStatusLabels();\n // The pick, valid for the suggestion it was made under: a new suggestion (another\n // status set from the table) picks itself again.\n const [pick, setPick] = useState<{ under: FeedbackStatus | undefined; value: FeedbackStatus | \"\" }>({\n under: suggestedStatus,\n value: suggestedStatus ?? \"\",\n });\n const picked = pick.under === suggestedStatus ? pick.value : (suggestedStatus ?? \"\");\n\n const send = async (body: string, file: File | null) => {\n const sent = picked;\n const url = await upload(file);\n // The third argument only with a file, so a text reply calls as before 0.47.\n const replied = url === undefined ? onReply(body, sent || undefined) : onReply(body, sent || undefined, url);\n return Promise.resolve(replied).then(() => {\n setPick((current) => (current.value === sent ? { under: suggestedStatus, value: \"\" } : current));\n });\n };\n\n return (\n <div className={cn(\"space-y-2\", REPLY_ROW_ALIGN, className)} data-feedback-reply-composer=\"\">\n {suggestedStatus && (\n <p className=\"flex items-start gap-1.5 text-xs text-secondary\" data-feedback-nudge={suggestedStatus}>\n <BellOff className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n <span>{labels.nudge}</span>\n </p>\n )}\n <ChatComposer\n onSend={send}\n pending={pending || uploading}\n attachment={composerAttachment}\n commit={commit}\n maxLength={FEEDBACK_REPLY_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.reply }}\n attachmentSlot={\n // The kit's standard Select at field height (#417: \"not invent new things where\n // there are usable ones already\") — `size=\"sm\"` was a 28 px box beside the\n // 38 px Reply. See REPLY_ROW_ALIGN for how the two keep one height.\n <Select\n aria-label={labels.status}\n value={picked}\n disabled={pending || uploading}\n onChange={(event) =>\n setPick({ under: suggestedStatus, value: event.target.value as FeedbackStatus | \"\" })\n }\n data-feedback-reply-status-pick=\"\"\n >\n <option value=\"\">{labels.noChange}</option>\n {selectableFeedbackStatuses(status).map((value) => (\n <option key={value} value={value}>\n {statusNames[value]}\n </option>\n ))}\n </Select>\n }\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -18,7 +18,6 @@ const DEFAULT_FEEDBACK_DETAIL_LABELS = {
|
|
|
18
18
|
outcomeUpdate: "Update",
|
|
19
19
|
outcomePlaceholder: "What was done, decided, or why this won't be addressed.",
|
|
20
20
|
rework: "Rework",
|
|
21
|
-
openPage: "Open page",
|
|
22
21
|
reworkTitle: "Send for rework",
|
|
23
22
|
reworkSend: "Send rework",
|
|
24
23
|
reworkPlaceholder: "What still needs refinement? Any new constraints or change of direction.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-row-detail-labels.ts"],"sourcesContent":["// The English words of ./feedback-row-detail, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-row-detail\n// re-exports them.\n\nimport type { FeedbackDetailLabels } from \"./feedback-row-detail\";\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n replies: \"Replies\",\n resolvedAt: (date) => `Resolved: ${date}`,\n outcomeAdd: \"Add outcome\",\n outcomeUpdate: \"Update\",\n outcomePlaceholder: \"What was done, decided, or why this won't be addressed.\",\n rework: \"Rework\",\n
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-row-detail-labels.ts"],"sourcesContent":["// The English words of ./feedback-row-detail, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-row-detail\n// re-exports them.\n\nimport type { FeedbackDetailLabels } from \"./feedback-row-detail\";\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n replies: \"Replies\",\n resolvedAt: (date) => `Resolved: ${date}`,\n outcomeAdd: \"Add outcome\",\n outcomeUpdate: \"Update\",\n outcomePlaceholder: \"What was done, decided, or why this won't be addressed.\",\n rework: \"Rework\",\n reworkTitle: \"Send for rework\",\n reworkSend: \"Send rework\",\n reworkPlaceholder: \"What still needs refinement? Any new constraints or change of direction.\",\n reworkUploadFailed: \"The file could not be uploaded. The rework was not sent.\",\n status: \"Status\",\n note: \"Note\",\n};\n"],"mappings":";AAOO,MAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,UAAU,CAAC,SAAS,YAAY,IAAI;AAAA,EACpC,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY,CAAC,SAAS,aAAa,IAAI;AAAA,EACvC,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,MAAM;AACR;","names":[]}
|
|
@@ -101,10 +101,6 @@ export interface FeedbackDetailLabels {
|
|
|
101
101
|
outcomePlaceholder: string;
|
|
102
102
|
/** The button that opens section 5 on an answered row. de-CH: "Nacharbeit" */
|
|
103
103
|
rework: string;
|
|
104
|
-
/** The link to the page the report was filed on. de-CH: "Seite öffnen"
|
|
105
|
-
* @deprecated 0.46 — the Replies section's "Open page" button is gone (keksdose live #449:
|
|
106
|
-
* it repeated the URL section's link); removed in 0.47. Unused, kept for one release. */
|
|
107
|
-
openPage: string;
|
|
108
104
|
/** Section 5's heading. de-CH: "Zur Nacharbeit senden" */
|
|
109
105
|
reworkTitle: string;
|
|
110
106
|
/** Section 5's send button. de-CH: "Nacharbeit senden" */
|
|
@@ -284,8 +280,8 @@ export interface FeedbackRowDetailProps {
|
|
|
284
280
|
onUpdate: FeedbackDetailUpdate;
|
|
285
281
|
/** `update.isPending`: the editors' save buttons wait for it. */
|
|
286
282
|
pending?: boolean;
|
|
287
|
-
/** `POST /feedback/attachments` for the rework's one file
|
|
288
|
-
*
|
|
283
|
+
/** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with
|
|
284
|
+
* `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */
|
|
289
285
|
onUpload?: FeedbackUpload;
|
|
290
286
|
/** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go
|
|
291
287
|
* through it, since the attachment route wants the bearer token (§3.5). Left out,
|
|
@@ -298,9 +294,11 @@ export interface FeedbackRowDetailProps {
|
|
|
298
294
|
* page's mutation (its observer outlives the row a status filter removes).
|
|
299
295
|
*/
|
|
300
296
|
statusChange?: FeedbackStatusChange;
|
|
301
|
-
/** For the rework's "Capture screenshot" — see
|
|
297
|
+
/** For the rework's and (0.47, with `replyAttachments`) the reply composer's "Capture screenshot" — see
|
|
298
|
+
* {@link FeedbackReworkSectionProps}. */
|
|
302
299
|
onCaptureScreenshot?: () => Promise<File | null>;
|
|
303
|
-
/** The rework's
|
|
300
|
+
/** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,
|
|
301
|
+
* over the contract's defaults. */
|
|
304
302
|
reworkAttachment?: Partial<FeedbackNoteAttachment>;
|
|
305
303
|
/** How the `resolved_at` date reads — the app's date cell, as a string. Default: the
|
|
306
304
|
* kit's `formatDate` in the provider's locale, `medium` style. */
|
|
@@ -324,8 +322,23 @@ export interface FeedbackRowDetailProps {
|
|
|
324
322
|
* reply composer stands under the timeline. Return the promise: the box clears when it
|
|
325
323
|
* resolves. A failure is the app's to toast — the server's `detail`, else
|
|
326
324
|
* `feedbackToast.replyFailed` (0.38.1).
|
|
325
|
+
*
|
|
326
|
+
* 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the
|
|
327
|
+
* composer takes one file as the rework note does — pasted into the box, captured or
|
|
328
|
+
* picked — uploaded first, and its URL is the third argument (`attachment_url` in the
|
|
329
|
+
* body; server-kit 0.18).
|
|
327
330
|
*/
|
|
328
|
-
onReply?: (body: string, status?: FeedbackStatus) => void | Promise<unknown>;
|
|
331
|
+
onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;
|
|
332
|
+
/**
|
|
333
|
+
* 0.47 (§12): the reply composer takes one file — through `onUpload`,
|
|
334
|
+
* `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:
|
|
335
|
+
* `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of
|
|
336
|
+
* `(body, status)` does not grow a file field whose file its reply would drop (round
|
|
337
|
+
* 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes
|
|
338
|
+
* `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an
|
|
339
|
+
* older server answers the field with a 422).
|
|
340
|
+
*/
|
|
341
|
+
replyAttachments?: boolean;
|
|
329
342
|
/** 0.38: the reply is in flight. */
|
|
330
343
|
replying?: boolean;
|
|
331
344
|
/**
|
|
@@ -374,4 +387,4 @@ export interface FeedbackRowDetailProps {
|
|
|
374
387
|
* collapsing the row closes it. Each editor is keyed by the row's id, so a draft is
|
|
375
388
|
* never thrown away by a list refresh under it.
|
|
376
389
|
*/
|
|
377
|
-
export declare function FeedbackRowDetail({ row, canEdit, viewerId, onUpdate, pending, onUpload, fetcher, statusChange, onCaptureScreenshot, reworkAttachment, renderDate, commit, showSubject, onReply, replying, onRework, comments, onPublicChange, labels: labelsProp, className, }: FeedbackRowDetailProps): import("react").JSX.Element;
|
|
390
|
+
export declare function FeedbackRowDetail({ row, canEdit, viewerId, onUpdate, pending, onUpload, fetcher, statusChange, onCaptureScreenshot, reworkAttachment, renderDate, commit, showSubject, onReply, replyAttachments, replying, onRework, comments, onPublicChange, labels: labelsProp, className, }: FeedbackRowDetailProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { RotateCcw } from "lucide-react";
|
|
5
5
|
import { AuthedImage } from "../components/authed-image.js";
|
|
6
6
|
import { CopyButton } from "../components/copy-button.js";
|
|
7
7
|
import { TextLink } from "../components/text-link.js";
|
|
8
8
|
import { toast } from "../components/toast.js";
|
|
9
9
|
import { Button } from "../components/button.js";
|
|
10
10
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
11
|
-
import { filenameFromDisposition, saveBlob } from "../lib/download.js";
|
|
12
11
|
import { formatDate } from "../lib/format.js";
|
|
13
12
|
import { DEFAULT_MAX_ATTACHMENT_BYTES } from "./feedback-attachment.js";
|
|
14
13
|
import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "./feedback-dialog.js";
|
|
@@ -28,7 +27,6 @@ import {
|
|
|
28
27
|
FEEDBACK_AUTHOR_EDITABLE_STATUSES,
|
|
29
28
|
FEEDBACK_REWORKABLE_STATUSES,
|
|
30
29
|
appendRework,
|
|
31
|
-
attachmentName,
|
|
32
30
|
isImageAttachment,
|
|
33
31
|
replaceDescription,
|
|
34
32
|
splitBodyAttachments,
|
|
@@ -37,6 +35,7 @@ import {
|
|
|
37
35
|
import { useFeedbackStatusUndo } from "./feedback-status-undo.js";
|
|
38
36
|
import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels.js";
|
|
39
37
|
import { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from "./feedback-replies.js";
|
|
38
|
+
import { FeedbackAttachmentDownload, plainFeedbackFetch } from "./feedback-attachment-download.js";
|
|
40
39
|
import { FeedbackPublicSwitch } from "./feedback-public-switch.js";
|
|
41
40
|
import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context.js";
|
|
42
41
|
import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels.js";
|
|
@@ -81,53 +80,6 @@ function feedbackAttachmentUrls(row) {
|
|
|
81
80
|
];
|
|
82
81
|
return [...new Set(urls)];
|
|
83
82
|
}
|
|
84
|
-
async function toBlob(answer) {
|
|
85
|
-
if (typeof Response !== "undefined" && answer instanceof Response) {
|
|
86
|
-
if (!answer.ok) throw new Error(`HTTP ${answer.status}`);
|
|
87
|
-
return answer.blob();
|
|
88
|
-
}
|
|
89
|
-
return answer;
|
|
90
|
-
}
|
|
91
|
-
const plainFetch = (url, { signal }) => fetch(url, { signal, credentials: "same-origin" });
|
|
92
|
-
function AttachmentDownload({
|
|
93
|
-
url,
|
|
94
|
-
fetcher,
|
|
95
|
-
labels
|
|
96
|
-
}) {
|
|
97
|
-
const [busy, setBusy] = useState(false);
|
|
98
|
-
const name = attachmentName(url);
|
|
99
|
-
const download = async (event) => {
|
|
100
|
-
event.stopPropagation();
|
|
101
|
-
setBusy(true);
|
|
102
|
-
try {
|
|
103
|
-
const answer = await fetcher(url, { signal: new AbortController().signal });
|
|
104
|
-
const blob = await toBlob(answer);
|
|
105
|
-
const saveAs = typeof Response !== "undefined" && answer instanceof Response ? filenameFromDisposition(answer.headers.get("Content-Disposition"), name) : name;
|
|
106
|
-
saveBlob(blob, saveAs);
|
|
107
|
-
} catch {
|
|
108
|
-
toast.error(labels.downloadFailed);
|
|
109
|
-
} finally {
|
|
110
|
-
setBusy(false);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
return /* @__PURE__ */ jsxs(
|
|
114
|
-
Button,
|
|
115
|
-
{
|
|
116
|
-
type: "button",
|
|
117
|
-
variant: "secondary",
|
|
118
|
-
size: "sm",
|
|
119
|
-
pending: busy,
|
|
120
|
-
onClick: (event) => void download(event),
|
|
121
|
-
"aria-label": labels.download(name),
|
|
122
|
-
className: "max-w-full",
|
|
123
|
-
children: [
|
|
124
|
-
/* @__PURE__ */ jsx(FileText, { className: "size-3.5 shrink-0", "aria-hidden": true }),
|
|
125
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 max-w-[14rem] truncate-until-large", children: name }),
|
|
126
|
-
/* @__PURE__ */ jsx(Download, { className: "size-3.5 shrink-0", "aria-hidden": true })
|
|
127
|
-
]
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
83
|
function feedbackRowAccess(row, { canEdit, viewerId }) {
|
|
132
84
|
const isAuthor = row.user_id !== null && viewerId !== void 0 && viewerId !== null && viewerId === row.user_id;
|
|
133
85
|
return {
|
|
@@ -245,6 +197,7 @@ function FeedbackRowDetail({
|
|
|
245
197
|
commit,
|
|
246
198
|
showSubject = true,
|
|
247
199
|
onReply,
|
|
200
|
+
replyAttachments = false,
|
|
248
201
|
replying = false,
|
|
249
202
|
onRework,
|
|
250
203
|
comments,
|
|
@@ -264,8 +217,8 @@ function FeedbackRowDetail({
|
|
|
264
217
|
const { description } = splitDescription(row.body);
|
|
265
218
|
const pageHref = feedbackPageHref(row.context);
|
|
266
219
|
const pageUrl = feedbackPageUrl(row.context);
|
|
267
|
-
const files = feedbackAttachmentUrls(row);
|
|
268
|
-
const fetchFile = fetcher ??
|
|
220
|
+
const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: void 0 } : row);
|
|
221
|
+
const fetchFile = fetcher ?? plainFeedbackFetch;
|
|
269
222
|
const dateText = (iso) => renderDate ? renderDate(iso) : formatDate(iso, "medium", { locale });
|
|
270
223
|
const saveDescription = (value) => {
|
|
271
224
|
if (value.trim() === description.trim()) {
|
|
@@ -337,7 +290,7 @@ function FeedbackRowDetail({
|
|
|
337
290
|
className: "max-h-64 max-w-full rounded border border-subtle"
|
|
338
291
|
},
|
|
339
292
|
url
|
|
340
|
-
) : /* @__PURE__ */ jsx(
|
|
293
|
+
) : /* @__PURE__ */ jsx(FeedbackAttachmentDownload, { url, fetcher: fetchFile }, url)
|
|
341
294
|
) }) }),
|
|
342
295
|
/* @__PURE__ */ jsx(
|
|
343
296
|
FeedbackDetailSection,
|
|
@@ -345,13 +298,16 @@ function FeedbackRowDetail({
|
|
|
345
298
|
title: replies ? labels.replies : labels.outcome,
|
|
346
299
|
action: row.resolved_at ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted", children: labels.resolvedAt(dateText(row.resolved_at)) }) : void 0,
|
|
347
300
|
children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
348
|
-
replies ? /* @__PURE__ */ jsx(FeedbackReplyTimeline, { replies }) : /* @__PURE__ */ jsx(FeedbackProse, { children: row.outcome || void 0 }),
|
|
301
|
+
replies ? /* @__PURE__ */ jsx(FeedbackReplyTimeline, { replies, fetcher: fetchFile }) : /* @__PURE__ */ jsx(FeedbackProse, { children: row.outcome || void 0 }),
|
|
349
302
|
composerShown && replies && onReply && /* @__PURE__ */ jsx(
|
|
350
303
|
FeedbackReplyComposer,
|
|
351
304
|
{
|
|
352
305
|
status: row.status,
|
|
353
306
|
onReply,
|
|
354
307
|
pending: replying,
|
|
308
|
+
onUpload: replyAttachments ? onUpload : void 0,
|
|
309
|
+
onCaptureScreenshot: replyAttachments ? onCaptureScreenshot : void 0,
|
|
310
|
+
attachment: replyAttachments ? reworkAttachment : void 0,
|
|
355
311
|
suggestedStatus: feedbackReplyNudge(row),
|
|
356
312
|
commit
|
|
357
313
|
},
|