@eifi1/ui-kit 0.24.1 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/chart.d.ts +2 -2
- package/dist/components/account-chips.d.ts +1 -1
- package/dist/components/amount-input.d.ts +2 -2
- package/dist/components/button-group.d.ts +2 -2
- package/dist/components/calculator.d.ts +2 -2
- package/dist/components/column-mapper.d.ts +28 -9
- package/dist/components/column-mapper.js +63 -77
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -2
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-labels.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +1 -1
- package/dist/components/data-table-pagination.js +6 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +7 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -2
- package/dist/components/file-button.d.ts +2 -2
- package/dist/components/file-dropzone.d.ts +2 -2
- package/dist/components/form-actions.d.ts +2 -2
- package/dist/components/iban-input.d.ts +2 -2
- package/dist/components/language-select.d.ts +2 -2
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/number-field.d.ts +2 -2
- package/dist/components/number-input.d.ts +2 -2
- package/dist/components/numpad-sheet.d.ts +2 -2
- package/dist/components/phone-input.d.ts +2 -2
- package/dist/components/series-chart.d.ts +2 -2
- package/dist/components/settings-fields.d.ts +2 -2
- package/dist/components/share-card.d.ts +2 -2
- package/dist/components/swipeable-row.js +1 -1
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table.d.ts +35 -1
- package/dist/components/table.js +4 -0
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +2 -2
- package/dist/components/time-input.d.ts +2 -2
- package/dist/components/tooltip.d.ts +103 -14
- package/dist/components/tooltip.js +194 -57
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +9 -1
- package/dist/components/translation-review-editor.js +11 -1
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-labels.d.ts +10 -0
- package/dist/components/translation-review-labels.js +5 -1
- package/dist/components/translation-review-labels.js.map +1 -1
- package/dist/components/translation-review.d.ts +145 -8
- package/dist/components/translation-review.js +372 -91
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +2 -2
- package/dist/components/ui.js +55 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-labels-B7OdnM0S.d.ts → data-table-labels-Cdn1QS2k.d.ts} +21 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -2
- package/dist/feedback/feedback-dialog.d.ts +2 -2
- package/dist/feedback/feedback-dialog.js +6 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +2 -2
- package/dist/feedback/feedback-inbox.js +2 -0
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +2 -2
- package/dist/{feedback-DOwPu-Il.d.ts → feedback-BXKH-ToU.d.ts} +32 -3
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-body-scroll-lock.js +16 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +2 -2
- package/dist/hooks/use-search-param-state.d.ts +58 -3
- package/dist/hooks/use-search-param-state.js +107 -35
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -2
- package/dist/i18n/german.d.ts +2 -2
- package/dist/i18n/german.js +8 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/languages.d.ts +2 -2
- package/dist/i18n/locales/de-CH.d.ts +2 -2
- package/dist/i18n/locales/en.d.ts +2 -2
- package/dist/i18n/locales/en.js +5 -1
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -2
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -2
- package/dist/i18n/locales/fr.js +6 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -2
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -2
- package/dist/i18n/locales/it.js +6 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -2
- package/dist/i18n/locales/zh.js +6 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +2 -2
- package/dist/i18n/review.js +11 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/strip-fade.d.ts +84 -5
- package/dist/lib/strip-fade.js +109 -11
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/lib/translation-review.d.ts +45 -1
- package/dist/lib/translation-review.js +38 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.d.ts +2 -2
- package/dist/rhf/form.d.ts +2 -2
- package/dist/rhf.d.ts +2 -2
- package/dist/shell/app-shell.d.ts +2 -2
- package/dist/shell/top-bar-brand.d.ts +2 -2
- package/dist/shell.d.ts +2 -2
- package/dist/wizard/stepper-nav.d.ts +2 -2
- package/dist/wizard.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/column-mapper.tsx +120 -113
- package/src/components/data-table-pagination.tsx +10 -1
- package/src/components/data-table.tsx +34 -1
- package/src/components/swipeable-row.tsx +1 -1
- package/src/components/table.tsx +39 -0
- package/src/components/tooltip.tsx +487 -104
- package/src/components/translation-review-editor.tsx +21 -1
- package/src/components/translation-review-labels.ts +17 -0
- package/src/components/translation-review.tsx +657 -109
- package/src/components/ui.tsx +104 -4
- package/src/feedback/feedback-dialog.tsx +21 -0
- package/src/feedback/feedback-inbox.tsx +19 -2
- package/src/hooks/use-body-scroll-lock.ts +25 -0
- package/src/hooks/use-search-param-state.ts +283 -41
- package/src/i18n/german.ts +10 -0
- package/src/i18n/locales/en.ts +7 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +11 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/i18n/review.ts +11 -0
- package/src/index.ts +12 -0
- package/src/lib/strip-fade.ts +220 -15
- package/src/lib/translation-review.ts +85 -0
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
4
|
-
import { Check, ListChecks, RotateCcw } from "lucide-react";
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Check, ListChecks, PencilLine, RotateCcw } from "lucide-react";
|
|
5
|
+
import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
5
6
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
6
7
|
import { cn } from "../lib/cn.js";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_TRANSLATION_REVIEW_FILTER,
|
|
9
10
|
REVIEW_STATUSES,
|
|
10
11
|
filterTranslationRows,
|
|
12
|
+
groupTranslationRows,
|
|
11
13
|
keyInAreas,
|
|
14
|
+
reviewUndo,
|
|
12
15
|
reviewWrite,
|
|
13
|
-
summariseRows
|
|
16
|
+
summariseRows,
|
|
17
|
+
unreviewedRows
|
|
14
18
|
} from "../lib/translation-review.js";
|
|
15
19
|
import { AlertBanner } from "./alert-banner.js";
|
|
16
20
|
import { BulkActionBar } from "./bulk-action-bar.js";
|
|
17
21
|
import { Checkbox } from "./checkbox.js";
|
|
18
22
|
import { Chip } from "./chip.js";
|
|
19
23
|
import { DataTable } from "./data-table.js";
|
|
24
|
+
import { Disclosure } from "./disclosure.js";
|
|
20
25
|
import { SearchField } from "./search-field.js";
|
|
26
|
+
import { toast } from "./toast.js";
|
|
21
27
|
import { ToggleGroup } from "./toggle-group.js";
|
|
22
28
|
import { TranslationReviewEditor } from "./translation-review-editor.js";
|
|
23
29
|
import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels.js";
|
|
24
30
|
import { ReviewStatusChip, TranslationProgress } from "./translation-review-parts.js";
|
|
25
31
|
import { Button, IconButton, Select } from "./ui.js";
|
|
32
|
+
import { useWriteLock } from "./write-lock.js";
|
|
26
33
|
import {
|
|
27
34
|
REVIEW_STATUS_TONES,
|
|
28
35
|
ReviewStatusChip as ReviewStatusChip2,
|
|
@@ -32,6 +39,54 @@ import {
|
|
|
32
39
|
} from "./translation-review-parts.js";
|
|
33
40
|
import { TranslationReviewEditor as TranslationReviewEditor2 } from "./translation-review-editor.js";
|
|
34
41
|
import { DEFAULT_TRANSLATION_REVIEW_LABELS as DEFAULT_TRANSLATION_REVIEW_LABELS2 } from "./translation-review-labels.js";
|
|
42
|
+
const MD_UP = "(min-width: 768px)";
|
|
43
|
+
const GROUPS_OPEN_ROW_BUDGET = 100;
|
|
44
|
+
function groupsFitOpen(groups, pageSize) {
|
|
45
|
+
const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);
|
|
46
|
+
let rendered = 0;
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
rendered += Math.min(group.rows.length, pageSize);
|
|
49
|
+
if (rendered > budget) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function devWarnings() {
|
|
54
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
55
|
+
}
|
|
56
|
+
const FILTER_ECHO_MS = 1500;
|
|
57
|
+
function useFilterEchoCheck(filterProp) {
|
|
58
|
+
const committed = useRef(filterProp);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
committed.current = filterProp;
|
|
61
|
+
});
|
|
62
|
+
const pending = useRef(/* @__PURE__ */ new Map());
|
|
63
|
+
const warned = useRef(/* @__PURE__ */ new Set());
|
|
64
|
+
const timer = useRef(void 0);
|
|
65
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
66
|
+
return (next) => {
|
|
67
|
+
if (filterProp === void 0 || !devWarnings()) return;
|
|
68
|
+
for (const field of Object.keys(next)) {
|
|
69
|
+
const from = filterProp[field];
|
|
70
|
+
if (from === void 0 || warned.current.has(field)) continue;
|
|
71
|
+
const seen = pending.current.get(field);
|
|
72
|
+
if (seen) seen.to = next[field];
|
|
73
|
+
else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });
|
|
74
|
+
}
|
|
75
|
+
if (pending.current.size === 0) return;
|
|
76
|
+
clearTimeout(timer.current);
|
|
77
|
+
timer.current = setTimeout(() => {
|
|
78
|
+
const now = committed.current;
|
|
79
|
+
for (const [field, { from, to }] of pending.current) {
|
|
80
|
+
if (to === from || now?.[field] !== from || warned.current.has(field)) continue;
|
|
81
|
+
warned.current.add(field);
|
|
82
|
+
console.warn(
|
|
83
|
+
`[ui-kit] TranslationReviewPanel: the "${field}" filter was changed to ${JSON.stringify(to)}, but the \`filter\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in \`filter\` is the app's: write back every field \`onFilterChange\` reports, in one update (useSearchParamsState's setter takes the object as it is), or leave the field out of \`filter\` and the panel keeps it. (Development builds only; once per field.)`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
pending.current.clear();
|
|
87
|
+
}, FILTER_ECHO_MS);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
35
90
|
function TextCell({ row, labels, clamp }) {
|
|
36
91
|
return row.text === "" ? /* @__PURE__ */ jsx("span", { className: "italic text-[var(--text-muted)]", children: labels.missingText }) : /* @__PURE__ */ jsx("span", { lang: row.locale, className: cn("whitespace-pre-line break-words", clamp && "line-clamp-3"), children: row.text });
|
|
37
92
|
}
|
|
@@ -53,21 +108,39 @@ function TranslationReviewPanel({
|
|
|
53
108
|
storageKey,
|
|
54
109
|
formatDate,
|
|
55
110
|
formatError,
|
|
111
|
+
swipe = false,
|
|
112
|
+
undo: undoProp,
|
|
113
|
+
groupBy,
|
|
114
|
+
groupLabel,
|
|
115
|
+
groupHeadingAs = "h2",
|
|
116
|
+
defaultGroupsOpen = "auto",
|
|
56
117
|
className,
|
|
57
118
|
labels: labelsProp
|
|
58
119
|
}) {
|
|
59
120
|
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
60
121
|
const locale = useKitLocale();
|
|
122
|
+
const lock = useWriteLock();
|
|
123
|
+
const phone = !useMediaQuery(MD_UP, true);
|
|
124
|
+
const baseId = useId();
|
|
125
|
+
const rootRef = useRef(null);
|
|
61
126
|
const editable = !readOnly && onSave !== void 0;
|
|
127
|
+
const undoOn = undoProp ?? (swipe || groupBy !== void 0);
|
|
62
128
|
const [ownFilter, setOwnFilter] = useState(DEFAULT_TRANSLATION_REVIEW_FILTER);
|
|
63
129
|
const filter = { ...ownFilter };
|
|
64
130
|
for (const [name, value] of Object.entries(filterProp ?? {})) {
|
|
65
131
|
if (value !== void 0) Object.assign(filter, { [name]: value });
|
|
66
132
|
}
|
|
133
|
+
const echoFilter = useFilterEchoCheck(filterProp);
|
|
67
134
|
const [openId, setOpenId] = useState(null);
|
|
135
|
+
const [focusId, setFocusId] = useState(null);
|
|
68
136
|
const [selected, setSelected] = useState(() => /* @__PURE__ */ new Set());
|
|
69
|
-
const [
|
|
137
|
+
const [queued, setQueued] = useState([]);
|
|
138
|
+
const claimed = useRef(/* @__PURE__ */ new Set());
|
|
139
|
+
const tail = useRef(Promise.resolve());
|
|
140
|
+
const undoCount = useRef(0);
|
|
70
141
|
const [failure, setFailure] = useState(null);
|
|
142
|
+
const busy = queued.length > 0;
|
|
143
|
+
const isQueued = (key) => queued.includes(key);
|
|
71
144
|
const rows = useMemo(
|
|
72
145
|
() => areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp],
|
|
73
146
|
[rowsProp, areas, inArea]
|
|
@@ -86,39 +159,117 @@ function TranslationReviewPanel({
|
|
|
86
159
|
() => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),
|
|
87
160
|
[rows, status, source, namespace, query, placeholdersOnly]
|
|
88
161
|
);
|
|
162
|
+
const groups = useMemo(() => {
|
|
163
|
+
if (groupBy === void 0) return null;
|
|
164
|
+
const of = groupBy === "namespace" ? (r) => r.namespace : groupBy === "source" ? (r) => r.source : groupBy;
|
|
165
|
+
return groupTranslationRows(visible, of);
|
|
166
|
+
}, [visible, groupBy]);
|
|
167
|
+
const groupName = (key) => groupLabel?.(key) ?? (groupBy === "source" ? sourceLabels?.[key] : void 0) ?? (key || "\u2014");
|
|
168
|
+
const [groupOpen, setGroupOpen] = useState({});
|
|
169
|
+
const groupOpenByDefault = defaultGroupsOpen !== "auto" ? defaultGroupsOpen : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));
|
|
170
|
+
const isGroupOpen = (key) => groupOpen[key] ?? groupOpenByDefault;
|
|
171
|
+
const setGroupOpenFor = (key, open) => setGroupOpen((prev) => ({ ...prev, [key]: open }));
|
|
172
|
+
const keepGroupOpen = (key) => {
|
|
173
|
+
if (key !== void 0) setGroupOpen((prev) => key in prev ? prev : { ...prev, [key]: true });
|
|
174
|
+
};
|
|
175
|
+
const [confirming, setConfirming] = useState(null);
|
|
176
|
+
const confirmFocus = useRef(null);
|
|
177
|
+
const [groupSorts, setGroupSorts] = useState([]);
|
|
89
178
|
const setFilter = (patch) => {
|
|
90
179
|
const next = { ...filter, ...patch };
|
|
91
180
|
setOwnFilter(next);
|
|
92
181
|
onFilterChange?.(next);
|
|
182
|
+
echoFilter(next);
|
|
93
183
|
setSelected(/* @__PURE__ */ new Set());
|
|
94
184
|
};
|
|
95
|
-
const run =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
185
|
+
const run = (key, action) => {
|
|
186
|
+
if (claimed.current.has(key)) return Promise.resolve(false);
|
|
187
|
+
claimed.current.add(key);
|
|
188
|
+
setQueued((q) => [...q, key]);
|
|
189
|
+
const step = tail.current.then(async () => {
|
|
190
|
+
setFailure(null);
|
|
191
|
+
try {
|
|
192
|
+
await action();
|
|
193
|
+
return true;
|
|
194
|
+
} catch (caught) {
|
|
195
|
+
setFailure(formatError ? formatError(caught) : labels.failed);
|
|
196
|
+
return false;
|
|
197
|
+
} finally {
|
|
198
|
+
claimed.current.delete(key);
|
|
199
|
+
setQueued((q) => q.filter((k) => k !== key));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
tail.current = step;
|
|
203
|
+
return step;
|
|
204
|
+
};
|
|
205
|
+
const sayApproved = (targets, description) => {
|
|
206
|
+
const back = reviewUndo(targets);
|
|
207
|
+
const message = labels.approvedToast(targets.length);
|
|
208
|
+
const save = onSave;
|
|
209
|
+
const clear = onClear;
|
|
210
|
+
if (!save || back.clears.length > 0 && !clear) {
|
|
211
|
+
toast.success(message, { description });
|
|
212
|
+
return;
|
|
106
213
|
}
|
|
214
|
+
const key = `undo:${++undoCount.current}`;
|
|
215
|
+
toast.undo(message, {
|
|
216
|
+
description,
|
|
217
|
+
onUndo: () => void run(key, async () => {
|
|
218
|
+
if (back.writes.length > 0) await save(back.writes, { origin: "undo", toasted: false });
|
|
219
|
+
if (back.clears.length > 0) await clear?.(back.clears, { origin: "undo", toasted: false });
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const approve = async (key, targets, origin, description) => {
|
|
224
|
+
if (!onSave || targets.length === 0) return false;
|
|
225
|
+
const save = onSave;
|
|
226
|
+
const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, "APPROVED")), { origin, toasted: undoOn }));
|
|
227
|
+
if (ok && undoOn) sayApproved(targets, description);
|
|
228
|
+
return ok;
|
|
229
|
+
};
|
|
230
|
+
const saveFromEditor = async (row, write) => {
|
|
231
|
+
const toasted = undoOn && write.verdict === "APPROVED";
|
|
232
|
+
await onSave?.([write], { origin: "editor", toasted });
|
|
233
|
+
if (toasted) sayApproved([row], row.key);
|
|
234
|
+
};
|
|
235
|
+
const keepFocusAt = (groupKey) => {
|
|
236
|
+
const check = () => {
|
|
237
|
+
const root = rootRef.current;
|
|
238
|
+
const active = document.activeElement;
|
|
239
|
+
if (!root || active && active !== document.body && active.isConnected) return;
|
|
240
|
+
const sections = [...root.querySelectorAll("[data-review-group]")];
|
|
241
|
+
const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];
|
|
242
|
+
section?.querySelector("[data-disclosure-trigger]")?.focus();
|
|
243
|
+
};
|
|
244
|
+
requestAnimationFrame(() => requestAnimationFrame(check));
|
|
245
|
+
};
|
|
246
|
+
const approveGroup = async (group, confirmed) => {
|
|
247
|
+
const targets = unreviewedRows(group.rows);
|
|
248
|
+
if (targets.length === 0) return;
|
|
249
|
+
if (!confirmed && targets.length > pageSize) {
|
|
250
|
+
setGroupOpenFor(group.key, true);
|
|
251
|
+
setConfirming(group.key);
|
|
252
|
+
confirmFocus.current = group.key;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const ok = await approve(`group:${group.key}`, targets, "group", groupName(group.key));
|
|
256
|
+
if (ok) setConfirming((key) => key === group.key ? null : key);
|
|
257
|
+
keepFocusAt(group.key);
|
|
107
258
|
};
|
|
108
259
|
const visibleIds = new Set(visible.map((r) => r.id));
|
|
109
260
|
const selectedRows = visible.filter((r) => selected.has(r.id));
|
|
110
|
-
const
|
|
261
|
+
const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;
|
|
262
|
+
const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));
|
|
111
263
|
const toApprove = selectedRows.filter((r) => r.text !== "" && r.status !== "approved");
|
|
112
264
|
const toReset = selectedRows.filter((r) => r.review !== null);
|
|
113
265
|
const approveSelected = async () => {
|
|
114
|
-
if (
|
|
115
|
-
if (await run("bulk-approve", () => onSave(toApprove.map((r) => reviewWrite(r, "APPROVED"))))) {
|
|
116
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
117
|
-
}
|
|
266
|
+
if (await approve("bulk-approve", toApprove, "selection")) setSelected(/* @__PURE__ */ new Set());
|
|
118
267
|
};
|
|
119
268
|
const resetSelected = async () => {
|
|
120
269
|
if (!onClear || toReset.length === 0) return;
|
|
121
|
-
|
|
270
|
+
const clear = onClear;
|
|
271
|
+
const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));
|
|
272
|
+
if (await run("bulk-reset", () => clear(keys, { origin: "selection", toasted: false }))) {
|
|
122
273
|
setSelected(/* @__PURE__ */ new Set());
|
|
123
274
|
}
|
|
124
275
|
};
|
|
@@ -155,8 +306,7 @@ function TranslationReviewPanel({
|
|
|
155
306
|
sortBy: (r) => REVIEW_STATUSES.indexOf(r.status)
|
|
156
307
|
}
|
|
157
308
|
];
|
|
158
|
-
if (editable
|
|
159
|
-
const save = onSave;
|
|
309
|
+
if (editable) {
|
|
160
310
|
columns.push({
|
|
161
311
|
key: "approve",
|
|
162
312
|
header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.approve }),
|
|
@@ -168,18 +318,205 @@ function TranslationReviewPanel({
|
|
|
168
318
|
tone: "muted",
|
|
169
319
|
commit: true,
|
|
170
320
|
label: labels.approve,
|
|
171
|
-
pending:
|
|
172
|
-
disabled: busy
|
|
321
|
+
pending: isQueued(`row:${r.id}`),
|
|
322
|
+
disabled: busy,
|
|
173
323
|
onClick: (event) => {
|
|
174
324
|
event.stopPropagation();
|
|
175
|
-
void
|
|
325
|
+
void approve(`row:${r.id}`, [r], "row", r.key);
|
|
176
326
|
},
|
|
177
327
|
children: /* @__PURE__ */ jsx(Check, {})
|
|
178
328
|
}
|
|
179
329
|
)
|
|
180
330
|
});
|
|
181
331
|
}
|
|
182
|
-
|
|
332
|
+
const swipeActions = (r, groupKey) => {
|
|
333
|
+
if (!editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
334
|
+
const missing = r.text === "";
|
|
335
|
+
return {
|
|
336
|
+
end: missing || r.status === "approved" ? [] : [
|
|
337
|
+
{
|
|
338
|
+
label: labels.approve,
|
|
339
|
+
icon: /* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
340
|
+
onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
|
|
341
|
+
className: "bg-[var(--success)]",
|
|
342
|
+
armedClassName: "bg-[var(--success)]"
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
start: [
|
|
346
|
+
{
|
|
347
|
+
label: missing ? labels.suggest : labels.flag,
|
|
348
|
+
icon: /* @__PURE__ */ jsx(PencilLine, { className: "size-4", "aria-hidden": true }),
|
|
349
|
+
// Opens the editor rather than sending the string back: "needs a change"
|
|
350
|
+
// with neither a note nor a better wording tells nobody anything.
|
|
351
|
+
onCommit: () => {
|
|
352
|
+
keepGroupOpen(groupKey);
|
|
353
|
+
setOpenId(r.id);
|
|
354
|
+
setFocusId(r.id);
|
|
355
|
+
},
|
|
356
|
+
className: missing ? "bg-[var(--brand)]" : "bg-[var(--danger)]",
|
|
357
|
+
armedClassName: missing ? "bg-[var(--brand-hover)]" : "bg-[var(--danger-hover)]"
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const toggleIn = (prev, many, checked) => {
|
|
363
|
+
const next = new Set([...prev].filter((id) => visibleIds.has(id)));
|
|
364
|
+
for (const r of many) {
|
|
365
|
+
if (checked) next.add(r.id);
|
|
366
|
+
else next.delete(r.id);
|
|
367
|
+
}
|
|
368
|
+
return next;
|
|
369
|
+
};
|
|
370
|
+
const selectionFor = (list) => {
|
|
371
|
+
if (!editable) return void 0;
|
|
372
|
+
const picked = list.filter((r) => selected.has(r.id)).length;
|
|
373
|
+
return {
|
|
374
|
+
isSelected: (r) => selected.has(r.id),
|
|
375
|
+
onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),
|
|
376
|
+
onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),
|
|
377
|
+
allSelected: list.length > 0 && picked === list.length,
|
|
378
|
+
someSelected: picked > 0 && picked < list.length,
|
|
379
|
+
onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked))
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
const table = (list, group) => /* @__PURE__ */ jsx(
|
|
383
|
+
DataTable,
|
|
384
|
+
{
|
|
385
|
+
rows: list,
|
|
386
|
+
rowKey: (r) => r.id,
|
|
387
|
+
columns,
|
|
388
|
+
empty: labels.empty,
|
|
389
|
+
defaultPageSize: pageSize,
|
|
390
|
+
...group ? {
|
|
391
|
+
// Inside the group's own card: no frame in a frame, no column rail per group,
|
|
392
|
+
// no scroller per group, and the group's name as the table's.
|
|
393
|
+
frame: false,
|
|
394
|
+
chrome: "minimal",
|
|
395
|
+
maxBodyHeight: "none",
|
|
396
|
+
sorts: groupSorts,
|
|
397
|
+
onSortsChange: setGroupSorts,
|
|
398
|
+
labels: { table: group.name }
|
|
399
|
+
} : { storageKey },
|
|
400
|
+
isExpanded: (r) => r.id === openId,
|
|
401
|
+
onRowClick: (r) => {
|
|
402
|
+
if (openId !== r.id) keepGroupOpen(group?.key);
|
|
403
|
+
setFocusId(null);
|
|
404
|
+
setOpenId((open) => open === r.id ? null : r.id);
|
|
405
|
+
},
|
|
406
|
+
expandedRow: (r) => /* @__PURE__ */ jsx(
|
|
407
|
+
TranslationReviewEditor,
|
|
408
|
+
{
|
|
409
|
+
row: r,
|
|
410
|
+
referenceLabel,
|
|
411
|
+
localeLabel,
|
|
412
|
+
readOnly: !editable,
|
|
413
|
+
focusWording: focusId === r.id,
|
|
414
|
+
onSave: onSave ? (write) => saveFromEditor(r, write) : void 0,
|
|
415
|
+
onClear: onClear ? (key) => onClear([key], { origin: "editor", toasted: false }) : void 0,
|
|
416
|
+
onClose: () => {
|
|
417
|
+
setOpenId(null);
|
|
418
|
+
setFocusId(null);
|
|
419
|
+
},
|
|
420
|
+
formatDate,
|
|
421
|
+
formatError,
|
|
422
|
+
labels: labelsProp
|
|
423
|
+
},
|
|
424
|
+
r.id
|
|
425
|
+
),
|
|
426
|
+
mobileCard: (r) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
427
|
+
/* @__PURE__ */ jsx("code", { className: "block break-all text-xs text-[var(--text-muted)]", children: r.key }),
|
|
428
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm text-[var(--text-primary)]", children: /* @__PURE__ */ jsx(TextCell, { row: r, labels, clamp: true }) }),
|
|
429
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
|
|
430
|
+
/* @__PURE__ */ jsx(ReviewStatusChip, { status: r.status, labels: labelsProp }),
|
|
431
|
+
r.placeholderMismatch && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "danger", variant: "outline", children: labels.placeholderChip })
|
|
432
|
+
] })
|
|
433
|
+
] }),
|
|
434
|
+
mobileSwipeActions: swipe ? (r) => swipeActions(r, group?.key) : void 0,
|
|
435
|
+
selection: selectionFor(list)
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
const groupSection = (g, index) => {
|
|
439
|
+
const name = groupName(g.key);
|
|
440
|
+
const titleId = `${baseId}-group-${index}`;
|
|
441
|
+
const questionId = `${baseId}-confirm-${index}`;
|
|
442
|
+
const targets = editable ? unreviewedRows(g.rows) : [];
|
|
443
|
+
const key = `group:${g.key}`;
|
|
444
|
+
const open = isGroupOpen(g.key);
|
|
445
|
+
const body = open || groupOpen[g.key] === false;
|
|
446
|
+
return /* @__PURE__ */ jsxs(
|
|
447
|
+
Disclosure,
|
|
448
|
+
{
|
|
449
|
+
"data-review-group": g.key,
|
|
450
|
+
headingAs: groupHeadingAs,
|
|
451
|
+
title: /* @__PURE__ */ jsx("span", { id: titleId, children: name }),
|
|
452
|
+
hint: labels.groupCount(g.summary.unreviewed, g.summary.total),
|
|
453
|
+
open,
|
|
454
|
+
onOpenChange: (next) => {
|
|
455
|
+
setGroupOpenFor(g.key, next);
|
|
456
|
+
if (!next) setConfirming((c) => c === g.key ? null : c);
|
|
457
|
+
},
|
|
458
|
+
trailing: targets.length > 0 ? /* @__PURE__ */ jsxs(
|
|
459
|
+
Button,
|
|
460
|
+
{
|
|
461
|
+
variant: "secondary",
|
|
462
|
+
size: "sm",
|
|
463
|
+
commit: true,
|
|
464
|
+
"aria-describedby": titleId,
|
|
465
|
+
pending: isQueued(key) && confirming !== g.key,
|
|
466
|
+
disabled: busy,
|
|
467
|
+
onClick: () => void approveGroup(g, false),
|
|
468
|
+
children: [
|
|
469
|
+
/* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
470
|
+
labels.approveGroup(targets.length)
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
) : void 0,
|
|
474
|
+
bodyClassName: "space-y-0 px-0 pb-0 border-t border-[var(--border)]",
|
|
475
|
+
children: [
|
|
476
|
+
body && confirming === g.key && targets.length > 0 && /* @__PURE__ */ jsx("div", { className: "border-b border-[var(--border)] p-3", children: /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", size: "sm", children: /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-2", children: [
|
|
477
|
+
/* @__PURE__ */ jsx("p", { id: questionId, children: labels.confirmGroup(targets.length, name) }),
|
|
478
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
479
|
+
/* @__PURE__ */ jsx(
|
|
480
|
+
Button,
|
|
481
|
+
{
|
|
482
|
+
ref: (el) => {
|
|
483
|
+
if (el && confirmFocus.current === g.key) {
|
|
484
|
+
confirmFocus.current = null;
|
|
485
|
+
el.focus();
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
variant: "primary",
|
|
489
|
+
size: "sm",
|
|
490
|
+
commit: true,
|
|
491
|
+
"aria-describedby": questionId,
|
|
492
|
+
pending: isQueued(key),
|
|
493
|
+
disabled: busy,
|
|
494
|
+
onClick: () => void approveGroup(g, true),
|
|
495
|
+
children: labels.approveGroup(targets.length)
|
|
496
|
+
}
|
|
497
|
+
),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
Button,
|
|
500
|
+
{
|
|
501
|
+
variant: "ghost",
|
|
502
|
+
size: "sm",
|
|
503
|
+
disabled: isQueued(key),
|
|
504
|
+
onClick: () => {
|
|
505
|
+
setConfirming(null);
|
|
506
|
+
keepFocusAt(g.key);
|
|
507
|
+
},
|
|
508
|
+
children: labels.cancel
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
] })
|
|
512
|
+
] }) }) }),
|
|
513
|
+
body && table(g.rows, { key: g.key, name })
|
|
514
|
+
]
|
|
515
|
+
},
|
|
516
|
+
g.key
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("min-w-0 space-y-4", className), children: [
|
|
183
520
|
scopeText && /* @__PURE__ */ jsx(AlertBanner, { tone: "info", variant: "inline", size: "sm", block: true, live: false, children: labels.scope(scopeText) }),
|
|
184
521
|
readOnly && /* @__PURE__ */ jsx(AlertBanner, { tone: "info", variant: "inline", size: "sm", block: true, live: false, children: labels.readOnly }),
|
|
185
522
|
progress && /* @__PURE__ */ jsx(TranslationProgress, { summary, labels: labelsProp }),
|
|
@@ -224,15 +561,15 @@ function TranslationReviewPanel({
|
|
|
224
561
|
}
|
|
225
562
|
)
|
|
226
563
|
] }),
|
|
227
|
-
editable &&
|
|
564
|
+
editable && shownRows.length > 0 && // The phone's table has no checkboxes; this is its way into the bulk actions.
|
|
228
565
|
/* @__PURE__ */ jsxs(
|
|
229
566
|
Button,
|
|
230
567
|
{
|
|
231
568
|
variant: "ghost",
|
|
232
569
|
size: "sm",
|
|
233
570
|
className: "md:hidden",
|
|
234
|
-
disabled:
|
|
235
|
-
onClick: () => setSelected(
|
|
571
|
+
disabled: allShownSelected,
|
|
572
|
+
onClick: () => setSelected((prev) => toggleIn(prev, shownRows, true)),
|
|
236
573
|
children: [
|
|
237
574
|
/* @__PURE__ */ jsx(ListChecks, { className: "size-4", "aria-hidden": true }),
|
|
238
575
|
labels.selectShown
|
|
@@ -247,8 +584,8 @@ function TranslationReviewPanel({
|
|
|
247
584
|
variant: "primary",
|
|
248
585
|
size: "sm",
|
|
249
586
|
commit: true,
|
|
250
|
-
pending:
|
|
251
|
-
disabled: busy
|
|
587
|
+
pending: isQueued("bulk-approve"),
|
|
588
|
+
disabled: busy || toApprove.length === 0,
|
|
252
589
|
onClick: () => void approveSelected(),
|
|
253
590
|
children: [
|
|
254
591
|
/* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
@@ -262,8 +599,8 @@ function TranslationReviewPanel({
|
|
|
262
599
|
variant: "ghost",
|
|
263
600
|
size: "sm",
|
|
264
601
|
commit: true,
|
|
265
|
-
pending:
|
|
266
|
-
disabled: busy
|
|
602
|
+
pending: isQueued("bulk-reset"),
|
|
603
|
+
disabled: busy || toReset.length === 0,
|
|
267
604
|
onClick: () => void resetSelected(),
|
|
268
605
|
children: [
|
|
269
606
|
/* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": true }),
|
|
@@ -272,63 +609,7 @@ function TranslationReviewPanel({
|
|
|
272
609
|
}
|
|
273
610
|
)
|
|
274
611
|
] }),
|
|
275
|
-
/* @__PURE__ */ jsx(
|
|
276
|
-
DataTable,
|
|
277
|
-
{
|
|
278
|
-
rows: visible,
|
|
279
|
-
rowKey: (r) => r.id,
|
|
280
|
-
columns,
|
|
281
|
-
empty: labels.empty,
|
|
282
|
-
defaultPageSize: pageSize,
|
|
283
|
-
storageKey,
|
|
284
|
-
isExpanded: (r) => r.id === openId,
|
|
285
|
-
onRowClick: (r) => setOpenId((open) => open === r.id ? null : r.id),
|
|
286
|
-
expandedRow: (r) => /* @__PURE__ */ jsx(
|
|
287
|
-
TranslationReviewEditor,
|
|
288
|
-
{
|
|
289
|
-
row: r,
|
|
290
|
-
referenceLabel,
|
|
291
|
-
localeLabel,
|
|
292
|
-
readOnly: !editable,
|
|
293
|
-
onSave: onSave ? (write) => onSave([write]) : void 0,
|
|
294
|
-
onClear: onClear ? (key) => onClear([key]) : void 0,
|
|
295
|
-
onClose: () => setOpenId(null),
|
|
296
|
-
formatDate,
|
|
297
|
-
formatError,
|
|
298
|
-
labels: labelsProp
|
|
299
|
-
},
|
|
300
|
-
r.id
|
|
301
|
-
),
|
|
302
|
-
mobileCard: (r) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
303
|
-
/* @__PURE__ */ jsx("code", { className: "block break-all text-xs text-[var(--text-muted)]", children: r.key }),
|
|
304
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm text-[var(--text-primary)]", children: /* @__PURE__ */ jsx(TextCell, { row: r, labels, clamp: true }) }),
|
|
305
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
|
|
306
|
-
/* @__PURE__ */ jsx(ReviewStatusChip, { status: r.status, labels: labelsProp }),
|
|
307
|
-
r.placeholderMismatch && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "danger", variant: "outline", children: labels.placeholderChip })
|
|
308
|
-
] })
|
|
309
|
-
] }),
|
|
310
|
-
selection: editable ? {
|
|
311
|
-
isSelected: (r) => selected.has(r.id),
|
|
312
|
-
onToggle: (r, checked) => setSelected((prev) => {
|
|
313
|
-
const next = new Set([...prev].filter((id) => visibleIds.has(id)));
|
|
314
|
-
if (checked) next.add(r.id);
|
|
315
|
-
else next.delete(r.id);
|
|
316
|
-
return next;
|
|
317
|
-
}),
|
|
318
|
-
onToggleMany: (many, checked) => setSelected((prev) => {
|
|
319
|
-
const next = new Set([...prev].filter((id) => visibleIds.has(id)));
|
|
320
|
-
for (const r of many) {
|
|
321
|
-
if (checked) next.add(r.id);
|
|
322
|
-
else next.delete(r.id);
|
|
323
|
-
}
|
|
324
|
-
return next;
|
|
325
|
-
}),
|
|
326
|
-
allSelected,
|
|
327
|
-
someSelected: selectedRows.length > 0 && !allSelected,
|
|
328
|
-
onToggleAll: (checked) => setSelected(checked ? new Set(visible.map((r) => r.id)) : /* @__PURE__ */ new Set())
|
|
329
|
-
} : void 0
|
|
330
|
-
}
|
|
331
|
-
)
|
|
612
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: groups.map(groupSection) }) : table(visible)
|
|
332
613
|
] });
|
|
333
614
|
}
|
|
334
615
|
export {
|