@12-apps/ui 6.1.0 → 6.3.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/components-catalog.md +8 -0
- package/dist/{chunk-35VGGLOC.js → chunk-CEKPLUQV.js} +2 -2
- package/dist/{chunk-35VGGLOC.js.map → chunk-CEKPLUQV.js.map} +1 -1
- package/dist/{chunk-62XQWGXU.js → chunk-MXPXKORR.js} +19 -17
- package/dist/chunk-MXPXKORR.js.map +1 -0
- package/dist/{chunk-7XLEWBKL.js → chunk-N554AFDB.js} +7 -10
- package/dist/chunk-N554AFDB.js.map +1 -0
- package/dist/{chunk-JDO7W4D4.js → chunk-R5ZQZSEQ.js} +2 -20
- package/dist/{chunk-JDO7W4D4.js.map → chunk-R5ZQZSEQ.js.map} +1 -1
- package/dist/{chunk-R4TYXKDD.js → chunk-TPQRIPCN.js} +52 -48
- package/dist/chunk-TPQRIPCN.js.map +1 -0
- package/dist/copy.js +1 -0
- package/dist/copy.js.map +1 -0
- package/dist/data-display/CardKit.js +277 -0
- package/dist/data-display/CardKit.js.map +1 -0
- package/dist/data-display/DataViews.js +664 -616
- package/dist/data-display/DataViews.js.map +1 -1
- package/dist/data-display/SectionOnboarding.js +3 -3
- package/dist/data-display/SectionOnboarding.js.map +1 -1
- package/dist/feedback/ConfirmAction.js +3 -5
- package/dist/form/CategorySelect.js +1 -1
- package/dist/form/CepField.js +1 -1
- package/dist/form/total-form.js +8 -4
- package/dist/form/total-form.js.map +1 -1
- package/dist/layout/ContentToolbar.js +1 -1
- package/dist/layout/TableFilter.js +1 -1
- package/dist/pt-BR.js +222 -0
- package/dist/pt-BR.js.map +1 -0
- package/dist/types/components/data-display/CardKit/CardKebab.d.ts +23 -0
- package/dist/types/components/data-display/CardKit/CardKebab.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/CardKit.types.d.ts +55 -0
- package/dist/types/components/data-display/CardKit/CardKit.types.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts +51 -0
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/index.d.ts +15 -0
- package/dist/types/components/data-display/CardKit/index.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/list-card-parts.d.ts +60 -0
- package/dist/types/components/data-display/CardKit/list-card-parts.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts +17 -0
- package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts +65 -0
- package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts +60 -0
- package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/DataViewsBoard.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/DeleteViewDialog.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts +4 -1
- package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts +0 -5
- package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-card.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-list-card-gutters.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-bar-controls.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-columns-tab.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts +17 -0
- package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +205 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/data-views-display-panel.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-display-tabs.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-export.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-filter-panel.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-grid-helpers.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-more-filters.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-range-pill.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts +3 -2
- package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-scopes.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-search.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-select-all-strip.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-table-parts.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts +1 -1
- package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/index.d.ts +2 -0
- package/dist/types/components/data-display/DataViews/index.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/save-view-preview.d.ts +10 -0
- package/dist/types/components/data-display/DataViews/save-view-preview.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/use-saved-views-controller.d.ts.map +1 -1
- package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts +6 -5
- package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts +1 -3
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts +4 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts +9 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts +2 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/index.d.ts +2 -2
- package/dist/types/components/feedback/ConfirmAction/index.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts +5 -1
- package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts +4 -1
- package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts +5 -2
- package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts +5 -2
- package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts +3 -0
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts +5 -0
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts +4 -1
- package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts.map +1 -1
- package/dist/types/components/form/CepField/CepField.d.ts +1 -1
- package/dist/types/components/form/CepField/CepField.d.ts.map +1 -1
- package/dist/types/components/form/CepField/CepField.types.d.ts +3 -0
- package/dist/types/components/form/CepField/CepField.types.d.ts.map +1 -1
- package/dist/types/components/form/total-form/Fields/CategoryField.d.ts +4 -1
- package/dist/types/components/form/total-form/Fields/CategoryField.d.ts.map +1 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts +4 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts +6 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/SavedViewsMenu.d.ts.map +1 -1
- package/dist/types/copy.d.ts +99 -0
- package/dist/types/copy.d.ts.map +1 -0
- package/dist/types/pt-BR.d.ts +28 -0
- package/dist/types/pt-BR.d.ts.map +1 -0
- package/dist/types/utils/export-rows.d.ts +31 -0
- package/dist/types/utils/export-rows.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/utils.js +50 -0
- package/dist/utils.js.map +1 -1
- package/package.json +14 -2
- package/src/components/data-display/CardKit/CardKebab.tsx +43 -0
- package/src/components/data-display/CardKit/CardKit.md +87 -0
- package/src/components/data-display/CardKit/CardKit.types.ts +60 -0
- package/src/components/data-display/CardKit/card-actions-context.tsx +98 -0
- package/src/components/data-display/CardKit/index.ts +19 -0
- package/src/components/data-display/CardKit/list-card-parts.tsx +163 -0
- package/src/components/data-display/CardKit/row-actions-to-menu.ts +29 -0
- package/src/components/data-display/CardKit/use-remove-confirm.tsx +111 -0
- package/src/components/data-display/CardKit/use-row-confirm.tsx +96 -0
- package/src/components/data-display/DataViews/DataViewsBoard.tsx +7 -4
- package/src/components/data-display/DataViews/DeleteViewDialog.tsx +6 -4
- package/src/components/data-display/DataViews/ManageViewsDialog.tsx +26 -10
- package/src/components/data-display/DataViews/SaveViewModal.tsx +56 -90
- package/src/components/data-display/DataViews/base-card.tsx +3 -1
- package/src/components/data-display/DataViews/base-list-card-gutters.tsx +3 -1
- package/src/components/data-display/DataViews/data-views-bar-controls.tsx +8 -5
- package/src/components/data-display/DataViews/data-views-category-pill.tsx +6 -3
- package/src/components/data-display/DataViews/data-views-columns-tab.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-copy-context.tsx +43 -0
- package/src/components/data-display/DataViews/data-views-copy.ts +214 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +15 -9
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +19 -29
- package/src/components/data-display/DataViews/data-views-export.tsx +9 -10
- package/src/components/data-display/DataViews/data-views-filter-panel.tsx +9 -5
- package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +5 -2
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +6 -3
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +9 -5
- package/src/components/data-display/DataViews/data-views-range-presets.ts +7 -8
- package/src/components/data-display/DataViews/data-views-scopes.tsx +3 -1
- package/src/components/data-display/DataViews/data-views-search.tsx +7 -4
- package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +7 -2
- package/src/components/data-display/DataViews/data-views-view-nav.tsx +22 -16
- package/src/components/data-display/DataViews/index.ts +20 -0
- package/src/components/data-display/DataViews/save-view-preview.tsx +78 -0
- package/src/components/data-display/DataViews/use-saved-views-controller.ts +3 -1
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +3 -3
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.types.ts +8 -7
- package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +2 -2
- package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +12 -5
- package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +9 -1
- package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +12 -4
- package/src/components/feedback/ConfirmAction/index.ts +2 -2
- package/src/components/feedback/ConfirmAction/with-confirmation.tsx +4 -3
- package/src/components/form/CategorySelect/CategoryPanel.tsx +18 -6
- package/src/components/form/CategorySelect/CategoryPanelFoot.tsx +10 -5
- package/src/components/form/CategorySelect/CategoryPanelHead.tsx +8 -3
- package/src/components/form/CategorySelect/CategoryPanelList.tsx +17 -10
- package/src/components/form/CategorySelect/CategorySelect.tsx +3 -3
- package/src/components/form/CategorySelect/CategorySelect.types.ts +5 -0
- package/src/components/form/CategorySelect/CategoryTrigger.tsx +5 -1
- package/src/components/form/CepField/CepField.tsx +6 -9
- package/src/components/form/CepField/CepField.types.ts +3 -0
- package/src/components/form/total-form/Fields/CategoryField.tsx +5 -0
- package/src/components/form/total-form/Fields/CepField.tsx +5 -0
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +6 -1
- package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +1 -21
- package/src/components/layout/TableFilter/TableFilter.tsx +1 -1
- package/src/copy.ts +103 -0
- package/src/pt-BR.ts +247 -0
- package/src/utils/export-rows.ts +95 -0
- package/src/utils/index.ts +1 -0
- package/dist/chunk-62XQWGXU.js.map +0 -1
- package/dist/chunk-7XLEWBKL.js.map +0 -1
- package/dist/chunk-R4TYXKDD.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import type { CepFieldCopy } from "../../../copy";
|
|
3
4
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
5
|
|
|
5
6
|
import { cepDigits, formatCep, isValidCep } from '@12-apps/forms-core';
|
|
@@ -8,13 +9,6 @@ import { FormControl, FormLabel, FormMessage } from '../Form';
|
|
|
8
9
|
import { Input } from '../Input';
|
|
9
10
|
import type { CepAddress, CepFieldProps, CepLookupStatus } from './CepField.types';
|
|
10
11
|
|
|
11
|
-
/** What the user is told for each non-idle outcome. */
|
|
12
|
-
const STATUS_TEXT: Record<Exclude<CepLookupStatus, 'idle'>, string> = {
|
|
13
|
-
loading: 'Buscando endereço…',
|
|
14
|
-
found: 'Endereço preenchido pelo CEP.',
|
|
15
|
-
notfound: 'CEP não encontrado — preencha o endereço manualmente.',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
/** Everything {@link useCepLookup} needs, so the hook signature stays flat. */
|
|
19
13
|
interface LookupOptions {
|
|
20
14
|
digits: string;
|
|
@@ -103,17 +97,19 @@ function CepStatus({
|
|
|
103
97
|
status,
|
|
104
98
|
testId,
|
|
105
99
|
suppressed,
|
|
100
|
+
copy,
|
|
106
101
|
}: {
|
|
107
102
|
status: CepLookupStatus;
|
|
108
103
|
testId: string;
|
|
109
104
|
suppressed: boolean;
|
|
105
|
+
copy: CepFieldCopy;
|
|
110
106
|
}): React.ReactElement {
|
|
111
107
|
const visible = !suppressed && status !== 'idle';
|
|
112
108
|
return (
|
|
113
109
|
<span id={`${testId}-status`} role="status" aria-live="polite">
|
|
114
110
|
{visible && (
|
|
115
111
|
<FormMessage error={status === 'notfound'} dataTestId={`${testId}-${status}`}>
|
|
116
|
-
{
|
|
112
|
+
{copy[status as Exclude<CepLookupStatus, 'idle'>]}
|
|
117
113
|
</FormMessage>
|
|
118
114
|
)}
|
|
119
115
|
</span>
|
|
@@ -157,6 +153,7 @@ export function CepField({
|
|
|
157
153
|
debounceMs = 500,
|
|
158
154
|
name = 'postalCode',
|
|
159
155
|
dataTestId,
|
|
156
|
+
copy,
|
|
160
157
|
}: CepFieldProps): React.ReactElement {
|
|
161
158
|
const testId = dataTestId ?? `cep-field-${name}`;
|
|
162
159
|
|
|
@@ -203,7 +200,7 @@ export function CepField({
|
|
|
203
200
|
{error}
|
|
204
201
|
</FormMessage>
|
|
205
202
|
)}
|
|
206
|
-
<CepStatus status={status} testId={testId} suppressed={Boolean(error)} />
|
|
203
|
+
<CepStatus status={status} testId={testId} suppressed={Boolean(error)} copy={copy} />
|
|
207
204
|
</FormControl>
|
|
208
205
|
);
|
|
209
206
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CepFieldCopy } from "../../../copy";
|
|
1
2
|
/**
|
|
2
3
|
* Types for {@link CepField} — the reusable CEP auto-completer.
|
|
3
4
|
*/
|
|
@@ -29,6 +30,8 @@ export interface CepAddress {
|
|
|
29
30
|
export type CepLookupStatus = 'idle' | 'loading' | 'found' | 'notfound';
|
|
30
31
|
|
|
31
32
|
export interface CepFieldProps {
|
|
33
|
+
/** The lookup status wording. REQUIRED — no default copy. */
|
|
34
|
+
copy: CepFieldCopy;
|
|
32
35
|
/** Current field value. Masked (`01310-100`) or bare — the field re-masks. */
|
|
33
36
|
value: string;
|
|
34
37
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import type { CategorySelectCopy } from "../../../../copy";
|
|
3
4
|
import React from 'react';
|
|
4
5
|
|
|
5
6
|
import { CategorySelect } from '../../CategorySelect';
|
|
@@ -8,6 +9,8 @@ import { useFormContext } from '../FormContext';
|
|
|
8
9
|
|
|
9
10
|
/** Props for the `total-form` {@link CategoryField}. */
|
|
10
11
|
export interface CategoryFieldProps {
|
|
12
|
+
/** The picker\'s words. REQUIRED — this package ships no default copy. */
|
|
13
|
+
copy: CategorySelectCopy;
|
|
11
14
|
/** Field name; also the key into the form's values/errors. */
|
|
12
15
|
name: string;
|
|
13
16
|
/** The categories to choose from, flat, each carrying its `parentId`. */
|
|
@@ -36,12 +39,14 @@ export function CategoryField({
|
|
|
36
39
|
label,
|
|
37
40
|
placeholder,
|
|
38
41
|
allowParentSelection = true,
|
|
42
|
+
copy,
|
|
39
43
|
}: CategoryFieldProps): React.ReactElement {
|
|
40
44
|
const { values, errors, setFieldValue } = useFormContext();
|
|
41
45
|
const error = errors[name];
|
|
42
46
|
|
|
43
47
|
return (
|
|
44
48
|
<CategorySelect
|
|
49
|
+
copy={copy}
|
|
45
50
|
mode="single"
|
|
46
51
|
fullWidth
|
|
47
52
|
label={label}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import type { CepFieldCopy } from "../../../../copy";
|
|
3
4
|
import React, { useCallback, useMemo, useRef } from 'react';
|
|
4
5
|
|
|
5
6
|
import { CepField as BaseCepField, type CepAddress } from '../../CepField';
|
|
@@ -16,6 +17,8 @@ export type CepFills = Partial<Record<CepPart, string>>;
|
|
|
16
17
|
|
|
17
18
|
/** Props for the `total-form` {@link CepField}. */
|
|
18
19
|
export interface CepFieldProps {
|
|
20
|
+
/** The lookup status wording. REQUIRED — no default copy. */
|
|
21
|
+
copy: CepFieldCopy;
|
|
19
22
|
/** Field name; also the key into the form's values/errors. */
|
|
20
23
|
name: string;
|
|
21
24
|
/** Optional visible label. Defaults to `CEP`. */
|
|
@@ -103,6 +106,7 @@ export function CepField({
|
|
|
103
106
|
lookup,
|
|
104
107
|
fills,
|
|
105
108
|
debounceMs,
|
|
109
|
+
copy,
|
|
106
110
|
}: CepFieldProps): React.ReactElement {
|
|
107
111
|
const { values, errors, setFieldValue } = useFormContext();
|
|
108
112
|
const { write, clearOwned } = useOwnedAutofill();
|
|
@@ -128,6 +132,7 @@ export function CepField({
|
|
|
128
132
|
|
|
129
133
|
return (
|
|
130
134
|
<BaseCepField
|
|
135
|
+
copy={copy}
|
|
131
136
|
name={name}
|
|
132
137
|
label={label}
|
|
133
138
|
value={values[name] ?? ''}
|
|
@@ -258,6 +258,11 @@ export interface SavedViewsMenuProps<V extends SavedViewLike = SavedViewLike> {
|
|
|
258
258
|
onToggleShare: (view: V) => void;
|
|
259
259
|
onManageAll: () => void;
|
|
260
260
|
/** Override any of the default (pt-BR) strings. */
|
|
261
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Every word this menu renders. REQUIRED since FUT-760: it used to be
|
|
263
|
+
* `Partial<…>` over a pt-BR default, so a host that passed nothing shipped
|
|
264
|
+
* one product's Portuguese. `PT_BR_SAVED_VIEWS_LABELS` is that exact set.
|
|
265
|
+
*/
|
|
266
|
+
labels: SavedViewsLabels;
|
|
262
267
|
testIdPrefix?: string;
|
|
263
268
|
}
|
|
@@ -13,26 +13,6 @@ import type {
|
|
|
13
13
|
SavedViewsMenuProps,
|
|
14
14
|
} from './ContentToolbar.types';
|
|
15
15
|
|
|
16
|
-
/** Default (pt-BR) strings; override any via the `labels` prop. */
|
|
17
|
-
const DEFAULT_LABELS: SavedViewsLabels = {
|
|
18
|
-
trigger: 'Visões',
|
|
19
|
-
mainView: 'Visão principal',
|
|
20
|
-
pinned: 'Fixadas',
|
|
21
|
-
recent: 'Recentes',
|
|
22
|
-
empty: 'Nenhuma visão salva ainda.',
|
|
23
|
-
saveCurrent: '+ Salvar visão atual',
|
|
24
|
-
manageAll: 'Gerenciar visões',
|
|
25
|
-
apply: 'Aplicar',
|
|
26
|
-
setDefault: 'Definir como padrão',
|
|
27
|
-
pin: 'Fixar na barra lateral',
|
|
28
|
-
unpin: 'Desafixar',
|
|
29
|
-
share: 'Compartilhar com a equipe',
|
|
30
|
-
unshare: 'Tornar privada',
|
|
31
|
-
edit: 'Editar',
|
|
32
|
-
remove: 'Excluir',
|
|
33
|
-
defaultTag: 'Padrão',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
16
|
/** How many non-pinned views show under "Recentes". */
|
|
37
17
|
const RECENT_LIMIT = 5;
|
|
38
18
|
|
|
@@ -280,7 +260,7 @@ export function SavedViewsMenu<V extends SavedViewLike = SavedViewLike>({
|
|
|
280
260
|
action();
|
|
281
261
|
close();
|
|
282
262
|
};
|
|
283
|
-
const labels =
|
|
263
|
+
const labels = labelOverrides;
|
|
284
264
|
const pinned = views.filter((view) => view.pinned);
|
|
285
265
|
const recent = views.filter((view) => !view.pinned).slice(0, RECENT_LIMIT);
|
|
286
266
|
|
|
@@ -328,7 +328,7 @@ const TableFilterRangeField = ({
|
|
|
328
328
|
step,
|
|
329
329
|
minPlaceholder = 'min',
|
|
330
330
|
maxPlaceholder = 'max',
|
|
331
|
-
invalidLabel
|
|
331
|
+
invalidLabel,
|
|
332
332
|
testId,
|
|
333
333
|
}: TableFilterRangeFieldProps): React.JSX.Element => {
|
|
334
334
|
const invalid = isRangeInverted(value);
|
package/src/copy.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words the standalone components render, as REQUIRED host config
|
|
3
|
+
* (FUT-760).
|
|
4
|
+
*
|
|
5
|
+
* `@12-apps/ui` is the package EVERY host renders, so copy compiled in here
|
|
6
|
+
* reached every adopter of every other package too — the same leak as anywhere
|
|
7
|
+
* else, at the widest possible blast radius. Several of these components made
|
|
8
|
+
* it worse by taking the words as OPTIONAL props with pt-BR defaults, which
|
|
9
|
+
* reads as configurable right up until nobody configures it.
|
|
10
|
+
*
|
|
11
|
+
* The DataViews family has its own object (`DataViewsCopy`) threaded through a
|
|
12
|
+
* context, because nineteen files are one mounted surface. These six are
|
|
13
|
+
* mounted independently, so each takes what it renders.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface SectionOnboardingCopy {
|
|
17
|
+
/** The button that begins the guided section. */
|
|
18
|
+
start: string;
|
|
19
|
+
/** The reveal toggle in the configured state, and its collapsed twin. */
|
|
20
|
+
edit: string;
|
|
21
|
+
collapse: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConfirmActionCopy {
|
|
25
|
+
/** The dismiss button every confirmation shares. */
|
|
26
|
+
cancel: string;
|
|
27
|
+
/** Shown when the action fails and the caller supplied no reason. */
|
|
28
|
+
defaultError: string;
|
|
29
|
+
/**
|
|
30
|
+
* The fallback body when the caller names no entity.
|
|
31
|
+
*
|
|
32
|
+
* A caller that DOES name one gets the host's own sentence — this package
|
|
33
|
+
* never guesses a noun for someone else's rows.
|
|
34
|
+
*/
|
|
35
|
+
irreversible: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CategorySelectCopy {
|
|
39
|
+
/**
|
|
40
|
+
* What categories are FOR, in the host's own terms.
|
|
41
|
+
*
|
|
42
|
+
* This used to read "Categorias organizam o cardápio e os filtros da loja" —
|
|
43
|
+
* a restaurant's menu, hard-coded into a generic picker. A host selling
|
|
44
|
+
* insurance policies got told about its cardápio.
|
|
45
|
+
*/
|
|
46
|
+
purpose: string;
|
|
47
|
+
clearSelection: string;
|
|
48
|
+
/** The empty state, when the host has no categories at all. */
|
|
49
|
+
emptyTitle: string;
|
|
50
|
+
createCategory: string;
|
|
51
|
+
/**
|
|
52
|
+
* The search found nothing.
|
|
53
|
+
*
|
|
54
|
+
* Two of the four interpolate the term typed, so they are functions rather
|
|
55
|
+
* than strings with a placeholder to substitute: where the term sits in the
|
|
56
|
+
* sentence is the translator's call, not ours.
|
|
57
|
+
*/
|
|
58
|
+
noResults: {
|
|
59
|
+
title: (query: string) => string;
|
|
60
|
+
hint: string;
|
|
61
|
+
clearSearch: string;
|
|
62
|
+
create: (query: string) => string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The panel's two footers.
|
|
66
|
+
*
|
|
67
|
+
* `selectedCount` is a function because Portuguese agrees the participle with
|
|
68
|
+
* the count and English does not — the shape has to let a translator decide
|
|
69
|
+
* the whole sentence, not fill a slot in ours.
|
|
70
|
+
*/
|
|
71
|
+
/** The search box above the list, and the chips beneath it. */
|
|
72
|
+
search: {
|
|
73
|
+
/** Two placeholders: a single-select picker offers no subcategories. */
|
|
74
|
+
placeholderSingle: string;
|
|
75
|
+
placeholderMulti: string;
|
|
76
|
+
clear: string;
|
|
77
|
+
removeChip(label: string): string;
|
|
78
|
+
/** The tray above the list, counting what is already picked. */
|
|
79
|
+
pinnedLabel(count: number): string;
|
|
80
|
+
};
|
|
81
|
+
footer: {
|
|
82
|
+
selectedCount(count: number): string;
|
|
83
|
+
clear: string;
|
|
84
|
+
/** The primary button, which reads `close` while the draft is unchanged. */
|
|
85
|
+
apply: string;
|
|
86
|
+
close: string;
|
|
87
|
+
/** The single-select footer: what to do, and the way out. */
|
|
88
|
+
singleHint: string;
|
|
89
|
+
cancel: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CepFieldCopy {
|
|
94
|
+
loading: string;
|
|
95
|
+
found: string;
|
|
96
|
+
/** Keyed to match `CepLookupStatus` exactly, so the lookup can index it. */
|
|
97
|
+
notfound: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface TableFilterCopy {
|
|
101
|
+
/** A numeric range whose maximum is below its minimum. */
|
|
102
|
+
invalidRange: string;
|
|
103
|
+
}
|
package/src/pt-BR.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { DataViewsCopy } from "./components/data-display/DataViews/data-views-copy";
|
|
2
|
+
import type { SavedViewsLabels } from "./components/layout/ContentToolbar/ContentToolbar.types";
|
|
3
|
+
import type {
|
|
4
|
+
CategorySelectCopy,
|
|
5
|
+
CepFieldCopy,
|
|
6
|
+
ConfirmActionCopy,
|
|
7
|
+
SectionOnboardingCopy,
|
|
8
|
+
TableFilterCopy,
|
|
9
|
+
} from "./copy";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The pt-BR packs — NAMED constants a host passes by hand, never defaults.
|
|
13
|
+
*
|
|
14
|
+
* The filename is what exempts this file from the copy-portability gate:
|
|
15
|
+
* Portuguese may ship, it may not be silent. Every sentence is VERBATIM what
|
|
16
|
+
* the components used to render, so a host adopting these sees no change on
|
|
17
|
+
* screen — what changes is that the words are chosen in a diff.
|
|
18
|
+
*/
|
|
19
|
+
export const PT_BR_SECTION_ONBOARDING_COPY: SectionOnboardingCopy = {
|
|
20
|
+
edit: "Editar",
|
|
21
|
+
collapse: "Ocultar",
|
|
22
|
+
start: "Começar",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PT_BR_CONFIRM_ACTION_COPY: ConfirmActionCopy = {
|
|
26
|
+
cancel: "Cancelar",
|
|
27
|
+
defaultError: "Não foi possível concluir a ação. Tente novamente.",
|
|
28
|
+
irreversible: "Esta ação não pode ser desfeita.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const PT_BR_CATEGORY_SELECT_COPY: CategorySelectCopy = {
|
|
32
|
+
purpose: "Categorias organizam o cardápio e os filtros da loja.",
|
|
33
|
+
clearSelection: "Limpar seleção",
|
|
34
|
+
emptyTitle: "Nenhuma categoria ainda",
|
|
35
|
+
createCategory: "Criar categoria",
|
|
36
|
+
noResults: {
|
|
37
|
+
title: (query) => `Nada encontrado para “${query}”`,
|
|
38
|
+
hint: "Tente outro termo ou verifique a grafia.",
|
|
39
|
+
clearSearch: "Limpar busca",
|
|
40
|
+
create: (query) => `Criar “${query}”`,
|
|
41
|
+
},
|
|
42
|
+
search: {
|
|
43
|
+
placeholderSingle: "Buscar categoria…",
|
|
44
|
+
placeholderMulti: "Buscar categoria ou subcategoria",
|
|
45
|
+
clear: "Limpar busca",
|
|
46
|
+
removeChip: (label) => `Remover ${label}`,
|
|
47
|
+
pinnedLabel: (count) => `Selecionadas · ${count}`,
|
|
48
|
+
},
|
|
49
|
+
footer: {
|
|
50
|
+
selectedCount: (count) => `${count} selecionada${count === 1 ? "" : "s"}`,
|
|
51
|
+
clear: "Limpar",
|
|
52
|
+
apply: "Aplicar",
|
|
53
|
+
close: "Fechar",
|
|
54
|
+
singleHint: "Escolha uma categoria",
|
|
55
|
+
cancel: "Cancelar",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const PT_BR_CEP_FIELD_COPY: CepFieldCopy = {
|
|
60
|
+
loading: "Buscando endereço…",
|
|
61
|
+
found: "Endereço preenchido pelo CEP.",
|
|
62
|
+
notfound: "CEP não encontrado — preencha o endereço manualmente.",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The saved-views dropdown's labels.
|
|
67
|
+
*
|
|
68
|
+
* Typed as the package's OWN `SavedViewsLabels` rather than a parallel shape:
|
|
69
|
+
* that interface already named every string, including the seven with no
|
|
70
|
+
* diacritics the copy gate could not see (`Aplicar`, `Fixar na barra lateral`,
|
|
71
|
+
* `Editar`…). The prop was `Partial<SavedViewsLabels>` with a pt-BR default —
|
|
72
|
+
* configurable in the type, inherited in practice.
|
|
73
|
+
*/
|
|
74
|
+
export const PT_BR_SAVED_VIEWS_LABELS: SavedViewsLabels = {
|
|
75
|
+
trigger: "Visões",
|
|
76
|
+
mainView: "Visão principal",
|
|
77
|
+
pinned: "Fixadas",
|
|
78
|
+
recent: "Recentes",
|
|
79
|
+
empty: "Nenhuma visão salva ainda.",
|
|
80
|
+
saveCurrent: "+ Salvar visão atual",
|
|
81
|
+
manageAll: "Gerenciar visões",
|
|
82
|
+
apply: "Aplicar",
|
|
83
|
+
setDefault: "Definir como padrão",
|
|
84
|
+
pin: "Fixar na barra lateral",
|
|
85
|
+
unpin: "Desafixar",
|
|
86
|
+
share: "Compartilhar com a equipe",
|
|
87
|
+
unshare: "Tornar privada",
|
|
88
|
+
edit: "Editar",
|
|
89
|
+
remove: "Excluir",
|
|
90
|
+
defaultTag: "Padrão",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const PT_BR_TABLE_FILTER_COPY: TableFilterCopy = {
|
|
94
|
+
invalidRange: "O máximo deve ser maior ou igual ao mínimo.",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const PT_BR_DATA_VIEWS_COPY: DataViewsCopy = {
|
|
98
|
+
savedViewsMenu: PT_BR_SAVED_VIEWS_LABELS,
|
|
99
|
+
confirmAction: PT_BR_CONFIRM_ACTION_COPY,
|
|
100
|
+
categorySelect: PT_BR_CATEGORY_SELECT_COPY,
|
|
101
|
+
board: {
|
|
102
|
+
countOnPage: (label, count) => `${label}: ${count} nesta página`,
|
|
103
|
+
onThisPage: (count) => `${count} nesta página`,
|
|
104
|
+
pageSum: (sum) => `Soma desta página: ${sum}`,
|
|
105
|
+
pageScopeNote:
|
|
106
|
+
"As contagens e somas abaixo referem-se apenas aos itens desta página.",
|
|
107
|
+
},
|
|
108
|
+
deleteView: {
|
|
109
|
+
title: "Excluir visão",
|
|
110
|
+
sharedWarning: "Esta visão está compartilhada — a equipe perderá o acesso a ela.",
|
|
111
|
+
rowsUnaffected: (entityLabel) =>
|
|
112
|
+
`${entityLabel} não são afetados; apenas o recorte salvo é removido.`,
|
|
113
|
+
confirm: "Excluir visão",
|
|
114
|
+
},
|
|
115
|
+
manageViews: {
|
|
116
|
+
title: "Gerenciar visões",
|
|
117
|
+
empty: "Nenhuma visão salva.",
|
|
118
|
+
defaultTag: "Padrão",
|
|
119
|
+
otherUserTag: "De outro usuário",
|
|
120
|
+
deleteTitle: "Excluir a visão salva?",
|
|
121
|
+
deleteBody: "Os filtros e colunas guardados nela são perdidos. Não é possível restaurar.",
|
|
122
|
+
deleteConfirm: "Excluir",
|
|
123
|
+
},
|
|
124
|
+
saveView: {
|
|
125
|
+
sharedDescription: "Qualquer pessoa da loja poderá abrir e usar esta visão.",
|
|
126
|
+
setDefaultTitle: "Definir como padrão",
|
|
127
|
+
setDefaultDescription: "Esta tela abre nesta visão em vez da Visão principal.",
|
|
128
|
+
previewHeading: "O que esta visão guarda",
|
|
129
|
+
previewUnchanged: "Nada foi alterado — esta visão ficará igual à Visão principal.",
|
|
130
|
+
sortRowLabel: "Ordenação",
|
|
131
|
+
saveFailed: "Não foi possível salvar a visão.",
|
|
132
|
+
titleEditing: "Editar visão",
|
|
133
|
+
titleCreating: "Salvar visão",
|
|
134
|
+
descriptionLabel: "Descrição — opcional",
|
|
135
|
+
descriptionPlaceholder: "Para que serve esta visão",
|
|
136
|
+
submitEditing: "Salvar alterações",
|
|
137
|
+
submitCreating: "Salvar visão",
|
|
138
|
+
nameLabel: "Nome",
|
|
139
|
+
cancel: "Cancelar",
|
|
140
|
+
pinnedTitle: "Fixar na barra lateral",
|
|
141
|
+
pinnedDescription: "Aparece como atalho no menu, abaixo desta tela.",
|
|
142
|
+
sharedTitle: "Compartilhar com a equipe",
|
|
143
|
+
namePlaceholder: "Ex.: Recusados no PIX desta semana",
|
|
144
|
+
previewFilters: "Filtros",
|
|
145
|
+
previewHiddenColumns: "Colunas ocultas",
|
|
146
|
+
},
|
|
147
|
+
columns: {
|
|
148
|
+
visibleCount: (visible, total) => `${visible} de ${total} visíveis`,
|
|
149
|
+
reset: "Padrão",
|
|
150
|
+
showAll: "Mostrar todas",
|
|
151
|
+
},
|
|
152
|
+
display: {
|
|
153
|
+
sortTab: "Ordenar",
|
|
154
|
+
columnsTab: "Colunas",
|
|
155
|
+
panelTab: "Exibição",
|
|
156
|
+
direction: "Direção",
|
|
157
|
+
layoutHints: {
|
|
158
|
+
table: "Colunas comparáveis, boa para varrer números.",
|
|
159
|
+
list: "Uma linha por item, com os dados principais.",
|
|
160
|
+
cards: "Blocos maiores, bom para poucos itens.",
|
|
161
|
+
board: "Colunas por etapa, para acompanhar o que está onde.",
|
|
162
|
+
},
|
|
163
|
+
densityHeadings: {
|
|
164
|
+
table: "Altura das linhas",
|
|
165
|
+
list: "Altura das linhas",
|
|
166
|
+
cards: "Cards por linha",
|
|
167
|
+
board: "Largura das colunas",
|
|
168
|
+
},
|
|
169
|
+
densityLabels: {
|
|
170
|
+
table: { compact: "Baixa", cozy: "Média", comfortable: "Alta" },
|
|
171
|
+
list: { compact: "Baixa", cozy: "Média", comfortable: "Alta" },
|
|
172
|
+
cards: { compact: "Muitos", cozy: "Médio", comfortable: "Poucos" },
|
|
173
|
+
board: { compact: "Estreita", cozy: "Média", comfortable: "Larga" },
|
|
174
|
+
},
|
|
175
|
+
boardUnavailable: "Esta tela não declara etapas, então não oferece quadro.",
|
|
176
|
+
densityUnavailableNarrow:
|
|
177
|
+
"Um card por linha nesta largura — a densidade volta em telas maiores.",
|
|
178
|
+
trigger: "Exibir",
|
|
179
|
+
},
|
|
180
|
+
export: {
|
|
181
|
+
formats: {
|
|
182
|
+
json: { label: "JSON (.json)", hint: "Para integrações" },
|
|
183
|
+
},
|
|
184
|
+
visibleColumns: (columnCount) => `${columnCount} colunas visíveis, na ordem atual`,
|
|
185
|
+
trigger: "Exportar",
|
|
186
|
+
},
|
|
187
|
+
grid: {
|
|
188
|
+
rowActions: "Ações",
|
|
189
|
+
bulkActions: (selectedCount) => `Ações (${selectedCount}) ▾`,
|
|
190
|
+
},
|
|
191
|
+
filters: {
|
|
192
|
+
moreFilters: (count) => `Mais filtros: ${count} sem espaço na barra`,
|
|
193
|
+
moreFiltersApplied: (count, appliedCount) =>
|
|
194
|
+
`Mais filtros: ${count} sem espaço na barra, ${appliedCount} aplicado(s)`,
|
|
195
|
+
overflowNote: "sem espaço na barra",
|
|
196
|
+
rangeEnd: "Até",
|
|
197
|
+
rangeInvalid: "O início precisa ser menor que o fim.",
|
|
198
|
+
rangePresets: {
|
|
199
|
+
hoje: "Hoje",
|
|
200
|
+
ontem: "Ontem",
|
|
201
|
+
semana: "Esta semana",
|
|
202
|
+
mes: "Este mês",
|
|
203
|
+
ano: "Este ano",
|
|
204
|
+
},
|
|
205
|
+
scopesLabel: "Situação",
|
|
206
|
+
panelTitle: "Filtros",
|
|
207
|
+
clear: "Limpar",
|
|
208
|
+
clearAll: "Limpar filtros",
|
|
209
|
+
clearRange: (label) => `Limpar ${label}`,
|
|
210
|
+
allOption: "Todas",
|
|
211
|
+
optionSearchPlaceholder: "Buscar…",
|
|
212
|
+
optionsEmpty: "Nenhum resultado",
|
|
213
|
+
},
|
|
214
|
+
selection: {
|
|
215
|
+
selectAllOnPage: "Selecionar todos nesta página",
|
|
216
|
+
clearSelection: "Limpar seleção",
|
|
217
|
+
selectAll: "Selecionar tudo",
|
|
218
|
+
onThisPage: (count) => `${count} nesta página`,
|
|
219
|
+
selectRow: "Selecionar",
|
|
220
|
+
},
|
|
221
|
+
nav: {
|
|
222
|
+
mainView: "Visão principal",
|
|
223
|
+
defaultTag: "Padrão",
|
|
224
|
+
viewOptions: (viewName) => `Opções de ${viewName}`,
|
|
225
|
+
emptyHint: "Nenhuma visão salva. Ajuste filtros, colunas ou ordenação e salve abaixo.",
|
|
226
|
+
setDefault: "Definir como padrão",
|
|
227
|
+
deleteView: "Excluir visão",
|
|
228
|
+
unsavedChanges: "Alterações não salvas",
|
|
229
|
+
label: "Visão",
|
|
230
|
+
update: "Atualizar visão",
|
|
231
|
+
save: "Salvar visão",
|
|
232
|
+
updateFailed: "Não foi possível atualizar a visão",
|
|
233
|
+
saveFailed: "Não foi possível salvar a alteração da visão.",
|
|
234
|
+
editView: "Renomear e editar",
|
|
235
|
+
pinToSidebar: "Fixar na barra lateral",
|
|
236
|
+
shareWithTeam: "Compartilhar com a equipe",
|
|
237
|
+
},
|
|
238
|
+
search: {
|
|
239
|
+
placeholder: "Buscar…",
|
|
240
|
+
allColumnsLabel: "Buscar em todas as colunas",
|
|
241
|
+
keywordPlaceholder: "Pressione Enter para filtrar",
|
|
242
|
+
open: "Buscar",
|
|
243
|
+
close: "Fechar busca",
|
|
244
|
+
clear: "Limpar busca",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialise rows and hand the browser a download.
|
|
3
|
+
*
|
|
4
|
+
* The companion of the grid's Export control, which emits a REQUEST and leaves
|
|
5
|
+
* the producing to the host — this is the producing half, for the common case
|
|
6
|
+
* where the host already has the rows and only needs a file out of them.
|
|
7
|
+
*
|
|
8
|
+
* Dependency-free on purpose: "Excel" is emitted as CSV, which Excel opens
|
|
9
|
+
* natively. A spreadsheet library would add ~700KB to a bundle for column
|
|
10
|
+
* widths nobody asked for, and the one thing a real `.xlsx` buys — types that
|
|
11
|
+
* survive the round trip — is not something a CSV export of an admin list is
|
|
12
|
+
* being asked for.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type ExportFormat = 'csv' | 'json';
|
|
16
|
+
|
|
17
|
+
/** One column of the exported file: its header, and how to read a row. */
|
|
18
|
+
export interface ExportColumn<T> {
|
|
19
|
+
/** Column header, used as the CSV column and the JSON key. */
|
|
20
|
+
header: string;
|
|
21
|
+
/** Extract the cell value for a row. */
|
|
22
|
+
value: (row: T) => string | number | boolean | null | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeCsvCell(value: string): string {
|
|
26
|
+
// Quote when the cell contains a comma, a quote or a newline; double any
|
|
27
|
+
// embedded quote. Without this a name with a comma silently shifts every
|
|
28
|
+
// column after it by one, which reads as corrupt data rather than as a bug.
|
|
29
|
+
if (/[",\n\r]/.test(value)) {
|
|
30
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toCsv<T>(rows: readonly T[], columns: readonly ExportColumn<T>[]): string {
|
|
36
|
+
const head = columns.map((column) => escapeCsvCell(column.header)).join(',');
|
|
37
|
+
const body = rows
|
|
38
|
+
.map((row) =>
|
|
39
|
+
columns
|
|
40
|
+
.map((column) => {
|
|
41
|
+
const cell = column.value(row);
|
|
42
|
+
return escapeCsvCell(cell == null ? '' : String(cell));
|
|
43
|
+
})
|
|
44
|
+
.join(','),
|
|
45
|
+
)
|
|
46
|
+
.join('\n');
|
|
47
|
+
return `${head}\n${body}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toJson<T>(rows: readonly T[], columns: readonly ExportColumn<T>[]): string {
|
|
51
|
+
const records = rows.map((row) => {
|
|
52
|
+
const record: Record<string, string | number | boolean | null> = {};
|
|
53
|
+
for (const column of columns) {
|
|
54
|
+
const cell = column.value(row);
|
|
55
|
+
record[column.header] = cell == null ? null : (cell as string | number | boolean);
|
|
56
|
+
}
|
|
57
|
+
return record;
|
|
58
|
+
});
|
|
59
|
+
return JSON.stringify(records, null, 2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function triggerDownload(content: string, filename: string, mimeType: string): void {
|
|
63
|
+
// A no-op without a document, so a module that imports this can still be
|
|
64
|
+
// loaded by a test runner or a server render without guarding the import.
|
|
65
|
+
if (typeof window === 'undefined') return;
|
|
66
|
+
const blob = new Blob([content], { type: mimeType });
|
|
67
|
+
const url = URL.createObjectURL(blob);
|
|
68
|
+
const anchor = document.createElement('a');
|
|
69
|
+
anchor.href = url;
|
|
70
|
+
anchor.download = filename;
|
|
71
|
+
document.body.appendChild(anchor);
|
|
72
|
+
anchor.click();
|
|
73
|
+
anchor.remove();
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Serialise `rows` using `columns` and download them as `<baseName>.<ext>`.
|
|
79
|
+
*
|
|
80
|
+
* @param format - `csv` (also what the Excel option produces) or `json`.
|
|
81
|
+
* @param baseName - File name without extension; the consumer's word for the
|
|
82
|
+
* collection, so it is never derived from a type name here.
|
|
83
|
+
*/
|
|
84
|
+
export function exportRows<T>(
|
|
85
|
+
format: ExportFormat,
|
|
86
|
+
rows: readonly T[],
|
|
87
|
+
columns: readonly ExportColumn<T>[],
|
|
88
|
+
baseName: string,
|
|
89
|
+
): void {
|
|
90
|
+
if (format === 'json') {
|
|
91
|
+
triggerDownload(toJson(rows, columns), `${baseName}.json`, 'application/json');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
triggerDownload(toCsv(rows, columns), `${baseName}.csv`, 'text/csv;charset=utf-8');
|
|
95
|
+
}
|
package/src/utils/index.ts
CHANGED