@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
package/src/i18n/german.ts
CHANGED
|
@@ -732,6 +732,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
732
732
|
roleOf: (column) => `Was enthält die Spalte „${column}“?`,
|
|
733
733
|
ignore: "Ignorieren",
|
|
734
734
|
requiredRole: (role) => `${role} (erforderlich)`,
|
|
735
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
735
736
|
previewOf: (shown, total) =>
|
|
736
737
|
shown === 1
|
|
737
738
|
? `Die erste von ${n(total)} Zeilen`
|
|
@@ -788,6 +789,15 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
788
789
|
scope: (areas) => `Ihre Prüfung ist beschränkt auf: ${areas}.`,
|
|
789
790
|
exportCorrections: (count) => `Korrekturen exportieren (${n(count)})`,
|
|
790
791
|
failed: "Das hat nicht geklappt. Bitte erneut versuchen.",
|
|
792
|
+
approvedToast: (count) => (count === 1 ? "Text bestätigt" : `${n(count)} Texte bestätigt`),
|
|
793
|
+
groupCount: (unreviewed, total) => `${n(unreviewed)} ungeprüft / ${n(total)}`,
|
|
794
|
+
approveGroup: (count) => `Ungeprüfte bestätigen (${n(count)})`,
|
|
795
|
+
// The group's name quoted, as `columnMapper.roleOf`'s column: it is often a key
|
|
796
|
+
// ("billing"), which unquoted would read as part of the sentence.
|
|
797
|
+
confirmGroup: (count, group) =>
|
|
798
|
+
count === 1
|
|
799
|
+
? `Den ungeprüften Text in „${group}“ bestätigen?`
|
|
800
|
+
: `Alle ${n(count)} ungeprüften Texte in „${group}“ bestätigen – auch die nicht angezeigten?`,
|
|
791
801
|
},
|
|
792
802
|
};
|
|
793
803
|
}
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -126,6 +126,13 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
|
|
|
126
126
|
progress: (approved, total) => `${n(approved)} of ${n(total)} approved`,
|
|
127
127
|
localeProgress: (approved, total) => `${n(approved)}/${n(total)}`,
|
|
128
128
|
exportCorrections: (count) => `Export corrections (${n(count)})`,
|
|
129
|
+
approvedToast: (count) => (count === 1 ? "String approved" : `${n(count)} strings approved`),
|
|
130
|
+
groupCount: (unreviewed, total) => `${n(unreviewed)} unreviewed / ${n(total)}`,
|
|
131
|
+
approveGroup: (count) => `Approve unreviewed (${n(count)})`,
|
|
132
|
+
confirmGroup: (count, group) =>
|
|
133
|
+
count === 1
|
|
134
|
+
? `Approve the unreviewed string in ${group}?`
|
|
135
|
+
: `Approve all ${n(count)} unreviewed strings in ${group}, including those not on screen?`,
|
|
129
136
|
},
|
|
130
137
|
characterCount: {
|
|
131
138
|
...d.characterCount,
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -712,6 +712,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
712
712
|
roleOf: (column) => `¿Qué contiene la columna «${column}»?`,
|
|
713
713
|
ignore: "Ignorar",
|
|
714
714
|
requiredRole: (role) => `${role} (obligatorio)`,
|
|
715
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
715
716
|
previewOf: (shown, total) =>
|
|
716
717
|
shown === 1
|
|
717
718
|
? `La primera de ${n(total)} filas`
|
|
@@ -768,6 +769,14 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
768
769
|
scope: (areas) => `Su revisión se limita a: ${areas}.`,
|
|
769
770
|
exportCorrections: (count) => `Exportar correcciones (${n(count)})`,
|
|
770
771
|
failed: "No ha funcionado. Inténtelo de nuevo.",
|
|
772
|
+
approvedToast: (count) => (count === 1 ? "Texto aprobado" : `${n(count)} textos aprobados`),
|
|
773
|
+
// "sin revisar" does not agree with the count, so one form serves both.
|
|
774
|
+
groupCount: (unreviewed, total) => `${n(unreviewed)} sin revisar / ${n(total)}`,
|
|
775
|
+
approveGroup: (count) => `Aprobar los sin revisar (${n(count)})`,
|
|
776
|
+
confirmGroup: (count, group) =>
|
|
777
|
+
count === 1
|
|
778
|
+
? `¿Aprobar el texto sin revisar de «${group}»?`
|
|
779
|
+
: `¿Aprobar los ${n(count)} textos sin revisar de «${group}», incluidos los que no están en pantalla?`,
|
|
771
780
|
},
|
|
772
781
|
};
|
|
773
782
|
}
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -730,6 +730,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
730
730
|
roleOf: (column) => `Que contient la colonne «\u202f${column}\u202f»\u202f?`,
|
|
731
731
|
ignore: "Ignorer",
|
|
732
732
|
requiredRole: (role) => `${role} (obligatoire)`,
|
|
733
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
733
734
|
previewOf: (shown, total) =>
|
|
734
735
|
shown < 2
|
|
735
736
|
? `La première ligne sur ${n(total)}`
|
|
@@ -785,6 +786,14 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
785
786
|
scope: (areas) => `Votre relecture se limite à\u00a0: ${areas}.`,
|
|
786
787
|
exportCorrections: (count) => `Exporter les corrections (${n(count)})`,
|
|
787
788
|
failed: "Cela n’a pas fonctionné. Veuillez réessayer.",
|
|
789
|
+
approvedToast: (count) => (count < 2 ? "Texte approuvé" : `${n(count)} textes approuvés`),
|
|
790
|
+
groupCount: (unreviewed, total) =>
|
|
791
|
+
`${n(unreviewed)} non ${plural(unreviewed, "relu", "relus")} / ${n(total)}`,
|
|
792
|
+
approveGroup: (count) => `Approuver les non relus (${n(count)})`,
|
|
793
|
+
confirmGroup: (count, group) =>
|
|
794
|
+
count < 2
|
|
795
|
+
? `Approuver le texte non relu de «\u202f${group}\u202f»\u202f?`
|
|
796
|
+
: `Approuver les ${n(count)} textes non relus de «\u202f${group}\u202f», y compris ceux qui ne sont pas à l’écran\u202f?`,
|
|
788
797
|
},
|
|
789
798
|
};
|
|
790
799
|
}
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -717,6 +717,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
717
717
|
roleOf: (column) => `Mit tartalmaz ez az oszlop: „${column}”?`,
|
|
718
718
|
ignore: "Kihagyás",
|
|
719
719
|
requiredRole: (role) => `${role} (kötelező)`,
|
|
720
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
720
721
|
// "(összesen N)", as `dataTable.pageChanged`: both numerals stay bare (convention 3).
|
|
721
722
|
previewOf: (shown, total) => `Az első ${n(shown)} sor (összesen ${n(total)})`,
|
|
722
723
|
// `roles` arrives joined with "vagy" ("Tartozik vagy Követel").
|
|
@@ -771,6 +772,16 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
771
772
|
scope: (areas) => `Az Ön ellenőrzése a következőkre korlátozódik: ${areas}.`,
|
|
772
773
|
exportCorrections: (count) => `Javítások exportálása (${n(count)})`,
|
|
773
774
|
failed: "Ez nem sikerült. Kérjük, próbálja újra.",
|
|
775
|
+
// No plural after a numeral (convention 2).
|
|
776
|
+
approvedToast: (count) => (count === 1 ? "Szöveg jóváhagyva" : `${n(count)} szöveg jóváhagyva`),
|
|
777
|
+
groupCount: (unreviewed, total) => `${n(unreviewed)} ellenőrizetlen / ${n(total)}`,
|
|
778
|
+
approveGroup: (count) => `Ellenőrizetlenek jóváhagyása (${n(count)})`,
|
|
779
|
+
// Opens with the quoted name, as `filePicker`'s refusals: no case ending ("-ban/-ben")
|
|
780
|
+
// has to agree with it. "a(z)" before the count, as `filePicker.rejectedPick`.
|
|
781
|
+
confirmGroup: (count, group) =>
|
|
782
|
+
count === 1
|
|
783
|
+
? `„${group}”: jóváhagyja az ellenőrizetlen szöveget?`
|
|
784
|
+
: `„${group}”: jóváhagyja mind a(z) ${n(count)} ellenőrizetlen szöveget, a képernyőn nem láthatókat is?`,
|
|
774
785
|
},
|
|
775
786
|
};
|
|
776
787
|
}
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -716,6 +716,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
716
716
|
roleOf: (column) => `Che cosa contiene la colonna «${column}»?`,
|
|
717
717
|
ignore: "Ignora",
|
|
718
718
|
requiredRole: (role) => `${role} (obbligatorio)`,
|
|
719
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
719
720
|
previewOf: (shown, total) =>
|
|
720
721
|
shown === 1
|
|
721
722
|
? `La prima di ${n(total)} righe`
|
|
@@ -772,6 +773,14 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
772
773
|
scope: (areas) => `La Sua revisione è limitata a: ${areas}.`,
|
|
773
774
|
exportCorrections: (count) => `Esporta le correzioni (${n(count)})`,
|
|
774
775
|
failed: "Non ha funzionato. Riprovi.",
|
|
776
|
+
approvedToast: (count) => (count === 1 ? "Testo approvato" : `${n(count)} testi approvati`),
|
|
777
|
+
groupCount: (unreviewed, total) =>
|
|
778
|
+
`${n(unreviewed)} ${plural(unreviewed, "non rivisto", "non rivisti")} / ${n(total)}`,
|
|
779
|
+
approveGroup: (count) => `Approva i non rivisti (${n(count)})`,
|
|
780
|
+
confirmGroup: (count, group) =>
|
|
781
|
+
count === 1
|
|
782
|
+
? `Approvare il testo non rivisto in «${group}»?`
|
|
783
|
+
: `Approvare tutti i ${n(count)} testi non rivisti in «${group}», compresi quelli non visibili sullo schermo?`,
|
|
775
784
|
},
|
|
776
785
|
};
|
|
777
786
|
}
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -694,6 +694,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
694
694
|
roleOf: (column) => `“${column}”列的内容是什么?`,
|
|
695
695
|
ignore: "忽略",
|
|
696
696
|
requiredRole: (role) => `${role}(必填)`,
|
|
697
|
+
requiredRoleShort: (role) => `${role} *`,
|
|
697
698
|
previewOf: (shown, total) => `显示前 ${n(shown)} 行,共 ${n(total)} 行`,
|
|
698
699
|
// `roles` arrives joined with "或" ("借方或贷方").
|
|
699
700
|
oneOf: (roles) => `${roles}(任选其一)`,
|
|
@@ -747,6 +748,13 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
747
748
|
scope: (areas) => `审校范围仅限于:${areas}。`,
|
|
748
749
|
exportCorrections: (count) => `导出更正(${n(count)})`,
|
|
749
750
|
failed: "操作未成功,请重试。",
|
|
751
|
+
approvedToast: (count) => (count === 1 ? "文本已通过" : `已通过 ${n(count)} 条文本`),
|
|
752
|
+
groupCount: (unreviewed, total) => `未审校 ${n(unreviewed)} / ${n(total)}`,
|
|
753
|
+
approveGroup: (count) => `通过未审校项(${n(count)})`,
|
|
754
|
+
confirmGroup: (count, group) =>
|
|
755
|
+
count === 1
|
|
756
|
+
? `通过“${group}”中的未审校文本?`
|
|
757
|
+
: `通过“${group}”中全部 ${n(count)} 条未审校文本(包括未显示在屏幕上的)?`,
|
|
750
758
|
},
|
|
751
759
|
};
|
|
752
760
|
}
|
package/src/i18n/review.ts
CHANGED
|
@@ -193,6 +193,16 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
193
193
|
"translationReview.lastFlagged": [["{{name}}", "{{date}}"]],
|
|
194
194
|
"translationReview.scope": [["{{areas}}"]],
|
|
195
195
|
"translationReview.exportCorrections": [[0], [1], [3]],
|
|
196
|
+
"translationReview.approvedToast": one,
|
|
197
|
+
"translationReview.groupCount": [
|
|
198
|
+
[1, 12],
|
|
199
|
+
[3, 12],
|
|
200
|
+
],
|
|
201
|
+
"translationReview.approveGroup": one,
|
|
202
|
+
"translationReview.confirmGroup": [
|
|
203
|
+
[1, "{{group}}"],
|
|
204
|
+
[3, "{{group}}"],
|
|
205
|
+
],
|
|
196
206
|
"characterCount.count": [[12, 80]],
|
|
197
207
|
"characterCount.remaining": one,
|
|
198
208
|
"inlineEdit.edit": [["{{label}}"]],
|
|
@@ -210,6 +220,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
210
220
|
"columnMapper.columnN": [[3]],
|
|
211
221
|
"columnMapper.roleOf": [["{{column}}"]],
|
|
212
222
|
"columnMapper.requiredRole": [["{{role}}"]],
|
|
223
|
+
"columnMapper.requiredRoleShort": [["{{role}}"]],
|
|
213
224
|
"columnMapper.previewOf": [
|
|
214
225
|
[1, 12],
|
|
215
226
|
[10, 250],
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,9 @@ export type { DebouncedCallbackOptions, DebouncedFunction } from "./hooks/use-de
|
|
|
47
47
|
// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.
|
|
48
48
|
export { useSearchParamState, useTabParam, useDialogParam } from "./hooks/use-search-param-state";
|
|
49
49
|
export type { SearchParamStateOptions, DialogParam } from "./hooks/use-search-param-state";
|
|
50
|
+
// 0.25: several keys, one navigation (keksdose live #378 — four setters in a row clobbered).
|
|
51
|
+
export { useSearchParamsState } from "./hooks/use-search-param-state";
|
|
52
|
+
export type { SearchParamField, SearchParamFields, SearchParamsUpdate } from "./hooks/use-search-param-state";
|
|
50
53
|
// Copy that reports whether it worked — keksdose's copy buttons said "Copied" when it had not.
|
|
51
54
|
export { useCopyToClipboard, copyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
52
55
|
export type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from "./hooks/use-copy-to-clipboard";
|
|
@@ -388,6 +391,10 @@ export {
|
|
|
388
391
|
mergeReviews,
|
|
389
392
|
dropReviews,
|
|
390
393
|
translationCorrections,
|
|
394
|
+
// 0.25: grouping and the approvals' Undo (keksdose live #377).
|
|
395
|
+
groupTranslationRows,
|
|
396
|
+
unreviewedRows,
|
|
397
|
+
reviewUndo,
|
|
391
398
|
} from "./lib/translation-review";
|
|
392
399
|
export type {
|
|
393
400
|
TranslationVerdict,
|
|
@@ -402,6 +409,8 @@ export type {
|
|
|
402
409
|
TranslationSummary,
|
|
403
410
|
TranslationReviewFilter,
|
|
404
411
|
TranslationCorrection,
|
|
412
|
+
TranslationRowGroup,
|
|
413
|
+
TranslationReviewUndo,
|
|
405
414
|
} from "./lib/translation-review";
|
|
406
415
|
export {
|
|
407
416
|
TranslationReviewPanel,
|
|
@@ -422,6 +431,9 @@ export type {
|
|
|
422
431
|
TranslationLocaleTabsProps,
|
|
423
432
|
TranslationExportButtonProps,
|
|
424
433
|
TranslationReviewLabels,
|
|
434
|
+
TranslationReviewSaveInfo,
|
|
435
|
+
TranslationReviewOrigin,
|
|
436
|
+
TranslationReviewGroupBy,
|
|
425
437
|
} from "./components/translation-review";
|
|
426
438
|
// The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.
|
|
427
439
|
export { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from "./components/legal";
|
package/src/lib/strip-fade.ts
CHANGED
|
@@ -1,12 +1,129 @@
|
|
|
1
|
-
import { useLayoutEffect, useState
|
|
1
|
+
import { useCallback, useLayoutEffect, useState } from "react";
|
|
2
|
+
import type { CSSProperties, FocusEvent, RefObject } from "react";
|
|
2
3
|
|
|
3
4
|
// How far the fade at a scrolled strip's cut edge reaches, and how much room a tab
|
|
4
5
|
// brought into view keeps from that edge so the fade never lies on top of it.
|
|
5
6
|
export const TAB_FADE_PX = 24;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
|
|
9
|
+
* What {@link useStripFade} can be asked to do beyond the strip it was written for.
|
|
10
|
+
*/
|
|
11
|
+
export interface StripFadeOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Keep the strip's own SCROLLBARS out of the fade (0.25, for the kit Table's and
|
|
14
|
+
* DataTable's `edgeFade`). A `mask-image` masks everything the element paints, and
|
|
15
|
+
* a classic scrollbar is painted by the element: on a desktop with always-visible
|
|
16
|
+
* scrollbars, a DataTable's vertical scrollbar sits at the inline end, inside the
|
|
17
|
+
* very band that fades whenever columns are hidden that way — it all but vanished,
|
|
18
|
+
* and the horizontal one lost its ends. With this set, the mask is opaque over
|
|
19
|
+
* each scrollbar (measured: `offsetWidth − clientWidth − borders`, its side read
|
|
20
|
+
* off `clientLeft`, which is where an RTL scrollbar on the left shows up) and the
|
|
21
|
+
* fade runs inside it, from the content's own edge. Where scrollbars take no room
|
|
22
|
+
* — phones, overlay scrollbars, `scrollbar-width: none` — nothing changes: the
|
|
23
|
+
* mask is the plain one. Off by default, so Tabs and AppShell paint exactly as
|
|
24
|
+
* they did.
|
|
25
|
+
*/
|
|
26
|
+
spareScrollbars?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The room the classic scrollbars of a strip take, and the borders beside them. */
|
|
30
|
+
export interface StripScrollbars {
|
|
31
|
+
/** Vertical scrollbar on the left (RTL in Chromium and Firefox), in px. */
|
|
32
|
+
left: number;
|
|
33
|
+
/** Vertical scrollbar on the right, in px. */
|
|
34
|
+
right: number;
|
|
35
|
+
/** Horizontal scrollbar, in px. */
|
|
36
|
+
bottom: number;
|
|
37
|
+
/** The border widths outside each of them, in px — the fade still covers a border. */
|
|
38
|
+
borderLeft: number;
|
|
39
|
+
borderRight: number;
|
|
40
|
+
borderBottom: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NO_SCROLLBARS: StripScrollbars = {
|
|
44
|
+
left: 0,
|
|
45
|
+
right: 0,
|
|
46
|
+
bottom: 0,
|
|
47
|
+
borderLeft: 0,
|
|
48
|
+
borderRight: 0,
|
|
49
|
+
borderBottom: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// A scrollbar is never this thin; anything under it is sub-pixel rounding between
|
|
53
|
+
// `offsetWidth` and `clientWidth`, not a scrollbar.
|
|
54
|
+
const MIN_SCROLLBAR_PX = 2;
|
|
55
|
+
|
|
56
|
+
function measureScrollbars(el: HTMLElement, style: CSSStyleDeclaration): StripScrollbars {
|
|
57
|
+
const px = (value: string) => parseFloat(value) || 0;
|
|
58
|
+
const borderLeft = px(style.borderLeftWidth);
|
|
59
|
+
const borderRight = px(style.borderRightWidth);
|
|
60
|
+
const borderTop = px(style.borderTopWidth);
|
|
61
|
+
const borderBottom = px(style.borderBottomWidth);
|
|
62
|
+
const vertical = el.offsetWidth - el.clientWidth - borderLeft - borderRight;
|
|
63
|
+
const horizontal = el.offsetHeight - el.clientHeight - borderTop - borderBottom;
|
|
64
|
+
const onLeft = el.clientLeft - borderLeft >= MIN_SCROLLBAR_PX;
|
|
65
|
+
const v = vertical >= MIN_SCROLLBAR_PX ? Math.round(vertical) : 0;
|
|
66
|
+
return {
|
|
67
|
+
left: onLeft ? v : 0,
|
|
68
|
+
right: onLeft ? 0 : v,
|
|
69
|
+
bottom: horizontal >= MIN_SCROLLBAR_PX ? Math.round(horizontal) : 0,
|
|
70
|
+
borderLeft,
|
|
71
|
+
borderRight,
|
|
72
|
+
borderBottom,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sameScrollbars = (a: StripScrollbars, b: StripScrollbars) =>
|
|
77
|
+
a.left === b.left &&
|
|
78
|
+
a.right === b.right &&
|
|
79
|
+
a.bottom === b.bottom &&
|
|
80
|
+
a.borderLeft === b.borderLeft &&
|
|
81
|
+
a.borderRight === b.borderRight &&
|
|
82
|
+
a.borderBottom === b.borderBottom;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The `mask-image` for a strip whose `left` and/or `right` (physical) edge has
|
|
86
|
+
* content behind it, or `undefined` when neither has. Pure, for {@link useStripFade}
|
|
87
|
+
* and its tests; `scrollbars` (see {@link StripFadeOptions.spareScrollbars}) keeps
|
|
88
|
+
* those bands opaque and starts the fade at the content's edge instead of the box's.
|
|
89
|
+
*/
|
|
90
|
+
export function stripFadeMask(
|
|
91
|
+
edges: { left: boolean; right: boolean },
|
|
92
|
+
scrollbars: StripScrollbars = NO_SCROLLBARS,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
if (!edges.left && !edges.right) return undefined;
|
|
95
|
+
const F = TAB_FADE_PX;
|
|
96
|
+
const sb = scrollbars;
|
|
97
|
+
// The plain mask, as Tabs and AppShell have always drawn it.
|
|
98
|
+
let l = edges.left ? `transparent, #000 ${F}px` : "#000";
|
|
99
|
+
let r = edges.right ? `#000 calc(100% - ${F}px), transparent` : "#000";
|
|
100
|
+
const fromRight = (px: number) => (px === 0 ? "100%" : `calc(100% - ${px}px)`);
|
|
101
|
+
// A vertical scrollbar on a FADED side: the border outside it fades with the rest
|
|
102
|
+
// of that edge, the scrollbar itself stays whole, and the fade starts inside it.
|
|
103
|
+
if (edges.left && sb.left > 0) {
|
|
104
|
+
const inner = sb.borderLeft + sb.left;
|
|
105
|
+
const border = sb.borderLeft > 0 ? `transparent ${sb.borderLeft}px, ` : "";
|
|
106
|
+
l = `${border}#000 ${sb.borderLeft}px, #000 ${inner}px, transparent ${inner}px, #000 ${inner + F}px`;
|
|
107
|
+
}
|
|
108
|
+
if (edges.right && sb.right > 0) {
|
|
109
|
+
const inner = sb.borderRight + sb.right;
|
|
110
|
+
const border =
|
|
111
|
+
sb.borderRight > 0 ? `, #000 ${fromRight(sb.borderRight)}, transparent ${fromRight(sb.borderRight)}` : "";
|
|
112
|
+
r = `#000 ${fromRight(inner + F)}, transparent ${fromRight(inner)}, #000 ${fromRight(inner)}${border}`;
|
|
113
|
+
}
|
|
114
|
+
const across = `linear-gradient(to right, ${l}, ${r})`;
|
|
115
|
+
if (sb.bottom <= 0) return across;
|
|
116
|
+
// The horizontal scrollbar, as a second layer: layers ADD, so this band is opaque
|
|
117
|
+
// over its whole width whatever the first layer says there.
|
|
118
|
+
const top = sb.borderBottom + sb.bottom;
|
|
119
|
+
const border = sb.borderBottom > 0 ? `transparent ${sb.borderBottom}px, ` : "";
|
|
120
|
+
return `${across}, linear-gradient(to top, ${border}#000 ${sb.borderBottom}px, #000 ${top}px, transparent ${top}px)`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Shared by Tabs' strip, AppShell's scrolling phone sub-nav, (0.24) ColumnRoleTable's
|
|
125
|
+
* preview and (0.25) Table's and DataTable's `edgeFade`; internal, not part of the
|
|
126
|
+
* package's surface.
|
|
10
127
|
*
|
|
11
128
|
* Which ends of a sideways-scrolling strip have tabs cut off behind them, as the
|
|
12
129
|
* `mask-image` that fades those ends out — or `undefined` while everything fits.
|
|
@@ -22,21 +139,27 @@ export const TAB_FADE_PX = 24;
|
|
|
22
139
|
*/
|
|
23
140
|
export function useStripFade(
|
|
24
141
|
ref: RefObject<HTMLElement | null>,
|
|
142
|
+
options?: StripFadeOptions,
|
|
25
143
|
): { mask: string | undefined; overflow: "start" | "end" | "both" | undefined } {
|
|
26
|
-
const
|
|
144
|
+
const spare = options?.spareScrollbars === true;
|
|
145
|
+
const [edges, setEdges] = useState({ left: false, right: false, rtl: false, scrollbars: NO_SCROLLBARS });
|
|
27
146
|
useLayoutEffect(() => {
|
|
28
147
|
const el = ref.current;
|
|
29
148
|
if (!el) return;
|
|
30
149
|
const measure = () => {
|
|
31
150
|
const hidden = el.scrollWidth - el.clientWidth;
|
|
32
151
|
const scrolled = Math.abs(el.scrollLeft);
|
|
33
|
-
const
|
|
152
|
+
const style = getComputedStyle(el);
|
|
153
|
+
const rtl = style.direction === "rtl";
|
|
34
154
|
const atStart = scrolled > 1;
|
|
35
155
|
const atEnd = hidden - scrolled > 1;
|
|
36
156
|
const left = rtl ? atEnd : atStart;
|
|
37
157
|
const right = rtl ? atStart : atEnd;
|
|
158
|
+
const scrollbars = spare && (left || right) ? measureScrollbars(el, style) : NO_SCROLLBARS;
|
|
38
159
|
setEdges((prev) =>
|
|
39
|
-
prev.left === left && prev.right === right && prev.rtl === rtl
|
|
160
|
+
prev.left === left && prev.right === right && prev.rtl === rtl && sameScrollbars(prev.scrollbars, scrollbars)
|
|
161
|
+
? prev
|
|
162
|
+
: { left, right, rtl, scrollbars },
|
|
40
163
|
);
|
|
41
164
|
};
|
|
42
165
|
measure();
|
|
@@ -51,14 +174,13 @@ export function useStripFade(
|
|
|
51
174
|
observer?.disconnect();
|
|
52
175
|
};
|
|
53
176
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const r = edges.right ? `#000 calc(100% - ${TAB_FADE_PX}px), transparent` : "#000";
|
|
177
|
+
const mask = stripFadeMask(edges, edges.scrollbars);
|
|
178
|
+
if (mask === undefined) return { mask: undefined, overflow: undefined };
|
|
57
179
|
const rtl = edges.rtl;
|
|
58
180
|
const start = rtl ? edges.right : edges.left;
|
|
59
181
|
const end = rtl ? edges.left : edges.right;
|
|
60
182
|
return {
|
|
61
|
-
mask
|
|
183
|
+
mask,
|
|
62
184
|
overflow: start && end ? "both" : start ? "start" : "end",
|
|
63
185
|
};
|
|
64
186
|
}
|
|
@@ -67,16 +189,99 @@ export function useStripFade(
|
|
|
67
189
|
* Scroll `item` into view inside `strip` — the strip only (`scrollBy` on it, never
|
|
68
190
|
* `scrollIntoView`, which would scroll the page too) — far enough to clear the fade.
|
|
69
191
|
* A no-op when the strip fits or the item is already clear of both edges.
|
|
192
|
+
*
|
|
193
|
+
* The edges are the strip's CONTENT edges (0.25: `clientLeft` + `clientWidth`), not
|
|
194
|
+
* its box's: a vertical scrollbar or a side border takes room the item can never be
|
|
195
|
+
* scrolled into, and {@link StripFadeOptions.spareScrollbars} starts the fade inside
|
|
196
|
+
* the scrollbar. A Tabs strip has neither, so its edges are where they were.
|
|
70
197
|
*/
|
|
71
198
|
export function scrollIntoStrip(strip: HTMLElement, item: HTMLElement): void {
|
|
72
199
|
if (strip.scrollWidth <= strip.clientWidth) return;
|
|
73
|
-
const
|
|
200
|
+
const box = strip.getBoundingClientRect();
|
|
201
|
+
const left = box.left + strip.clientLeft;
|
|
202
|
+
const right = left + strip.clientWidth;
|
|
74
203
|
const t = item.getBoundingClientRect();
|
|
75
204
|
const delta =
|
|
76
|
-
t.left <
|
|
77
|
-
? t.left -
|
|
78
|
-
: t.right >
|
|
79
|
-
? t.right -
|
|
205
|
+
t.left < left + TAB_FADE_PX
|
|
206
|
+
? t.left - left - TAB_FADE_PX
|
|
207
|
+
: t.right > right - TAB_FADE_PX
|
|
208
|
+
? t.right - right + TAB_FADE_PX
|
|
80
209
|
: 0;
|
|
81
210
|
if (delta !== 0) strip.scrollBy({ left: delta });
|
|
82
211
|
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Focus landed on `focused` inside a faded table's scroller (0.25, Table and
|
|
215
|
+
* DataTable `edgeFade`): bring it clear of the fade. The browser's own focus scroll
|
|
216
|
+
* stops at the scroller's edge — under the fade, where a focused control is a few
|
|
217
|
+
* faint pixels. Its whole CELL is brought clear when the cell fits between the two
|
|
218
|
+
* fades (a select that fills its header, as ColumnRoleTable's do, then keeps the
|
|
219
|
+
* cell's padding round it too); a cell wider than that — a long note with a link at
|
|
220
|
+
* its end — would be brought in by its start and leave the link out, so then the
|
|
221
|
+
* focused element itself is what is scrolled clear.
|
|
222
|
+
*/
|
|
223
|
+
export function scrollFocusIntoStrip(strip: HTMLElement, focused: HTMLElement): void {
|
|
224
|
+
if (focused === strip || !strip.contains(focused)) return;
|
|
225
|
+
const cell = focused.closest<HTMLElement>("td, th");
|
|
226
|
+
const rect = cell && strip.contains(cell) ? cell.getBoundingClientRect() : null;
|
|
227
|
+
const fits = rect !== null && rect.right - rect.left <= strip.clientWidth - 2 * TAB_FADE_PX;
|
|
228
|
+
scrollIntoStrip(strip, fits && cell ? cell : focused);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `edgeFade` off: {@link useStripFade} gets no element and measures nothing. */
|
|
232
|
+
const NO_ELEMENT: RefObject<HTMLElement | null> = { current: null };
|
|
233
|
+
const SPARE_SCROLLBARS: StripFadeOptions = { spareScrollbars: true };
|
|
234
|
+
|
|
235
|
+
/** `:focus-visible`, or `false` where the engine cannot say. */
|
|
236
|
+
function focusVisible(el: Element): boolean {
|
|
237
|
+
try {
|
|
238
|
+
return el.matches(":focus-visible");
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** What {@link useEdgeFade} puts on the scroller; every field `undefined` while off. */
|
|
245
|
+
export interface EdgeFadeProps {
|
|
246
|
+
"data-overflow": "start" | "end" | "both" | undefined;
|
|
247
|
+
style: CSSProperties | undefined;
|
|
248
|
+
onFocus: ((event: FocusEvent<HTMLElement>) => void) | undefined;
|
|
249
|
+
onBlur: ((event: FocusEvent<HTMLElement>) => void) | undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Table's and DataTable's `edgeFade` on their scroller (0.25; internal, like the rest
|
|
254
|
+
* of this file): the measured fade with the scroller's own scrollbars spared, its
|
|
255
|
+
* `data-overflow`, focus inside brought clear of it ({@link scrollFocusIntoStrip}),
|
|
256
|
+
* and — on a scroller that is itself a tab stop, as Table's is while it overflows —
|
|
257
|
+
* the fade lifted while the scroller shows the keyboard's focus outline: an inset
|
|
258
|
+
* outline lies exactly in the faded band, and its sides would fade out with the
|
|
259
|
+
* columns. Inert while `on` is false: no measuring, no listeners, no attributes.
|
|
260
|
+
*
|
|
261
|
+
* `style` is the mask alone; a caller with a style of its own spreads it over that.
|
|
262
|
+
*/
|
|
263
|
+
export function useEdgeFade(scroller: RefObject<HTMLElement | null>, on: boolean): EdgeFadeProps {
|
|
264
|
+
const fade = useStripFade(on ? scroller : NO_ELEMENT, SPARE_SCROLLBARS);
|
|
265
|
+
const [selfFocused, setSelfFocused] = useState(false);
|
|
266
|
+
const onFocus = useCallback((event: FocusEvent<HTMLElement>) => {
|
|
267
|
+
const box = event.currentTarget;
|
|
268
|
+
if (event.target === box) {
|
|
269
|
+
// A click in the table's text focuses the scroller too (it is the nearest tab
|
|
270
|
+
// stop); only the keyboard's focus draws an outline worth lifting the fade for.
|
|
271
|
+
if (focusVisible(box)) setSelfFocused(true);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (event.target instanceof HTMLElement) scrollFocusIntoStrip(box, event.target);
|
|
275
|
+
}, []);
|
|
276
|
+
const onBlur = useCallback((event: FocusEvent<HTMLElement>) => {
|
|
277
|
+
if (event.target === event.currentTarget) setSelfFocused(false);
|
|
278
|
+
}, []);
|
|
279
|
+
if (!on) return { "data-overflow": undefined, style: undefined, onFocus: undefined, onBlur: undefined };
|
|
280
|
+
const mask = selfFocused ? undefined : fade.mask;
|
|
281
|
+
return {
|
|
282
|
+
"data-overflow": fade.overflow,
|
|
283
|
+
style: mask ? { maskImage: mask, WebkitMaskImage: mask } : undefined,
|
|
284
|
+
onFocus,
|
|
285
|
+
onBlur,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -387,6 +387,49 @@ export function filterTranslationRows(
|
|
|
387
387
|
);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
// ── Grouping ────────────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/** One group of rows — an area, a source — and how they stand. */
|
|
393
|
+
export interface TranslationRowGroup {
|
|
394
|
+
key: string;
|
|
395
|
+
rows: TranslationRow[];
|
|
396
|
+
summary: TranslationSummary;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Rows in groups, each group where its first row is — the reference's order, so the areas
|
|
401
|
+
* read the way the app's authors wrote the bundle — and each group's rows in their own
|
|
402
|
+
* order. keksdose live #377: a reviewer on a phone works area by area ("the budget
|
|
403
|
+
* strings, then the legal ones"), and a flat list of 3859 rows is not a way to see
|
|
404
|
+
* where an area stands.
|
|
405
|
+
*
|
|
406
|
+
* `groupOf` names a row's group: `(row) => row.namespace` for the areas,
|
|
407
|
+
* `(row) => row.source` for kastlan's screen texts and documents.
|
|
408
|
+
*/
|
|
409
|
+
export function groupTranslationRows(
|
|
410
|
+
rows: readonly TranslationRow[],
|
|
411
|
+
groupOf: (row: TranslationRow) => string,
|
|
412
|
+
): TranslationRowGroup[] {
|
|
413
|
+
const byKey = new Map<string, TranslationRow[]>();
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
const key = groupOf(row);
|
|
416
|
+
const list = byKey.get(key);
|
|
417
|
+
if (list) list.push(row);
|
|
418
|
+
else byKey.set(key, [row]);
|
|
419
|
+
}
|
|
420
|
+
return [...byKey].map(([key, list]) => ({ key, rows: list, summary: summariseRows(list) }));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The rows a group's "Approve the unreviewed" takes: the ones nobody has given a verdict
|
|
425
|
+
* and that have a text. Not `changed` — a string that moved since it was judged asks to
|
|
426
|
+
* be READ again, which a bulk action is the opposite of — and not `missing`, which has
|
|
427
|
+
* nothing to approve.
|
|
428
|
+
*/
|
|
429
|
+
export function unreviewedRows(rows: readonly TranslationRow[]): TranslationRow[] {
|
|
430
|
+
return rows.filter((row) => row.status === "unreviewed" && row.text !== "");
|
|
431
|
+
}
|
|
432
|
+
|
|
390
433
|
// ── Writing ─────────────────────────────────────────────────────────────────────
|
|
391
434
|
|
|
392
435
|
/** What gets stored for a verdict: the wording it was given on, so a later change of the
|
|
@@ -408,6 +451,48 @@ export function reviewWrite(
|
|
|
408
451
|
};
|
|
409
452
|
}
|
|
410
453
|
|
|
454
|
+
/** What takes a set of verdicts back: see {@link reviewUndo}. */
|
|
455
|
+
export interface TranslationReviewUndo {
|
|
456
|
+
/** The verdicts the rows had before, to store again — each on the wording it was
|
|
457
|
+
* given on, so a row that was `changed` reads `changed` again. */
|
|
458
|
+
writes: TranslationReviewWrite[];
|
|
459
|
+
/** The rows that had no verdict before: back to unreviewed. */
|
|
460
|
+
clears: TranslationReviewKey[];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* How to take back what was just said about `rows` — the rows AS THEY WERE before the
|
|
465
|
+
* write, with the verdicts they carried then. The panel's Undo toast (0.25, keksdose live
|
|
466
|
+
* #377) runs it: a swipe on a phone approves in one movement, and a movement can be a
|
|
467
|
+
* mistake.
|
|
468
|
+
*
|
|
469
|
+
* A row that had a verdict gets it stored again (its note, its suggestion, the wording it
|
|
470
|
+
* was judged on); one that had none is cleared. The server stamps the restored verdict
|
|
471
|
+
* with the person undoing and the time, as it stamps every write — the contract has no
|
|
472
|
+
* way to write a verdict in somebody else's name, and should not.
|
|
473
|
+
*/
|
|
474
|
+
export function reviewUndo(rows: readonly TranslationRow[]): TranslationReviewUndo {
|
|
475
|
+
const writes: TranslationReviewWrite[] = [];
|
|
476
|
+
const clears: TranslationReviewKey[] = [];
|
|
477
|
+
for (const row of rows) {
|
|
478
|
+
const before = row.review;
|
|
479
|
+
if (!before) {
|
|
480
|
+
clears.push({ locale: row.locale, key: row.key });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
writes.push({
|
|
484
|
+
locale: row.locale,
|
|
485
|
+
key: row.key,
|
|
486
|
+
text: before.text,
|
|
487
|
+
referenceText: before.referenceText,
|
|
488
|
+
verdict: before.verdict,
|
|
489
|
+
note: before.note,
|
|
490
|
+
suggestion: before.suggestion,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return { writes, clears };
|
|
494
|
+
}
|
|
495
|
+
|
|
411
496
|
/** The app's cached verdict list with `written` in place of what was said before — a
|
|
412
497
|
* save answers with the rows it wrote, so a page patches its copy instead of
|
|
413
498
|
* downloading every verdict again after each click. Any spelling with `locale` and
|