@eifi1/ui-kit 0.24.1 → 0.25.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 +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 +109 -8
- package/dist/components/translation-review.js +315 -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 +100 -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 +497 -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 +268 -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 { 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,7 @@ 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)";
|
|
35
43
|
function TextCell({ row, labels, clamp }) {
|
|
36
44
|
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
45
|
}
|
|
@@ -53,21 +61,37 @@ function TranslationReviewPanel({
|
|
|
53
61
|
storageKey,
|
|
54
62
|
formatDate,
|
|
55
63
|
formatError,
|
|
64
|
+
swipe = false,
|
|
65
|
+
undo: undoProp,
|
|
66
|
+
groupBy,
|
|
67
|
+
groupLabel,
|
|
68
|
+
groupHeadingAs = "h2",
|
|
56
69
|
className,
|
|
57
70
|
labels: labelsProp
|
|
58
71
|
}) {
|
|
59
72
|
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
60
73
|
const locale = useKitLocale();
|
|
74
|
+
const lock = useWriteLock();
|
|
75
|
+
const phone = !useMediaQuery(MD_UP, true);
|
|
76
|
+
const baseId = useId();
|
|
77
|
+
const rootRef = useRef(null);
|
|
61
78
|
const editable = !readOnly && onSave !== void 0;
|
|
79
|
+
const undoOn = undoProp ?? (swipe || groupBy !== void 0);
|
|
62
80
|
const [ownFilter, setOwnFilter] = useState(DEFAULT_TRANSLATION_REVIEW_FILTER);
|
|
63
81
|
const filter = { ...ownFilter };
|
|
64
82
|
for (const [name, value] of Object.entries(filterProp ?? {})) {
|
|
65
83
|
if (value !== void 0) Object.assign(filter, { [name]: value });
|
|
66
84
|
}
|
|
67
85
|
const [openId, setOpenId] = useState(null);
|
|
86
|
+
const [focusId, setFocusId] = useState(null);
|
|
68
87
|
const [selected, setSelected] = useState(() => /* @__PURE__ */ new Set());
|
|
69
|
-
const [
|
|
88
|
+
const [queued, setQueued] = useState([]);
|
|
89
|
+
const claimed = useRef(/* @__PURE__ */ new Set());
|
|
90
|
+
const tail = useRef(Promise.resolve());
|
|
91
|
+
const undoCount = useRef(0);
|
|
70
92
|
const [failure, setFailure] = useState(null);
|
|
93
|
+
const busy = queued.length > 0;
|
|
94
|
+
const isQueued = (key) => queued.includes(key);
|
|
71
95
|
const rows = useMemo(
|
|
72
96
|
() => areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp],
|
|
73
97
|
[rowsProp, areas, inArea]
|
|
@@ -86,39 +110,113 @@ function TranslationReviewPanel({
|
|
|
86
110
|
() => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),
|
|
87
111
|
[rows, status, source, namespace, query, placeholdersOnly]
|
|
88
112
|
);
|
|
113
|
+
const groups = useMemo(() => {
|
|
114
|
+
if (groupBy === void 0) return null;
|
|
115
|
+
const of = groupBy === "namespace" ? (r) => r.namespace : groupBy === "source" ? (r) => r.source : groupBy;
|
|
116
|
+
return groupTranslationRows(visible, of);
|
|
117
|
+
}, [visible, groupBy]);
|
|
118
|
+
const groupName = (key) => groupLabel?.(key) ?? (groupBy === "source" ? sourceLabels?.[key] : void 0) ?? (key || "\u2014");
|
|
119
|
+
const [groupOpen, setGroupOpen] = useState({});
|
|
120
|
+
const groupOpenByDefault = !(phone && (groups?.length ?? 0) > 1);
|
|
121
|
+
const isGroupOpen = (key) => groupOpen[key] ?? groupOpenByDefault;
|
|
122
|
+
const setGroupOpenFor = (key, open) => setGroupOpen((prev) => ({ ...prev, [key]: open }));
|
|
123
|
+
const [confirming, setConfirming] = useState(null);
|
|
124
|
+
const confirmFocus = useRef(null);
|
|
125
|
+
const [groupSorts, setGroupSorts] = useState([]);
|
|
89
126
|
const setFilter = (patch) => {
|
|
90
127
|
const next = { ...filter, ...patch };
|
|
91
128
|
setOwnFilter(next);
|
|
92
129
|
onFilterChange?.(next);
|
|
93
130
|
setSelected(/* @__PURE__ */ new Set());
|
|
94
131
|
};
|
|
95
|
-
const run =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
const run = (key, action) => {
|
|
133
|
+
if (claimed.current.has(key)) return Promise.resolve(false);
|
|
134
|
+
claimed.current.add(key);
|
|
135
|
+
setQueued((q) => [...q, key]);
|
|
136
|
+
const step = tail.current.then(async () => {
|
|
137
|
+
setFailure(null);
|
|
138
|
+
try {
|
|
139
|
+
await action();
|
|
140
|
+
return true;
|
|
141
|
+
} catch (caught) {
|
|
142
|
+
setFailure(formatError ? formatError(caught) : labels.failed);
|
|
143
|
+
return false;
|
|
144
|
+
} finally {
|
|
145
|
+
claimed.current.delete(key);
|
|
146
|
+
setQueued((q) => q.filter((k) => k !== key));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
tail.current = step;
|
|
150
|
+
return step;
|
|
151
|
+
};
|
|
152
|
+
const sayApproved = (targets, description) => {
|
|
153
|
+
const back = reviewUndo(targets);
|
|
154
|
+
const message = labels.approvedToast(targets.length);
|
|
155
|
+
const save = onSave;
|
|
156
|
+
const clear = onClear;
|
|
157
|
+
if (!save || back.clears.length > 0 && !clear) {
|
|
158
|
+
toast.success(message, { description });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const key = `undo:${++undoCount.current}`;
|
|
162
|
+
toast.undo(message, {
|
|
163
|
+
description,
|
|
164
|
+
onUndo: () => void run(key, async () => {
|
|
165
|
+
if (back.writes.length > 0) await save(back.writes, { origin: "undo", toasted: false });
|
|
166
|
+
if (back.clears.length > 0) await clear?.(back.clears, { origin: "undo", toasted: false });
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const approve = async (key, targets, origin, description) => {
|
|
171
|
+
if (!onSave || targets.length === 0) return false;
|
|
172
|
+
const save = onSave;
|
|
173
|
+
const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, "APPROVED")), { origin, toasted: undoOn }));
|
|
174
|
+
if (ok && undoOn) sayApproved(targets, description);
|
|
175
|
+
return ok;
|
|
176
|
+
};
|
|
177
|
+
const saveFromEditor = async (row, write) => {
|
|
178
|
+
const toasted = undoOn && write.verdict === "APPROVED";
|
|
179
|
+
await onSave?.([write], { origin: "editor", toasted });
|
|
180
|
+
if (toasted) sayApproved([row], row.key);
|
|
181
|
+
};
|
|
182
|
+
const keepFocusAt = (groupKey) => {
|
|
183
|
+
const check = () => {
|
|
184
|
+
const root = rootRef.current;
|
|
185
|
+
const active = document.activeElement;
|
|
186
|
+
if (!root || active && active !== document.body && active.isConnected) return;
|
|
187
|
+
const sections = [...root.querySelectorAll("[data-review-group]")];
|
|
188
|
+
const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];
|
|
189
|
+
section?.querySelector("[data-disclosure-trigger]")?.focus();
|
|
190
|
+
};
|
|
191
|
+
requestAnimationFrame(() => requestAnimationFrame(check));
|
|
192
|
+
};
|
|
193
|
+
const approveGroup = async (group, confirmed) => {
|
|
194
|
+
const targets = unreviewedRows(group.rows);
|
|
195
|
+
if (targets.length === 0) return;
|
|
196
|
+
if (!confirmed && targets.length > pageSize) {
|
|
197
|
+
setGroupOpenFor(group.key, true);
|
|
198
|
+
setConfirming(group.key);
|
|
199
|
+
confirmFocus.current = group.key;
|
|
200
|
+
return;
|
|
106
201
|
}
|
|
202
|
+
const ok = await approve(`group:${group.key}`, targets, "group", groupName(group.key));
|
|
203
|
+
if (ok) setConfirming((key) => key === group.key ? null : key);
|
|
204
|
+
keepFocusAt(group.key);
|
|
107
205
|
};
|
|
108
206
|
const visibleIds = new Set(visible.map((r) => r.id));
|
|
109
207
|
const selectedRows = visible.filter((r) => selected.has(r.id));
|
|
110
|
-
const
|
|
208
|
+
const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;
|
|
209
|
+
const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));
|
|
111
210
|
const toApprove = selectedRows.filter((r) => r.text !== "" && r.status !== "approved");
|
|
112
211
|
const toReset = selectedRows.filter((r) => r.review !== null);
|
|
113
212
|
const approveSelected = async () => {
|
|
114
|
-
if (
|
|
115
|
-
if (await run("bulk-approve", () => onSave(toApprove.map((r) => reviewWrite(r, "APPROVED"))))) {
|
|
116
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
117
|
-
}
|
|
213
|
+
if (await approve("bulk-approve", toApprove, "selection")) setSelected(/* @__PURE__ */ new Set());
|
|
118
214
|
};
|
|
119
215
|
const resetSelected = async () => {
|
|
120
216
|
if (!onClear || toReset.length === 0) return;
|
|
121
|
-
|
|
217
|
+
const clear = onClear;
|
|
218
|
+
const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));
|
|
219
|
+
if (await run("bulk-reset", () => clear(keys, { origin: "selection", toasted: false }))) {
|
|
122
220
|
setSelected(/* @__PURE__ */ new Set());
|
|
123
221
|
}
|
|
124
222
|
};
|
|
@@ -155,8 +253,7 @@ function TranslationReviewPanel({
|
|
|
155
253
|
sortBy: (r) => REVIEW_STATUSES.indexOf(r.status)
|
|
156
254
|
}
|
|
157
255
|
];
|
|
158
|
-
if (editable
|
|
159
|
-
const save = onSave;
|
|
256
|
+
if (editable) {
|
|
160
257
|
columns.push({
|
|
161
258
|
key: "approve",
|
|
162
259
|
header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.approve }),
|
|
@@ -168,18 +265,201 @@ function TranslationReviewPanel({
|
|
|
168
265
|
tone: "muted",
|
|
169
266
|
commit: true,
|
|
170
267
|
label: labels.approve,
|
|
171
|
-
pending:
|
|
172
|
-
disabled: busy
|
|
268
|
+
pending: isQueued(`row:${r.id}`),
|
|
269
|
+
disabled: busy,
|
|
173
270
|
onClick: (event) => {
|
|
174
271
|
event.stopPropagation();
|
|
175
|
-
void
|
|
272
|
+
void approve(`row:${r.id}`, [r], "row", r.key);
|
|
176
273
|
},
|
|
177
274
|
children: /* @__PURE__ */ jsx(Check, {})
|
|
178
275
|
}
|
|
179
276
|
)
|
|
180
277
|
});
|
|
181
278
|
}
|
|
182
|
-
|
|
279
|
+
const swipeActions = (r) => {
|
|
280
|
+
if (!editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
281
|
+
const missing = r.text === "";
|
|
282
|
+
return {
|
|
283
|
+
end: missing || r.status === "approved" ? [] : [
|
|
284
|
+
{
|
|
285
|
+
label: labels.approve,
|
|
286
|
+
icon: /* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
287
|
+
onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
|
|
288
|
+
className: "bg-[var(--success)]",
|
|
289
|
+
armedClassName: "bg-[var(--success)]"
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
start: [
|
|
293
|
+
{
|
|
294
|
+
label: missing ? labels.suggest : labels.flag,
|
|
295
|
+
icon: /* @__PURE__ */ jsx(PencilLine, { className: "size-4", "aria-hidden": true }),
|
|
296
|
+
// Opens the editor rather than sending the string back: "needs a change"
|
|
297
|
+
// with neither a note nor a better wording tells nobody anything.
|
|
298
|
+
onCommit: () => {
|
|
299
|
+
setOpenId(r.id);
|
|
300
|
+
setFocusId(r.id);
|
|
301
|
+
},
|
|
302
|
+
className: missing ? "bg-[var(--brand)]" : "bg-[var(--danger)]",
|
|
303
|
+
armedClassName: missing ? "bg-[var(--brand-hover)]" : "bg-[var(--danger-hover)]"
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const toggleIn = (prev, many, checked) => {
|
|
309
|
+
const next = new Set([...prev].filter((id) => visibleIds.has(id)));
|
|
310
|
+
for (const r of many) {
|
|
311
|
+
if (checked) next.add(r.id);
|
|
312
|
+
else next.delete(r.id);
|
|
313
|
+
}
|
|
314
|
+
return next;
|
|
315
|
+
};
|
|
316
|
+
const selectionFor = (list) => {
|
|
317
|
+
if (!editable) return void 0;
|
|
318
|
+
const picked = list.filter((r) => selected.has(r.id)).length;
|
|
319
|
+
return {
|
|
320
|
+
isSelected: (r) => selected.has(r.id),
|
|
321
|
+
onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),
|
|
322
|
+
onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),
|
|
323
|
+
allSelected: list.length > 0 && picked === list.length,
|
|
324
|
+
someSelected: picked > 0 && picked < list.length,
|
|
325
|
+
onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked))
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
const table = (list, group) => /* @__PURE__ */ jsx(
|
|
329
|
+
DataTable,
|
|
330
|
+
{
|
|
331
|
+
rows: list,
|
|
332
|
+
rowKey: (r) => r.id,
|
|
333
|
+
columns,
|
|
334
|
+
empty: labels.empty,
|
|
335
|
+
defaultPageSize: pageSize,
|
|
336
|
+
...group ? {
|
|
337
|
+
// Inside the group's own card: no frame in a frame, no column rail per group,
|
|
338
|
+
// no scroller per group, and the group's name as the table's.
|
|
339
|
+
frame: false,
|
|
340
|
+
chrome: "minimal",
|
|
341
|
+
maxBodyHeight: "none",
|
|
342
|
+
sorts: groupSorts,
|
|
343
|
+
onSortsChange: setGroupSorts,
|
|
344
|
+
labels: { table: group.name }
|
|
345
|
+
} : { storageKey },
|
|
346
|
+
isExpanded: (r) => r.id === openId,
|
|
347
|
+
onRowClick: (r) => {
|
|
348
|
+
setFocusId(null);
|
|
349
|
+
setOpenId((open) => open === r.id ? null : r.id);
|
|
350
|
+
},
|
|
351
|
+
expandedRow: (r) => /* @__PURE__ */ jsx(
|
|
352
|
+
TranslationReviewEditor,
|
|
353
|
+
{
|
|
354
|
+
row: r,
|
|
355
|
+
referenceLabel,
|
|
356
|
+
localeLabel,
|
|
357
|
+
readOnly: !editable,
|
|
358
|
+
focusWording: focusId === r.id,
|
|
359
|
+
onSave: onSave ? (write) => saveFromEditor(r, write) : void 0,
|
|
360
|
+
onClear: onClear ? (key) => onClear([key], { origin: "editor", toasted: false }) : void 0,
|
|
361
|
+
onClose: () => {
|
|
362
|
+
setOpenId(null);
|
|
363
|
+
setFocusId(null);
|
|
364
|
+
},
|
|
365
|
+
formatDate,
|
|
366
|
+
formatError,
|
|
367
|
+
labels: labelsProp
|
|
368
|
+
},
|
|
369
|
+
r.id
|
|
370
|
+
),
|
|
371
|
+
mobileCard: (r) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
372
|
+
/* @__PURE__ */ jsx("code", { className: "block break-all text-xs text-[var(--text-muted)]", children: r.key }),
|
|
373
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm text-[var(--text-primary)]", children: /* @__PURE__ */ jsx(TextCell, { row: r, labels, clamp: true }) }),
|
|
374
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
|
|
375
|
+
/* @__PURE__ */ jsx(ReviewStatusChip, { status: r.status, labels: labelsProp }),
|
|
376
|
+
r.placeholderMismatch && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "danger", variant: "outline", children: labels.placeholderChip })
|
|
377
|
+
] })
|
|
378
|
+
] }),
|
|
379
|
+
mobileSwipeActions: swipe ? swipeActions : void 0,
|
|
380
|
+
selection: selectionFor(list)
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
const groupSection = (g, index) => {
|
|
384
|
+
const name = groupName(g.key);
|
|
385
|
+
const titleId = `${baseId}-group-${index}`;
|
|
386
|
+
const questionId = `${baseId}-confirm-${index}`;
|
|
387
|
+
const targets = editable ? unreviewedRows(g.rows) : [];
|
|
388
|
+
const key = `group:${g.key}`;
|
|
389
|
+
return /* @__PURE__ */ jsxs(
|
|
390
|
+
Disclosure,
|
|
391
|
+
{
|
|
392
|
+
"data-review-group": g.key,
|
|
393
|
+
headingAs: groupHeadingAs,
|
|
394
|
+
title: /* @__PURE__ */ jsx("span", { id: titleId, children: name }),
|
|
395
|
+
hint: labels.groupCount(g.summary.unreviewed, g.summary.total),
|
|
396
|
+
open: isGroupOpen(g.key),
|
|
397
|
+
onOpenChange: (open) => {
|
|
398
|
+
setGroupOpenFor(g.key, open);
|
|
399
|
+
if (!open) setConfirming((c) => c === g.key ? null : c);
|
|
400
|
+
},
|
|
401
|
+
trailing: targets.length > 0 ? /* @__PURE__ */ jsxs(
|
|
402
|
+
Button,
|
|
403
|
+
{
|
|
404
|
+
variant: "secondary",
|
|
405
|
+
size: "sm",
|
|
406
|
+
commit: true,
|
|
407
|
+
"aria-describedby": titleId,
|
|
408
|
+
pending: isQueued(key) && confirming !== g.key,
|
|
409
|
+
disabled: busy,
|
|
410
|
+
onClick: () => void approveGroup(g, false),
|
|
411
|
+
children: [
|
|
412
|
+
/* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
413
|
+
labels.approveGroup(targets.length)
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
) : void 0,
|
|
417
|
+
bodyClassName: "space-y-0 px-0 pb-0 border-t border-[var(--border)]",
|
|
418
|
+
children: [
|
|
419
|
+
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: [
|
|
420
|
+
/* @__PURE__ */ jsx("p", { id: questionId, children: labels.confirmGroup(targets.length, name) }),
|
|
421
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
422
|
+
/* @__PURE__ */ jsx(
|
|
423
|
+
Button,
|
|
424
|
+
{
|
|
425
|
+
ref: (el) => {
|
|
426
|
+
if (el && confirmFocus.current === g.key) {
|
|
427
|
+
confirmFocus.current = null;
|
|
428
|
+
el.focus();
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
variant: "primary",
|
|
432
|
+
size: "sm",
|
|
433
|
+
commit: true,
|
|
434
|
+
"aria-describedby": questionId,
|
|
435
|
+
pending: isQueued(key),
|
|
436
|
+
disabled: busy,
|
|
437
|
+
onClick: () => void approveGroup(g, true),
|
|
438
|
+
children: labels.approveGroup(targets.length)
|
|
439
|
+
}
|
|
440
|
+
),
|
|
441
|
+
/* @__PURE__ */ jsx(
|
|
442
|
+
Button,
|
|
443
|
+
{
|
|
444
|
+
variant: "ghost",
|
|
445
|
+
size: "sm",
|
|
446
|
+
disabled: isQueued(key),
|
|
447
|
+
onClick: () => {
|
|
448
|
+
setConfirming(null);
|
|
449
|
+
keepFocusAt(g.key);
|
|
450
|
+
},
|
|
451
|
+
children: labels.cancel
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
] })
|
|
455
|
+
] }) }) }),
|
|
456
|
+
table(g.rows, { name })
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
g.key
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("min-w-0 space-y-4", className), children: [
|
|
183
463
|
scopeText && /* @__PURE__ */ jsx(AlertBanner, { tone: "info", variant: "inline", size: "sm", block: true, live: false, children: labels.scope(scopeText) }),
|
|
184
464
|
readOnly && /* @__PURE__ */ jsx(AlertBanner, { tone: "info", variant: "inline", size: "sm", block: true, live: false, children: labels.readOnly }),
|
|
185
465
|
progress && /* @__PURE__ */ jsx(TranslationProgress, { summary, labels: labelsProp }),
|
|
@@ -224,15 +504,15 @@ function TranslationReviewPanel({
|
|
|
224
504
|
}
|
|
225
505
|
)
|
|
226
506
|
] }),
|
|
227
|
-
editable &&
|
|
507
|
+
editable && shownRows.length > 0 && // The phone's table has no checkboxes; this is its way into the bulk actions.
|
|
228
508
|
/* @__PURE__ */ jsxs(
|
|
229
509
|
Button,
|
|
230
510
|
{
|
|
231
511
|
variant: "ghost",
|
|
232
512
|
size: "sm",
|
|
233
513
|
className: "md:hidden",
|
|
234
|
-
disabled:
|
|
235
|
-
onClick: () => setSelected(
|
|
514
|
+
disabled: allShownSelected,
|
|
515
|
+
onClick: () => setSelected((prev) => toggleIn(prev, shownRows, true)),
|
|
236
516
|
children: [
|
|
237
517
|
/* @__PURE__ */ jsx(ListChecks, { className: "size-4", "aria-hidden": true }),
|
|
238
518
|
labels.selectShown
|
|
@@ -247,8 +527,8 @@ function TranslationReviewPanel({
|
|
|
247
527
|
variant: "primary",
|
|
248
528
|
size: "sm",
|
|
249
529
|
commit: true,
|
|
250
|
-
pending:
|
|
251
|
-
disabled: busy
|
|
530
|
+
pending: isQueued("bulk-approve"),
|
|
531
|
+
disabled: busy || toApprove.length === 0,
|
|
252
532
|
onClick: () => void approveSelected(),
|
|
253
533
|
children: [
|
|
254
534
|
/* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
@@ -262,8 +542,8 @@ function TranslationReviewPanel({
|
|
|
262
542
|
variant: "ghost",
|
|
263
543
|
size: "sm",
|
|
264
544
|
commit: true,
|
|
265
|
-
pending:
|
|
266
|
-
disabled: busy
|
|
545
|
+
pending: isQueued("bulk-reset"),
|
|
546
|
+
disabled: busy || toReset.length === 0,
|
|
267
547
|
onClick: () => void resetSelected(),
|
|
268
548
|
children: [
|
|
269
549
|
/* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": true }),
|
|
@@ -272,63 +552,7 @@ function TranslationReviewPanel({
|
|
|
272
552
|
}
|
|
273
553
|
)
|
|
274
554
|
] }),
|
|
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
|
-
)
|
|
555
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: groups.map(groupSection) }) : table(visible)
|
|
332
556
|
] });
|
|
333
557
|
}
|
|
334
558
|
export {
|