@eifi1/ui-kit 0.46.1 → 0.48.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 -6
- package/dist/admin/admin-sections.d.ts +8 -0
- package/dist/admin/admin-sections.js +2 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +22 -13
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +29 -1
- package/dist/components/alert-banner.js +84 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -0
- package/dist/components/button-look.js +2 -0
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox-core.js +5 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +6 -1
- package/dist/components/data-table-column-settings.js +12 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +44 -0
- package/dist/components/data-table-fit.js +103 -0
- package/dist/components/data-table-fit.js.map +1 -0
- package/dist/components/data-table-groups.d.ts +95 -0
- package/dist/components/data-table-groups.js +165 -0
- package/dist/components/data-table-groups.js.map +1 -0
- package/dist/components/data-table-head.js +3 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +7 -0
- package/dist/components/data-table-labels.js +3 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +17 -0
- package/dist/components/data-table-leading-row.js +41 -0
- package/dist/components/data-table-leading-row.js.map +1 -0
- package/dist/components/data-table-resize.js +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table.d.ts +78 -2
- package/dist/components/data-table.js +355 -247
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/disclosure.d.ts +3 -1
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +8 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/floating-panel.d.ts +14 -1
- package/dist/components/floating-panel.js +7 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/hover-menu.js +5 -1
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +2 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/popover.js +5 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +25 -0
- package/dist/components/row-actions.js +13 -3
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/table-skeleton.d.ts +35 -0
- package/dist/components/table-skeleton.js +75 -0
- package/dist/components/table-skeleton.js.map +1 -0
- package/dist/components/tabs.d.ts +22 -16
- package/dist/components/tabs.js +47 -31
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/ui-labels.js +1 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/components/use-table-state.d.ts +8 -7
- package/dist/components/use-table-state.js +37 -3
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -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/feedback-table.d.ts +5 -0
- package/dist/feedback/feedback-table.js +30 -7
- package/dist/feedback/feedback-table.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/hooks/use-dismiss.d.ts +30 -1
- package/dist/hooks/use-dismiss.js +30 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/german.js +7 -7
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +7 -7
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +7 -7
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +7 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +7 -7
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +7 -7
- 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/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/float-clearance.d.ts +8 -0
- package/dist/lib/float-clearance.js +29 -0
- package/dist/lib/float-clearance.js.map +1 -0
- package/dist/notifications/device-zone-report.d.ts +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +0 -3
- package/dist/notifications/notifications-labels.d.ts +2 -15
- 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/settings/settings-catalogue.d.ts +10 -0
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.js +10 -3
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/app-shell.d.ts +5 -0
- package/dist/shell/app-shell.js +5 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +2 -0
- package/dist/shell/option-switcher-menu.js +6 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +5 -3
- package/dist/shell/topbar-controls.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/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +2 -2
- package/src/admin/admin-sections.tsx +8 -0
- package/src/auth/verify-email.tsx +34 -9
- package/src/components/alert-banner.tsx +131 -7
- package/src/components/button-look.ts +6 -0
- package/src/components/combobox-core.tsx +7 -1
- package/src/components/data-table-column-settings.tsx +23 -2
- package/src/components/data-table-fit.ts +187 -0
- package/src/components/data-table-groups.tsx +275 -0
- package/src/components/data-table-head.tsx +4 -1
- package/src/components/data-table-labels.ts +13 -0
- package/src/components/data-table-leading-row.tsx +55 -0
- package/src/components/data-table-resize.ts +2 -1
- package/src/components/data-table.tsx +274 -45
- package/src/components/disclosure.tsx +22 -5
- package/src/components/dropdown.tsx +11 -1
- package/src/components/floating-panel.tsx +24 -1
- package/src/components/hover-menu.tsx +6 -1
- package/src/components/icon-button.tsx +3 -3
- package/src/components/popover.tsx +7 -1
- package/src/components/row-actions.tsx +42 -3
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +102 -50
- package/src/components/ui-labels.ts +1 -1
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -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/feedback-table.tsx +33 -1
- package/src/feedback.ts +1 -0
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +6 -6
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +6 -6
- package/src/i18n/locales/fr.ts +6 -6
- package/src/i18n/locales/hu.ts +6 -6
- package/src/i18n/locales/it.ts +6 -6
- package/src/i18n/locales/zh.ts +6 -6
- package/src/i18n/review.ts +0 -1
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +13 -3
- package/src/lib/float-clearance.ts +54 -0
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-types.ts +0 -3
- package/src/notifications/notifications-labels.ts +2 -19
- package/src/notifications/quiet-hours-setting.tsx +16 -11
- package/src/settings/settings-catalogue.ts +10 -0
- package/src/settings/settings-layout.tsx +18 -4
- package/src/shell/app-shell.tsx +17 -3
- package/src/shell/option-switcher-menu.tsx +16 -4
- package/src/shell/topbar-action-menu.tsx +22 -10
- package/src/shell/topbar-controls.tsx +18 -12
- package/src/theme/theme-store.ts +13 -5
- package/src/tour/tour.tsx +5 -0
- package/dist/notifications/time-zone-select.d.ts +0 -54
- package/dist/notifications/time-zone-select.js +0 -71
- package/dist/notifications/time-zone-select.js.map +0 -1
- package/src/notifications/time-zone-select.tsx +0 -125
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { BellOff
|
|
3
|
+
import { BellOff } from "lucide-react";
|
|
4
4
|
import { Select } from "../components/select";
|
|
5
5
|
import type { CommitScope } from "../components/write-lock";
|
|
6
|
+
import type { AuthedFetcher } from "../hooks/use-authed-src";
|
|
6
7
|
import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
7
8
|
import { cn } from "../lib/cn";
|
|
8
9
|
import { formatDate } from "../lib/format";
|
|
9
10
|
import { FeedbackStatusBadge, selectableFeedbackStatuses, type FeedbackStatus } from "./feedback-inbox";
|
|
10
11
|
import { useFeedbackStatusLabels } from "./feedback-labels";
|
|
11
12
|
import type { FeedbackReply } from "./feedback-public-types";
|
|
12
|
-
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX
|
|
13
|
+
import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX } from "./feedback-record";
|
|
14
|
+
import { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from "./feedback-entry-file";
|
|
13
15
|
import { ChatComposer } from "./feedback-thread";
|
|
14
16
|
import { DEFAULT_FEEDBACK_REPLIES_LABELS } from "./feedback-thread-parts-labels";
|
|
15
17
|
import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip";
|
|
@@ -82,10 +84,14 @@ export interface FeedbackReplyTimelineProps {
|
|
|
82
84
|
renderDate?: (iso: string) => string;
|
|
83
85
|
/** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */
|
|
84
86
|
redact?: boolean;
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
87
|
+
/** How an entry's file (`attachment_url` — a rework's since 0.40, an admin reply's
|
|
88
|
+
* since 0.47) is drawn under its text. Default (0.47, §12): {@link FeedbackEntryFile} —
|
|
89
|
+
* a picture inline as a thumbnail that opens full size, any other file a download —
|
|
90
|
+
* through `fetcher`. */
|
|
88
91
|
renderAttachment?: (url: string) => ReactNode;
|
|
92
|
+
/** 0.47: the app's authenticated GET (`AuthedImage`'s) for the entries' files. Left
|
|
93
|
+
* out, the browser's own fetch with same-origin cookies. */
|
|
94
|
+
fetcher?: AuthedFetcher | null;
|
|
89
95
|
labels?: LabelOverride<FeedbackRepliesLabels>;
|
|
90
96
|
className?: string;
|
|
91
97
|
}
|
|
@@ -118,12 +124,16 @@ function EntryStatus({ reply, statusChanged }: { reply: FeedbackReply; statusCha
|
|
|
118
124
|
* 0.40 (§10.3): an author's **rework** entry (`kind: "rework"`) stands among them as the
|
|
119
125
|
* author's — their name (`null` → "A user", never "Admin"), the **Rework** chip, the
|
|
120
126
|
* status it moved the report to (none on a migrated one) and its file.
|
|
127
|
+
*
|
|
128
|
+
* 0.47 (§12): any entry's file — a rework's, an admin reply's — is drawn under its text,
|
|
129
|
+
* a picture INLINE ({@link FeedbackEntryFile}).
|
|
121
130
|
*/
|
|
122
131
|
export function FeedbackReplyTimeline({
|
|
123
132
|
replies,
|
|
124
133
|
renderDate,
|
|
125
134
|
redact = true,
|
|
126
135
|
renderAttachment,
|
|
136
|
+
fetcher,
|
|
127
137
|
labels: labelsProp,
|
|
128
138
|
className,
|
|
129
139
|
}: FeedbackReplyTimelineProps) {
|
|
@@ -160,15 +170,12 @@ export function FeedbackReplyTimeline({
|
|
|
160
170
|
<p data-private={priv} dir="auto" className="mt-0.5 whitespace-pre-wrap break-words text-start text-primary">
|
|
161
171
|
{reply.body}
|
|
162
172
|
</p>
|
|
163
|
-
{
|
|
164
|
-
<div className="mt-1 min-w-0" data-private={priv} data-feedback-entry-
|
|
173
|
+
{reply.attachment_url && (
|
|
174
|
+
<div className="mt-1 min-w-0" data-private={priv} data-feedback-entry-attachment="">
|
|
165
175
|
{renderAttachment ? (
|
|
166
176
|
renderAttachment(reply.attachment_url)
|
|
167
177
|
) : (
|
|
168
|
-
<
|
|
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>
|
|
178
|
+
<FeedbackEntryFile url={reply.attachment_url} fetcher={fetcher} />
|
|
172
179
|
)}
|
|
173
180
|
</div>
|
|
174
181
|
)}
|
|
@@ -204,15 +211,16 @@ const REPLY_ROW_ALIGN = cn(
|
|
|
204
211
|
"pointer-coarse:[&_[data-chat-composer-hint]]:sr-only",
|
|
205
212
|
);
|
|
206
213
|
|
|
207
|
-
export interface FeedbackReplyComposerProps {
|
|
214
|
+
export interface FeedbackReplyComposerProps extends FeedbackEntryUploadOptions {
|
|
208
215
|
/** The report's status now — what "No change" keeps. */
|
|
209
216
|
status: FeedbackStatus;
|
|
210
217
|
/**
|
|
211
218
|
* `POST /feedback/{id}/replies` — the body (1–{@link FEEDBACK_REPLY_MAX} after
|
|
212
|
-
* trimming)
|
|
213
|
-
* the
|
|
219
|
+
* trimming), the status picked, `undefined` for "No change", and (0.47, §12) the URL
|
|
220
|
+
* of the one file uploaded with it, `undefined` for none. Return the promise: the box
|
|
221
|
+
* clears when it resolves, and keeps everything on a rejection.
|
|
214
222
|
*/
|
|
215
|
-
onReply: (body: string, status?: FeedbackStatus) => void | Promise<unknown>;
|
|
223
|
+
onReply: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;
|
|
216
224
|
/** The reply is in flight. */
|
|
217
225
|
pending?: boolean;
|
|
218
226
|
/** Preselect this status and say the author has not been told (§9.7, the nudge) —
|
|
@@ -235,6 +243,13 @@ export interface FeedbackReplyComposerProps {
|
|
|
235
243
|
* With `suggestedStatus` the line "The author has not been told. Add a reply." stands
|
|
236
244
|
* above it and that status is picked, so the reply that answers it records the status
|
|
237
245
|
* and the nudge goes away.
|
|
246
|
+
*
|
|
247
|
+
* 0.47 (§12, keksdose live #447): with `onUpload`, one file as the rework note takes it —
|
|
248
|
+
* a screenshot pasted into the box, a capture (`onCaptureScreenshot`), or a pick; the
|
|
249
|
+
* contract's types and 10 MB. It is uploaded FIRST; a failed upload toasts and sends
|
|
250
|
+
* nothing, and the draft and the file stay. `onUpload` here is the composer's own opt-in
|
|
251
|
+
* (new in 0.47, so no page passed it before); `FeedbackRowDetail` hands it on only with
|
|
252
|
+
* `replyAttachments`, since its `onUpload` is the rework note's too.
|
|
238
253
|
*/
|
|
239
254
|
export function FeedbackReplyComposer({
|
|
240
255
|
status,
|
|
@@ -242,10 +257,18 @@ export function FeedbackReplyComposer({
|
|
|
242
257
|
pending = false,
|
|
243
258
|
suggestedStatus,
|
|
244
259
|
commit,
|
|
260
|
+
onUpload,
|
|
261
|
+
onCaptureScreenshot,
|
|
262
|
+
attachment,
|
|
245
263
|
labels: labelsProp,
|
|
246
264
|
className,
|
|
247
265
|
}: FeedbackReplyComposerProps) {
|
|
248
266
|
const labels = useFeedbackRepliesLabels(labelsProp);
|
|
267
|
+
const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({
|
|
268
|
+
onUpload,
|
|
269
|
+
onCaptureScreenshot,
|
|
270
|
+
attachment,
|
|
271
|
+
});
|
|
249
272
|
const statusNames = useFeedbackStatusLabels();
|
|
250
273
|
// The pick, valid for the suggestion it was made under: a new suggestion (another
|
|
251
274
|
// status set from the table) picks itself again.
|
|
@@ -255,9 +278,12 @@ export function FeedbackReplyComposer({
|
|
|
255
278
|
});
|
|
256
279
|
const picked = pick.under === suggestedStatus ? pick.value : (suggestedStatus ?? "");
|
|
257
280
|
|
|
258
|
-
const send = (body: string) => {
|
|
281
|
+
const send = async (body: string, file: File | null) => {
|
|
259
282
|
const sent = picked;
|
|
260
|
-
|
|
283
|
+
const url = await upload(file);
|
|
284
|
+
// The third argument only with a file, so a text reply calls as before 0.47.
|
|
285
|
+
const replied = url === undefined ? onReply(body, sent || undefined) : onReply(body, sent || undefined, url);
|
|
286
|
+
return Promise.resolve(replied).then(() => {
|
|
261
287
|
setPick((current) => (current.value === sent ? { under: suggestedStatus, value: "" } : current));
|
|
262
288
|
});
|
|
263
289
|
};
|
|
@@ -272,7 +298,8 @@ export function FeedbackReplyComposer({
|
|
|
272
298
|
)}
|
|
273
299
|
<ChatComposer
|
|
274
300
|
onSend={send}
|
|
275
|
-
pending={pending}
|
|
301
|
+
pending={pending || uploading}
|
|
302
|
+
attachment={composerAttachment}
|
|
276
303
|
commit={commit}
|
|
277
304
|
maxLength={FEEDBACK_REPLY_MAX}
|
|
278
305
|
showCount
|
|
@@ -284,7 +311,7 @@ export function FeedbackReplyComposer({
|
|
|
284
311
|
<Select
|
|
285
312
|
aria-label={labels.status}
|
|
286
313
|
value={picked}
|
|
287
|
-
disabled={pending}
|
|
314
|
+
disabled={pending || uploading}
|
|
288
315
|
onChange={(event) =>
|
|
289
316
|
setPick({ under: suggestedStatus, value: event.target.value as FeedbackStatus | "" })
|
|
290
317
|
}
|
|
@@ -24,7 +24,6 @@ export const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {
|
|
|
24
24
|
outcomeUpdate: "Update",
|
|
25
25
|
outcomePlaceholder: "What was done, decided, or why this won't be addressed.",
|
|
26
26
|
rework: "Rework",
|
|
27
|
-
openPage: "Open page",
|
|
28
27
|
reworkTitle: "Send for rework",
|
|
29
28
|
reworkSend: "Send rework",
|
|
30
29
|
reworkPlaceholder: "What still needs refinement? Any new constraints or change of direction.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { RotateCcw } from "lucide-react";
|
|
4
4
|
import { AuthedImage } from "../components/authed-image";
|
|
5
5
|
import { CopyButton } from "../components/copy-button";
|
|
6
6
|
import { TextLink } from "../components/text-link";
|
|
@@ -8,7 +8,6 @@ import { toast } from "../components/toast";
|
|
|
8
8
|
import { Button } from "../components/button";
|
|
9
9
|
import type { AuthedFetcher } from "../hooks/use-authed-src";
|
|
10
10
|
import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
11
|
-
import { filenameFromDisposition, saveBlob } from "../lib/download";
|
|
12
11
|
import { formatDate } from "../lib/format";
|
|
13
12
|
import { DEFAULT_MAX_ATTACHMENT_BYTES } from "./feedback-attachment";
|
|
14
13
|
import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "./feedback-dialog";
|
|
@@ -29,7 +28,6 @@ import {
|
|
|
29
28
|
FEEDBACK_AUTHOR_EDITABLE_STATUSES,
|
|
30
29
|
FEEDBACK_REWORKABLE_STATUSES,
|
|
31
30
|
appendRework,
|
|
32
|
-
attachmentName,
|
|
33
31
|
isImageAttachment,
|
|
34
32
|
replaceDescription,
|
|
35
33
|
splitBodyAttachments,
|
|
@@ -41,6 +39,7 @@ import type { FeedbackStatusChange } from "./feedback-status-undo";
|
|
|
41
39
|
import type { CommitScope } from "../components/write-lock";
|
|
42
40
|
import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels";
|
|
43
41
|
import { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from "./feedback-replies";
|
|
42
|
+
import { FeedbackAttachmentDownload, plainFeedbackFetch } from "./feedback-attachment-download";
|
|
44
43
|
import { FeedbackPublicSwitch } from "./feedback-public-switch";
|
|
45
44
|
import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context";
|
|
46
45
|
import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels";
|
|
@@ -143,10 +142,6 @@ export interface FeedbackDetailLabels {
|
|
|
143
142
|
outcomePlaceholder: string;
|
|
144
143
|
/** The button that opens section 5 on an answered row. de-CH: "Nacharbeit" */
|
|
145
144
|
rework: string;
|
|
146
|
-
/** The link to the page the report was filed on. de-CH: "Seite öffnen"
|
|
147
|
-
* @deprecated 0.46 — the Replies section's "Open page" button is gone (keksdose live #449:
|
|
148
|
-
* it repeated the URL section's link); removed in 0.47. Unused, kept for one release. */
|
|
149
|
-
openPage: string;
|
|
150
145
|
/** Section 5's heading. de-CH: "Zur Nacharbeit senden" */
|
|
151
146
|
reworkTitle: string;
|
|
152
147
|
/** Section 5's send button. de-CH: "Nacharbeit senden" */
|
|
@@ -257,75 +252,6 @@ export function feedbackAttachmentUrls(
|
|
|
257
252
|
return [...new Set(urls)];
|
|
258
253
|
}
|
|
259
254
|
|
|
260
|
-
/** The bytes behind a fetcher's answer — a `Blob` as it is, a `Response` checked. */
|
|
261
|
-
async function toBlob(answer: Blob | Response): Promise<Blob> {
|
|
262
|
-
if (typeof Response !== "undefined" && answer instanceof Response) {
|
|
263
|
-
if (!answer.ok) throw new Error(`HTTP ${answer.status}`);
|
|
264
|
-
return answer.blob();
|
|
265
|
-
}
|
|
266
|
-
return answer as Blob;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** No fetcher: the browser's own `fetch`, with the cookies a same-origin app uses. */
|
|
270
|
-
const plainFetch: AuthedFetcher = (url, { signal }) => fetch(url, { signal, credentials: "same-origin" });
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* A PDF or a text log (keksdose `FeedbackFileDownload`, dev #578): the bytes sit behind
|
|
274
|
-
* an authenticated route that serves them as `attachment`, never inline (§3.5), so a
|
|
275
|
-
* plain link would be a 401 the moment it was opened. Fetched through the app's
|
|
276
|
-
* fetcher and saved with {@link saveBlob}; a failure says so in a toast.
|
|
277
|
-
*
|
|
278
|
-
* Saved under the name the server gives it when the fetcher answers with a `Response`
|
|
279
|
-
* (server-kit's `content_disposition` puts the uploader's own name in `filename*`), and
|
|
280
|
-
* under the URL's last segment — the name on the button — when it answers with a bare
|
|
281
|
-
* `Blob`, which has no headers to ask.
|
|
282
|
-
*/
|
|
283
|
-
function AttachmentDownload({
|
|
284
|
-
url,
|
|
285
|
-
fetcher,
|
|
286
|
-
labels,
|
|
287
|
-
}: {
|
|
288
|
-
url: string;
|
|
289
|
-
fetcher: AuthedFetcher;
|
|
290
|
-
labels: FeedbackDetailLabels;
|
|
291
|
-
}) {
|
|
292
|
-
const [busy, setBusy] = useState(false);
|
|
293
|
-
const name = attachmentName(url);
|
|
294
|
-
const download = async (event: MouseEvent) => {
|
|
295
|
-
// The row around the detail may toggle on a click.
|
|
296
|
-
event.stopPropagation();
|
|
297
|
-
setBusy(true);
|
|
298
|
-
try {
|
|
299
|
-
const answer = await fetcher(url, { signal: new AbortController().signal });
|
|
300
|
-
const blob = await toBlob(answer);
|
|
301
|
-
const saveAs =
|
|
302
|
-
typeof Response !== "undefined" && answer instanceof Response
|
|
303
|
-
? filenameFromDisposition(answer.headers.get("Content-Disposition"), name)
|
|
304
|
-
: name;
|
|
305
|
-
saveBlob(blob, saveAs);
|
|
306
|
-
} catch {
|
|
307
|
-
toast.error(labels.downloadFailed);
|
|
308
|
-
} finally {
|
|
309
|
-
setBusy(false);
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
return (
|
|
313
|
-
<Button
|
|
314
|
-
type="button"
|
|
315
|
-
variant="secondary"
|
|
316
|
-
size="sm"
|
|
317
|
-
pending={busy}
|
|
318
|
-
onClick={(event) => void download(event)}
|
|
319
|
-
aria-label={labels.download(name)}
|
|
320
|
-
className="max-w-full"
|
|
321
|
-
>
|
|
322
|
-
<FileText className="size-3.5 shrink-0" aria-hidden />
|
|
323
|
-
<span className="min-w-0 max-w-[14rem] truncate-until-large">{name}</span>
|
|
324
|
-
<Download className="size-3.5 shrink-0" aria-hidden />
|
|
325
|
-
</Button>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
255
|
/* ── Who may do what (§3.4, §4.3, §4.4) ───────────────────────────────────── */
|
|
330
256
|
|
|
331
257
|
/** What the person looking at a row may do with it — {@link feedbackRowAccess}. */
|
|
@@ -589,8 +515,8 @@ export interface FeedbackRowDetailProps {
|
|
|
589
515
|
onUpdate: FeedbackDetailUpdate;
|
|
590
516
|
/** `update.isPending`: the editors' save buttons wait for it. */
|
|
591
517
|
pending?: boolean;
|
|
592
|
-
/** `POST /feedback/attachments` for the rework's one file
|
|
593
|
-
*
|
|
518
|
+
/** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with
|
|
519
|
+
* `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */
|
|
594
520
|
onUpload?: FeedbackUpload;
|
|
595
521
|
/** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go
|
|
596
522
|
* through it, since the attachment route wants the bearer token (§3.5). Left out,
|
|
@@ -603,9 +529,11 @@ export interface FeedbackRowDetailProps {
|
|
|
603
529
|
* page's mutation (its observer outlives the row a status filter removes).
|
|
604
530
|
*/
|
|
605
531
|
statusChange?: FeedbackStatusChange;
|
|
606
|
-
/** For the rework's "Capture screenshot" — see
|
|
532
|
+
/** For the rework's and (0.47, with `replyAttachments`) the reply composer's "Capture screenshot" — see
|
|
533
|
+
* {@link FeedbackReworkSectionProps}. */
|
|
607
534
|
onCaptureScreenshot?: () => Promise<File | null>;
|
|
608
|
-
/** The rework's
|
|
535
|
+
/** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,
|
|
536
|
+
* over the contract's defaults. */
|
|
609
537
|
reworkAttachment?: Partial<FeedbackNoteAttachment>;
|
|
610
538
|
/** How the `resolved_at` date reads — the app's date cell, as a string. Default: the
|
|
611
539
|
* kit's `formatDate` in the provider's locale, `medium` style. */
|
|
@@ -629,8 +557,23 @@ export interface FeedbackRowDetailProps {
|
|
|
629
557
|
* reply composer stands under the timeline. Return the promise: the box clears when it
|
|
630
558
|
* resolves. A failure is the app's to toast — the server's `detail`, else
|
|
631
559
|
* `feedbackToast.replyFailed` (0.38.1).
|
|
560
|
+
*
|
|
561
|
+
* 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the
|
|
562
|
+
* composer takes one file as the rework note does — pasted into the box, captured or
|
|
563
|
+
* picked — uploaded first, and its URL is the third argument (`attachment_url` in the
|
|
564
|
+
* body; server-kit 0.18).
|
|
565
|
+
*/
|
|
566
|
+
onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;
|
|
567
|
+
/**
|
|
568
|
+
* 0.47 (§12): the reply composer takes one file — through `onUpload`,
|
|
569
|
+
* `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:
|
|
570
|
+
* `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of
|
|
571
|
+
* `(body, status)` does not grow a file field whose file its reply would drop (round
|
|
572
|
+
* 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes
|
|
573
|
+
* `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an
|
|
574
|
+
* older server answers the field with a 422).
|
|
632
575
|
*/
|
|
633
|
-
|
|
576
|
+
replyAttachments?: boolean;
|
|
634
577
|
/** 0.38: the reply is in flight. */
|
|
635
578
|
replying?: boolean;
|
|
636
579
|
/**
|
|
@@ -695,6 +638,7 @@ export function FeedbackRowDetail({
|
|
|
695
638
|
commit,
|
|
696
639
|
showSubject = true,
|
|
697
640
|
onReply,
|
|
641
|
+
replyAttachments = false,
|
|
698
642
|
replying = false,
|
|
699
643
|
onRework,
|
|
700
644
|
comments,
|
|
@@ -719,8 +663,10 @@ export function FeedbackRowDetail({
|
|
|
719
663
|
const { description } = splitDescription(row.body);
|
|
720
664
|
const pageHref = feedbackPageHref(row.context);
|
|
721
665
|
const pageUrl = feedbackPageUrl(row.context);
|
|
722
|
-
|
|
723
|
-
|
|
666
|
+
// 0.47 (§12): with the entries, each entry's file is drawn inline in the Replies
|
|
667
|
+
// timeline, so the Attachment section holds the report's own files.
|
|
668
|
+
const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: undefined } : row);
|
|
669
|
+
const fetchFile = fetcher ?? plainFeedbackFetch;
|
|
724
670
|
const dateText = (iso: string) => (renderDate ? renderDate(iso) : formatDate(iso, "medium", { locale }));
|
|
725
671
|
|
|
726
672
|
const saveDescription = (value: string) => {
|
|
@@ -823,7 +769,7 @@ export function FeedbackRowDetail({
|
|
|
823
769
|
className="max-h-64 max-w-full rounded border border-subtle"
|
|
824
770
|
/>
|
|
825
771
|
) : (
|
|
826
|
-
<
|
|
772
|
+
<FeedbackAttachmentDownload key={url} url={url} fetcher={fetchFile} />
|
|
827
773
|
),
|
|
828
774
|
)}
|
|
829
775
|
</div>
|
|
@@ -841,7 +787,7 @@ export function FeedbackRowDetail({
|
|
|
841
787
|
>
|
|
842
788
|
<div className="space-y-3">
|
|
843
789
|
{replies ? (
|
|
844
|
-
<FeedbackReplyTimeline replies={replies} />
|
|
790
|
+
<FeedbackReplyTimeline replies={replies} fetcher={fetchFile} />
|
|
845
791
|
) : (
|
|
846
792
|
<FeedbackProse>{row.outcome || undefined}</FeedbackProse>
|
|
847
793
|
)}
|
|
@@ -851,6 +797,9 @@ export function FeedbackRowDetail({
|
|
|
851
797
|
status={row.status}
|
|
852
798
|
onReply={onReply}
|
|
853
799
|
pending={replying}
|
|
800
|
+
onUpload={replyAttachments ? onUpload : undefined}
|
|
801
|
+
onCaptureScreenshot={replyAttachments ? onCaptureScreenshot : undefined}
|
|
802
|
+
attachment={replyAttachments ? reworkAttachment : undefined}
|
|
854
803
|
suggestedStatus={feedbackReplyNudge(row)}
|
|
855
804
|
commit={commit}
|
|
856
805
|
/>
|
|
@@ -7,6 +7,7 @@ import { EMPTY_FORMATTED_VALUE, formatDate, toDate } from "../lib/format";
|
|
|
7
7
|
import { useKitDateFormatter, useKitLabels, useKitLink, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
8
8
|
import type { KitLinkProps } from "../i18n/kit-labels";
|
|
9
9
|
import { Chip } from "../components/chip";
|
|
10
|
+
import { Tooltip } from "../components/tooltip";
|
|
10
11
|
import { FloatingAction, FloatingActionGroup } from "../components/floating-panel";
|
|
11
12
|
import type { FloatingCorner } from "../components/floating-panel";
|
|
12
13
|
import { pickLinkRenderer } from "../components/text-link";
|
|
@@ -449,6 +450,11 @@ function statusRank(status: FeedbackStatus): number {
|
|
|
449
450
|
* filter that never offered it.
|
|
450
451
|
* - The status column owns the status filter, and only it (keksdose live #330's revert:
|
|
451
452
|
* the phone's filter sheet already exposes every column filter).
|
|
453
|
+
* - Fitted to the table's width (0.48, keksdose live #451): Subject `grow`s and
|
|
454
|
+
* ellipsises, and a narrow table leaves out URL, then Email, then Resolved
|
|
455
|
+
* (`fitPriority` 1–3) — the open report repeats all three — then Votes and Category
|
|
456
|
+
* (4, 5) before Subject would drop below 10rem. Drop the two keys with
|
|
457
|
+
* `{ ...col, grow: undefined, fitPriority: undefined }` for the 0.47 layout.
|
|
452
458
|
* - `/my-feedback` shows the same nine: the Email cell's context fallback exists for it
|
|
453
459
|
* (the admin list's join is absent there, §3.1), and its phone card drops the submitter
|
|
454
460
|
* instead ({@link FeedbackMobileCardProps.showSubmitter}).
|
|
@@ -479,6 +485,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
479
485
|
sortBy: (row) => row.votes ?? 0,
|
|
480
486
|
filter: { type: "number", getValue: (row) => row.votes ?? 0 },
|
|
481
487
|
mobileHidden: true,
|
|
488
|
+
// Fourth to go (Kurvenschmiede S1, 0.48): with only the first three, Subject was
|
|
489
|
+
// crushed to 24px at 1100px. Still reachable from the Columns panel.
|
|
490
|
+
fitPriority: 4,
|
|
482
491
|
},
|
|
483
492
|
]
|
|
484
493
|
: [];
|
|
@@ -508,6 +517,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
508
517
|
getValue: (row) => row.category,
|
|
509
518
|
options: FEEDBACK_CATEGORY_ORDER.map((value) => ({ value, label: categoryLabels[value] })),
|
|
510
519
|
},
|
|
520
|
+
// Last to go (Kurvenschmiede S1, 0.48), before Subject would drop below 10rem:
|
|
521
|
+
// the open report's Subject section repeats the badge.
|
|
522
|
+
fitPriority: 5,
|
|
511
523
|
},
|
|
512
524
|
{
|
|
513
525
|
key: "title",
|
|
@@ -521,11 +533,25 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
521
533
|
return env ? `${row.title} ${labels.environment(env)}` : row.title;
|
|
522
534
|
},
|
|
523
535
|
mobilePrimary: true,
|
|
536
|
+
// The rest of the row, one line and an ellipsis (0.48, keksdose live #451: one long
|
|
537
|
+
// crash title pushed Status and the row actions off the edge).
|
|
538
|
+
grow: true,
|
|
524
539
|
},
|
|
525
540
|
{
|
|
526
541
|
key: "user",
|
|
527
542
|
header: labels.columnUser,
|
|
528
|
-
|
|
543
|
+
// At most 12rem, one line and an ellipsis, the whole value in the kit's Tooltip (0.48 build
|
|
544
|
+
// review 2, keksdose N2): beside the grow Subject every other cell keeps one line, so
|
|
545
|
+
// an email standing in for a missing name widened the column and cost Votes and
|
|
546
|
+
// Category their room first. The open report shows both in full.
|
|
547
|
+
cell: (row) => {
|
|
548
|
+
const who = feedbackSubmitter(row, labels);
|
|
549
|
+
return (
|
|
550
|
+
<Tooltip label={who} portal lazy>
|
|
551
|
+
<span className="block max-w-[12rem] truncate-until-large text-secondary">{who}</span>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
);
|
|
554
|
+
},
|
|
529
555
|
sortBy: (row) => feedbackSubmitter(row, labels),
|
|
530
556
|
// Name AND email, so either finds the person; the erased ones by the placeholder
|
|
531
557
|
// (their context holds no name any more).
|
|
@@ -552,6 +578,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
552
578
|
sortBy: feedbackSubmitterEmail,
|
|
553
579
|
filterBy: feedbackSubmitterEmail,
|
|
554
580
|
mobileHidden: true,
|
|
581
|
+
// Second to go when the table is narrow: the open report shows the submitter.
|
|
582
|
+
fitPriority: 2,
|
|
555
583
|
},
|
|
556
584
|
{
|
|
557
585
|
key: "url",
|
|
@@ -566,6 +594,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
566
594
|
sortBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
567
595
|
filterBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
568
596
|
mobileHidden: true,
|
|
597
|
+
// First to go when the table is narrow (0.48): the open report repeats the page.
|
|
598
|
+
fitPriority: 1,
|
|
569
599
|
},
|
|
570
600
|
...votesColumn,
|
|
571
601
|
{
|
|
@@ -600,6 +630,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
600
630
|
sortBy: (row) => instant(row.resolved_at),
|
|
601
631
|
filter: { type: "date", getValue: (row) => row.resolved_at },
|
|
602
632
|
mobileHidden: true,
|
|
633
|
+
// Third to go (0.48).
|
|
634
|
+
fitPriority: 3,
|
|
603
635
|
},
|
|
604
636
|
];
|
|
605
637
|
}
|
package/src/feedback.ts
CHANGED
package/src/hooks/use-dismiss.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from "react";
|
|
3
3
|
import { isComposing } from "../lib/ime";
|
|
4
4
|
|
|
@@ -220,6 +220,29 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
220
220
|
useDismissLayer(enabled, handler);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** Called with each native press an owner's React tree saw — see {@link useOutsideClick}. */
|
|
224
|
+
type PressMark = (press: Event) => void;
|
|
225
|
+
|
|
226
|
+
/** The outside-click hooks watching each ref, for {@link pressInside}. */
|
|
227
|
+
const watchers = new WeakMap<RefObject<HTMLElement | null>, Set<PressMark>>();
|
|
228
|
+
|
|
229
|
+
/** What an `onPointerDownCapture` hands over: a React event, its press inside. */
|
|
230
|
+
type ReactPress = { nativeEvent: Event };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* An `onPointerDownCapture` for a panel whose `ref` an outside-click hook watches but
|
|
234
|
+
* whose element the hook's owner does not render — {@link DropdownPanel} and
|
|
235
|
+
* `ComboboxPanel` take `panelRef` from `useDropdown` / `useComboboxCore`. Every press
|
|
236
|
+
* that reaches the panel through the React tree, a portalled list opened inside it
|
|
237
|
+
* included, is marked inside for each hook watching `ref`. See {@link useOutsideClick}.
|
|
238
|
+
*/
|
|
239
|
+
export function pressInside(ref: RefObject<HTMLElement | null> | undefined): ((e: ReactPress) => void) | undefined {
|
|
240
|
+
if (!ref) return undefined;
|
|
241
|
+
return (e) => {
|
|
242
|
+
watchers.get(ref)?.forEach((mark) => mark(e.nativeEvent));
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
/**
|
|
224
247
|
* Calls `handler` on a press that lands outside every supplied element, while
|
|
225
248
|
* `enabled` — the outside-click-to-close half of the same dismissal story. Pass one
|
|
@@ -227,6 +250,23 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
227
250
|
* `handler` are read live, so an inline `[triggerRef, panelRef]` array is fine and
|
|
228
251
|
* won't churn the listener.
|
|
229
252
|
*
|
|
253
|
+
* ## A panel opened from inside is inside (0.48, keksdose dev#623)
|
|
254
|
+
*
|
|
255
|
+
* A CurrencySelect in a Popover: the currency list is portalled to `<body>`, so in the
|
|
256
|
+
* DOM it is outside the popover's panel, and the first press on a currency closed the
|
|
257
|
+
* POPOVER at pointerdown — the list went with it and the choice never landed. The same
|
|
258
|
+
* for a combobox, a menu, a date picker's popover, a dropdown, at any depth.
|
|
259
|
+
*
|
|
260
|
+
* In the React tree the portal is still where it was opened from, and React passes a
|
|
261
|
+
* press up that tree, through portals. So the hook hands back an
|
|
262
|
+
* `onPointerDownCapture`: put it on the element the press should count as inside of —
|
|
263
|
+
* the kit's Popover panel, HoverMenu, DropdownPanel and ComboboxPanel do — and every
|
|
264
|
+
* press that travels through that element in React, portalled panels opened inside it
|
|
265
|
+
* included, is not outside. React's listeners sit on the root and on each portal's
|
|
266
|
+
* container, so they hear the press before this document listener does. No owner chain
|
|
267
|
+
* to keep: depth is free, and an app's own portal inside a kit panel is covered too. A
|
|
268
|
+
* panel the hook's owner does not render takes {@link pressInside} with the watched ref.
|
|
269
|
+
*
|
|
230
270
|
* **`pointerdown`, not `mousedown`** — one event covering mouse, touch and pen.
|
|
231
271
|
* This listened for `mousedown` alone, which on a touch platform is a SYNTHESISED
|
|
232
272
|
* event: the browser emits it for most taps, which is why nothing obviously broke,
|
|
@@ -245,17 +285,35 @@ export function useOutsideClick(
|
|
|
245
285
|
refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[],
|
|
246
286
|
handler: () => void,
|
|
247
287
|
enabled = true,
|
|
248
|
-
): void {
|
|
288
|
+
): (e: ReactPress) => void {
|
|
249
289
|
const refsRef = useRef(refs);
|
|
250
290
|
const handlerRef = useRef(handler);
|
|
291
|
+
// The last press this owner's React tree saw: the one the document listener is about
|
|
292
|
+
// to hear, when it went through a panel the owner marked.
|
|
293
|
+
const insideRef = useRef<Event | null>(null);
|
|
251
294
|
// In an effect rather than during render — see the note in useDismissLayer.
|
|
252
295
|
useEffect(() => {
|
|
253
296
|
refsRef.current = refs;
|
|
254
297
|
handlerRef.current = handler;
|
|
255
298
|
});
|
|
299
|
+
const markInside = useCallback((e: ReactPress) => {
|
|
300
|
+
insideRef.current = e.nativeEvent;
|
|
301
|
+
}, []);
|
|
256
302
|
useEffect(() => {
|
|
257
303
|
if (!enabled) return;
|
|
304
|
+
const mark: PressMark = (press) => {
|
|
305
|
+
insideRef.current = press;
|
|
306
|
+
};
|
|
307
|
+
const watched = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
308
|
+
for (const r of watched) {
|
|
309
|
+
const set = watchers.get(r) ?? new Set<PressMark>();
|
|
310
|
+
set.add(mark);
|
|
311
|
+
watchers.set(r, set);
|
|
312
|
+
}
|
|
258
313
|
const onPointerDown = (e: Event) => {
|
|
314
|
+
const inside = insideRef.current === e;
|
|
315
|
+
insideRef.current = null;
|
|
316
|
+
if (inside) return;
|
|
259
317
|
const target = e.target as Node;
|
|
260
318
|
const list = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
261
319
|
for (const r of list) {
|
|
@@ -264,6 +322,10 @@ export function useOutsideClick(
|
|
|
264
322
|
handlerRef.current();
|
|
265
323
|
};
|
|
266
324
|
document.addEventListener("pointerdown", onPointerDown);
|
|
267
|
-
return () =>
|
|
325
|
+
return () => {
|
|
326
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
327
|
+
for (const r of watched) watchers.get(r)?.delete(mark);
|
|
328
|
+
};
|
|
268
329
|
}, [enabled]);
|
|
330
|
+
return markInside;
|
|
269
331
|
}
|
package/src/i18n/german.ts
CHANGED
|
@@ -202,7 +202,6 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
202
202
|
outcomeUpdate: "Aktualisieren",
|
|
203
203
|
outcomePlaceholder: "Was wurde umgesetzt, entschieden oder warum nicht.",
|
|
204
204
|
rework: "Nacharbeit",
|
|
205
|
-
openPage: "Seite öffnen",
|
|
206
205
|
reworkTitle: "Zur Nacharbeit senden",
|
|
207
206
|
reworkSend: "Nacharbeit senden",
|
|
208
207
|
reworkPlaceholder: "Was muss noch angepasst werden? Neue Anforderungen oder Richtungswechsel.",
|
|
@@ -340,6 +339,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
340
339
|
opensInNewTab: "öffnet in einem neuen Tab",
|
|
341
340
|
aboutField: (field) => `Hilfe zu „${field}“`,
|
|
342
341
|
help: "Hilfe",
|
|
342
|
+
showMore: "Mehr",
|
|
343
|
+
showLess: "Weniger",
|
|
343
344
|
},
|
|
344
345
|
dataTable: {
|
|
345
346
|
columns: "Spalten",
|
|
@@ -352,6 +353,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
352
353
|
cancelSelection: "Abbrechen",
|
|
353
354
|
rowDetails: "Details",
|
|
354
355
|
autoSize: "Spaltenbreiten anpassen",
|
|
356
|
+
hiddenForWidth: "aus Platzgründen ausgeblendet",
|
|
355
357
|
loading: "Wird geladen…",
|
|
356
358
|
filters: "Filter",
|
|
357
359
|
// Resets every active filter; "Alle löschen" would read as deleting the filters.
|
|
@@ -403,6 +405,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
403
405
|
booleanTrue: "Ja",
|
|
404
406
|
booleanFalse: "Nein",
|
|
405
407
|
booleanUnset: "Nicht festgelegt",
|
|
408
|
+
loadMore: "Mehr laden",
|
|
409
|
+
allLoaded: "Alle Zeilen geladen",
|
|
406
410
|
sortBy: "Sortieren nach",
|
|
407
411
|
sortDefault: "Standardreihenfolge",
|
|
408
412
|
sortAscending: "Aufsteigend",
|
|
@@ -541,7 +545,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
541
545
|
add: "Tab hinzufügen",
|
|
542
546
|
remove: (tab) => `${tab} entfernen`,
|
|
543
547
|
more: "Weitere Tabs",
|
|
544
|
-
|
|
548
|
+
previous: "Vorherige Tabs",
|
|
545
549
|
},
|
|
546
550
|
appShell: {
|
|
547
551
|
collapse: "Seitenleiste einklappen",
|
|
@@ -1184,7 +1188,6 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1184
1188
|
notifications: {
|
|
1185
1189
|
typesTitle: "Benachrichtigungen zu",
|
|
1186
1190
|
typesDescription: "Wählen Sie, worüber Sie benachrichtigt werden, und wie.",
|
|
1187
|
-
byEmail: "Per E-Mail",
|
|
1188
1191
|
channelApp: "In der App",
|
|
1189
1192
|
channelPush: "Push",
|
|
1190
1193
|
channelEmail: "E-Mail",
|
|
@@ -1207,9 +1210,6 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1207
1210
|
quietFrom: "Von",
|
|
1208
1211
|
quietUntil: "Bis",
|
|
1209
1212
|
quietIncomplete: "Legen Sie eine Start- und eine Endzeit fest. Ruhezeiten gelten erst, wenn beide gesetzt sind.",
|
|
1210
|
-
timeZone: "Zeitzone",
|
|
1211
|
-
deviceZone: (zone) => `Dieses Gerät ist in der Zeitzone ${zone}.`,
|
|
1212
|
-
useDeviceZone: "Übernehmen",
|
|
1213
1213
|
regions: {
|
|
1214
1214
|
Africa: "Afrika",
|
|
1215
1215
|
America: "Amerika",
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -189,6 +189,10 @@ export interface CommonLabels {
|
|
|
189
189
|
aboutField: (field: string) => string;
|
|
190
190
|
/** …and of a `help` "?" on a field with no label and no `aria-label` to name it by. */
|
|
191
191
|
help: string;
|
|
192
|
+
/** 0.48: the toggle that shows the rest of a clamped text (`AlertBanner clampMobile`)… */
|
|
193
|
+
showMore: string;
|
|
194
|
+
/** …and folds it back to its one line. */
|
|
195
|
+
showLess: string;
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
/** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
|