@cosmicdrift/kumiko-renderer 0.399.0 → 0.401.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/dist/app/entity-edit-prefill.js +7 -1
- package/dist/app/form-schema.js +42 -17
- package/dist/app/projection-detail-body.js +7 -0
- package/dist/app/projection-detail-header-model.d.ts +1 -1
- package/dist/app/projection-detail-header-model.js +2 -2
- package/dist/app/projection-detail-header-region.d.ts +1 -0
- package/dist/app/projection-detail-header-region.js +1 -1
- package/dist/app/projection-detail-header.js +4 -2
- package/dist/app/projection-detail-navigation.d.ts +1 -0
- package/dist/app/projection-detail-navigation.js +9 -1
- package/dist/app/use-related-list-tab-counts.d.ts +5 -0
- package/dist/app/use-related-list-tab-counts.js +68 -0
- package/dist/components/embedded-list-field.js +69 -91
- package/dist/components/embedded-list-paste.d.ts +2 -0
- package/dist/components/embedded-list-paste.js +73 -0
- package/dist/components/related-list-section.js +4 -8
- package/dist/components/render-edit-helpers.js +2 -2
- package/dist/components/render-field-choice-inputs.js +1 -1
- package/dist/components/use-reference-column-lookups.d.ts +6 -0
- package/dist/components/use-reference-column-lookups.js +36 -0
- package/dist/components/use-render-list-columns.d.ts +2 -3
- package/dist/components/use-render-list-columns.js +4 -31
- package/dist/embedded-list-types.d.ts +117 -0
- package/dist/embedded-list-types.js +1 -0
- package/dist/headless/form/form-controller.js +9 -1
- package/dist/headless/index.d.ts +1 -1
- package/dist/headless/index.js +1 -1
- package/dist/headless/view-model/edit.js +40 -50
- package/dist/headless/view-model/embedded-list-cells.d.ts +12 -0
- package/dist/headless/view-model/embedded-list-cells.js +49 -0
- package/dist/headless/view-model/embedded-list.d.ts +1 -1
- package/dist/headless/view-model/embedded-list.js +1 -1
- package/dist/headless/view-model/index.d.ts +1 -1
- package/dist/headless/view-model/index.js +1 -1
- package/dist/headless/view-model/list.js +2 -3
- package/dist/headless/view-model/types.d.ts +23 -6
- package/dist/hooks/related-list-parent-payload.d.ts +2 -0
- package/dist/hooks/related-list-parent-payload.js +7 -0
- package/dist/i18n-defaults.js +3 -0
- package/dist/primitives.d.ts +4 -88
- package/dist/web/app/dashboard-body.js +3 -3
- package/dist/web/app/dashboard-panels.d.ts +2 -1
- package/dist/web/app/dashboard-panels.js +11 -3
- package/dist/web/app/dashboard-reference-list-table.d.ts +17 -0
- package/dist/web/app/dashboard-reference-list-table.js +47 -0
- package/dist/web/lib/file-upload-form-data.d.ts +2 -0
- package/dist/web/lib/file-upload-form-data.js +2 -0
- package/dist/web/lib/upload-file-ref.d.ts +1 -1
- package/dist/web/primitives/data-table-cell.d.ts +2 -0
- package/dist/web/primitives/data-table-cell.js +1 -1
- package/dist/web/primitives/embedded-file-cell.d.ts +11 -0
- package/dist/web/primitives/embedded-file-cell.js +50 -0
- package/dist/web/primitives/embedded-list-cards.d.ts +38 -0
- package/dist/web/primitives/embedded-list-cards.js +71 -0
- package/dist/web/primitives/embedded-list-cell.d.ts +41 -0
- package/dist/web/primitives/embedded-list-cell.js +132 -0
- package/dist/web/primitives/embedded-list-input.d.ts +1 -1
- package/dist/web/primitives/embedded-list-input.js +29 -142
- package/dist/web/primitives/file-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +2 -1
- package/dist/web/primitives/input-choice-kinds.js +1 -1
- package/dist/web/primitives/input-optional-props.d.ts +2 -1
- package/dist/web/primitives/input-optional-props.js +2 -1
- package/dist/web/primitives/locale-number.d.ts +3 -0
- package/dist/web/primitives/locale-number.js +8 -0
- package/dist/web/primitives/number-input.d.ts +2 -1
- package/dist/web/primitives/number-input.js +4 -11
- package/dist/web/primitives/segmented-select.d.ts +6 -1
- package/dist/web/primitives/segmented-select.js +8 -4
- package/dist/web/widgets/dashboard-list.d.ts +2 -0
- package/dist/web/widgets/dashboard-list.js +2 -0
- package/dist/web/widgets/status-badge.d.ts +1 -0
- package/dist/web/widgets/status-badge.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListEmptyStateVariant, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { EditFieldSpec, EditSectionSpec, EmbeddedDerivedCellDef, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListEmptyStateVariant, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
|
|
3
3
|
/** Where a cell's text links to. `href` comes from NavApi.hrefFor (new-tab and
|
|
4
4
|
* copy-link work); `onPress` is the in-app navigation for a plain click. */
|
|
@@ -79,15 +79,26 @@ export type ListViewModel = {
|
|
|
79
79
|
export type EmbeddedListCellViewModel = {
|
|
80
80
|
readonly field: string;
|
|
81
81
|
readonly label: string;
|
|
82
|
-
readonly type: "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
|
|
82
|
+
readonly type: "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp" | "images" | "files";
|
|
83
83
|
readonly required: boolean;
|
|
84
|
+
/** Per-row state: hidden / required for a row where the condition holds. */
|
|
85
|
+
readonly visibleWhen?: Exclude<FieldCondition, boolean>;
|
|
86
|
+
readonly requiredWhen?: Exclude<FieldCondition, boolean>;
|
|
84
87
|
/** Only for `type: "select"`. */
|
|
85
88
|
readonly options?: readonly string[];
|
|
89
|
+
/** Only for `type: "select"`; tone of each option's badge or selected segment. */
|
|
90
|
+
readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
|
|
91
|
+
/** Only for `type: "images" | "files"`. */
|
|
92
|
+
readonly capture?: "user" | "environment";
|
|
93
|
+
readonly maxCount?: number;
|
|
94
|
+
readonly accept?: readonly string[];
|
|
86
95
|
readonly optionLabels?: Readonly<Record<string, string>>;
|
|
87
96
|
/** Only for `type: "select"`; `"radio"` renders segmented buttons instead of a dropdown. */
|
|
88
97
|
readonly display?: "radio" | "dropdown";
|
|
89
98
|
/** Cell is shown but not editable (`EditFieldSpec.embeddedList.readOnlyCells`). */
|
|
90
99
|
readonly readOnly?: boolean;
|
|
100
|
+
/** Not shown on phone cards (`EditFieldSpec.embeddedList.cardHiddenCells`). */
|
|
101
|
+
readonly hiddenInCard?: boolean;
|
|
91
102
|
/** Only for `type: "reference"`. */
|
|
92
103
|
readonly refEntity?: string;
|
|
93
104
|
readonly refFeature?: string;
|
|
@@ -140,6 +151,8 @@ export type EditFieldViewModel = {
|
|
|
140
151
|
* `type: "select"` when SelectFieldDef.display is "radio" or "dropdown" —
|
|
141
152
|
* the renderer then skips its own radio-vs-dropdown heuristic. */
|
|
142
153
|
readonly display?: "checkboxes" | "dropdown" | "radio";
|
|
154
|
+
/** `type: "select"` only: tone per option, shown on the selected radio segment. */
|
|
155
|
+
readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
|
|
143
156
|
/** Set for `type: "multiSelect"` when MultiSelectFieldDef.columns is
|
|
144
157
|
* defined — column count for the checkbox grid at the widest breakpoint.
|
|
145
158
|
* Ignored unless `display` is "checkboxes". */
|
|
@@ -249,10 +262,7 @@ export type EditFieldViewModel = {
|
|
|
249
262
|
readonly embeddedListMaxItems?: number;
|
|
250
263
|
/** Sub-field name → how to compute its cell from other sub-fields of the
|
|
251
264
|
* same row. From EmbeddedFieldDef.derived. */
|
|
252
|
-
readonly embeddedListDerived?: Readonly<Record<string,
|
|
253
|
-
readonly op: "multiply" | "sum" | "subtract" | "percent";
|
|
254
|
-
readonly from: readonly string[];
|
|
255
|
-
}>>;
|
|
265
|
+
readonly embeddedListDerived?: Readonly<Record<string, EmbeddedDerivedCellDef>>;
|
|
256
266
|
/** Footer totals: a sub-field name, or a labelled sum of several. From EmbeddedFieldDef.totals. */
|
|
257
267
|
readonly embeddedListTotals?: readonly (string | {
|
|
258
268
|
readonly label: string;
|
|
@@ -261,6 +271,13 @@ export type EditFieldViewModel = {
|
|
|
261
271
|
/** No add, remove, duplicate or move; cells stay editable. From
|
|
262
272
|
* EditFieldSpec.embeddedList.fixedRows. */
|
|
263
273
|
readonly embeddedListFixedRows?: boolean;
|
|
274
|
+
/** Where photos of a row's file cells are uploaded: the entity and the top-level list field;
|
|
275
|
+
* the cell name travels as `subField`. Set when the list has a file cell. */
|
|
276
|
+
readonly embeddedListUploadTarget?: {
|
|
277
|
+
readonly entityType: string;
|
|
278
|
+
readonly fieldName: string;
|
|
279
|
+
readonly entityId?: string;
|
|
280
|
+
};
|
|
264
281
|
/** Currency for the whole embedded list's money cells and totals row —
|
|
265
282
|
* currency lives on the head aggregate (EntityDefinition.defaultCurrency),
|
|
266
283
|
* not per row. Falls back to "EUR" when the entity has no defaultCurrency. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `parentFilter` sends the parent id as a server-side `filter` clause: the generic
|
|
2
|
+
// `<entity>:list` query understands `filter`, not an arbitrary `parentParam` key.
|
|
3
|
+
export function buildRelatedListParentPayload(section, parentId) {
|
|
4
|
+
return section.parentFilter !== undefined
|
|
5
|
+
? { filter: { field: section.parentFilter.field, op: "eq", value: parentId } }
|
|
6
|
+
: { [section.parentParam ?? "id"]: parentId };
|
|
7
|
+
}
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -50,6 +50,9 @@ export const kumikoDefaultTranslations = {
|
|
|
50
50
|
"kumiko.field.writeOnly.willRemove": "Removed on save",
|
|
51
51
|
"kumiko.field.embedded-list.actions": "Actions",
|
|
52
52
|
"kumiko.field.embedded-list.cell-label": "{column}, row {row}",
|
|
53
|
+
"kumiko.field.embedded-list.limit-range": "Limit {min} to {max}",
|
|
54
|
+
"kumiko.field.embedded-list.limit-min": "At least {min}",
|
|
55
|
+
"kumiko.field.embedded-list.limit-max": "At most {max}",
|
|
53
56
|
"kumiko.field.file-list.add": "Add file",
|
|
54
57
|
"kumiko.field.file-list.move-up": "Move file up",
|
|
55
58
|
"kumiko.field.file-list.move-down": "Move file down",
|
package/dist/primitives.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { type ComponentType, type FormEvent, type KeyboardEvent, type ReactNode,
|
|
|
4
4
|
import type { DataTableBulk } from "./data-table-bulk-types.ts";
|
|
5
5
|
import type { DataTableDateRangeFacet, DataTableFacet } from "./data-table-facet-types.ts";
|
|
6
6
|
import type { DataTableTableBlock } from "./data-table-view-types.ts";
|
|
7
|
+
import type { EmbeddedListInputProps } from "./embedded-list-types.ts";
|
|
7
8
|
import type { FieldIssue, ListColumnViewModel, ListRowViewModel, RuntimeRenderer } from "./headless/index.ts";
|
|
8
9
|
import type { MetricBandProps, MetricProps } from "./metric-types.ts";
|
|
10
|
+
export type { EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, } from "./embedded-list-types.ts";
|
|
9
11
|
export type { RuntimeRenderer };
|
|
10
12
|
/** Escape hatch for `data-*` test hooks (e.g. an E2E selector) that don't
|
|
11
13
|
* warrant a dedicated typed prop. Web merges these onto the rendered
|
|
@@ -460,6 +462,8 @@ export type InputProps = {
|
|
|
460
462
|
* decides; the web impl keeps its short-option-set heuristic. Custom
|
|
461
463
|
* impls may ignore it — it is a request, not a contract. */
|
|
462
464
|
readonly display?: "radio" | "dropdown";
|
|
465
|
+
/** Tone per option value; the selected segment of a radio presentation takes its colors. */
|
|
466
|
+
readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
|
|
463
467
|
} | ({
|
|
464
468
|
readonly kind: "combobox";
|
|
465
469
|
readonly id: string;
|
|
@@ -797,94 +801,6 @@ export type DataTableProps = {
|
|
|
797
801
|
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
798
802
|
} & DataTableTableBlock;
|
|
799
803
|
export type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
|
|
800
|
-
/** Cell type for one column of an embedded-list field. Mirrors
|
|
801
|
-
* EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-renderer/headless`. */
|
|
802
|
-
export type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
|
|
803
|
-
/** One column of an embedded-list table (one entry per key of the source
|
|
804
|
-
* EmbeddedFieldDef's `schema`). */
|
|
805
|
-
export type EmbeddedListColumn = {
|
|
806
|
-
readonly field: string;
|
|
807
|
-
readonly label: string;
|
|
808
|
-
readonly type: EmbeddedListCellType;
|
|
809
|
-
readonly required: boolean;
|
|
810
|
-
/** Read-only, value supplied by the caller (already computed from
|
|
811
|
-
* sibling cells) — not directly user-editable. */
|
|
812
|
-
readonly derived: boolean;
|
|
813
|
-
/** Shown but not editable by the user; unlike `derived` it is not computed. */
|
|
814
|
-
readonly readOnly?: boolean;
|
|
815
|
-
/** Only for `type: "select"`. */
|
|
816
|
-
readonly options?: readonly string[];
|
|
817
|
-
readonly optionLabels?: Readonly<Record<string, string>>;
|
|
818
|
-
/** Only for `type: "select"`; `"radio"` renders segmented buttons, the
|
|
819
|
-
* default is a dropdown. */
|
|
820
|
-
readonly display?: "radio" | "dropdown";
|
|
821
|
-
/** Only for `type: "reference"` — pre-fetched by the caller, ONE query
|
|
822
|
-
* per column shared across every row (not one query per cell). */
|
|
823
|
-
readonly referenceOptions?: readonly {
|
|
824
|
-
readonly value: string;
|
|
825
|
-
readonly label: string;
|
|
826
|
-
}[];
|
|
827
|
-
readonly referenceLoading?: boolean;
|
|
828
|
-
};
|
|
829
|
-
/** One entry of an embedded-list totals row (e.g. an invoice's total
|
|
830
|
-
* amount, summed across all rows by the caller). */
|
|
831
|
-
export type EmbeddedListTotal = {
|
|
832
|
-
readonly field: string;
|
|
833
|
-
readonly label: string;
|
|
834
|
-
readonly value: number;
|
|
835
|
-
/** Set for labelled multi-column totals, which have no single column to
|
|
836
|
-
* read the money type from. */
|
|
837
|
-
readonly money?: boolean;
|
|
838
|
-
};
|
|
839
|
-
/** Row-array + totals table for a `createEmbeddedListField` (invoice-
|
|
840
|
-
* positions-style) field — structurally too different from `Input`'s
|
|
841
|
-
* value/onChange union to fit there, so it's its own primitive, same as
|
|
842
|
-
* `DataTable`. Controlled: the caller (the eventual form-field wrapper)
|
|
843
|
-
* computes rows/derived values/issue groups and wires every callback;
|
|
844
|
-
* this primitive only renders and reports interaction. */
|
|
845
|
-
export type EmbeddedListInputProps = {
|
|
846
|
-
readonly id: string;
|
|
847
|
-
readonly columns: readonly EmbeddedListColumn[];
|
|
848
|
-
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
849
|
-
readonly totals?: readonly EmbeddedListTotal[];
|
|
850
|
-
/** Currency for money cells and the totals row — one value for the whole
|
|
851
|
-
* list (currency lives on the head aggregate, not the row). Web impl
|
|
852
|
-
* falls back to "EUR" when absent. */
|
|
853
|
-
readonly currency?: string;
|
|
854
|
-
/** Whole-list read-only (e.g. a released invoice) — hides row-mutation
|
|
855
|
-
* affordances and disables every cell. */
|
|
856
|
-
readonly disabled?: boolean;
|
|
857
|
-
/** Rows are fixed: no add, remove, duplicate or move; cells stay editable. */
|
|
858
|
-
readonly fixedRows?: boolean;
|
|
859
|
-
readonly minItems?: number;
|
|
860
|
-
readonly maxItems?: number;
|
|
861
|
-
/** Shown under the whole list (e.g. under the totals row). */
|
|
862
|
-
readonly listIssues?: readonly FieldIssue[];
|
|
863
|
-
/** Keyed by row index. */
|
|
864
|
-
readonly rowIssues?: Readonly<Record<number, readonly FieldIssue[]>>;
|
|
865
|
-
/** Keyed `${rowIndex}.${field}`. */
|
|
866
|
-
readonly cellIssues?: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
867
|
-
readonly onCellChange: (rowIndex: number, field: string, value: unknown) => void;
|
|
868
|
-
readonly onAddRow: () => void;
|
|
869
|
-
readonly onRemoveRow: (rowIndex: number) => void;
|
|
870
|
-
readonly onDuplicateRow: (rowIndex: number) => void;
|
|
871
|
-
readonly onMoveRow: (fromIndex: number, toIndex: number) => void;
|
|
872
|
-
/** Tab/newline-delimited clipboard paste starting at (rowIndex,
|
|
873
|
-
* columnIndex) — parsed to a 2D string grid by the Web impl (paste
|
|
874
|
-
* events are a browser-only concept); undefined caller = no paste
|
|
875
|
-
* handling wired up. */
|
|
876
|
-
readonly onPasteCells?: (rowIndex: number, columnIndex: number, grid: readonly (readonly string[])[]) => void;
|
|
877
|
-
readonly addLabel: string;
|
|
878
|
-
/** Accessible name of the (otherwise headerless) row-actions column. */
|
|
879
|
-
readonly actionsLabel: string;
|
|
880
|
-
readonly removeLabel: string;
|
|
881
|
-
readonly duplicateLabel: string;
|
|
882
|
-
readonly moveUpLabel: string;
|
|
883
|
-
readonly moveDownLabel: string;
|
|
884
|
-
readonly emptyLabel: string;
|
|
885
|
-
readonly emptyCtaLabel: string;
|
|
886
|
-
readonly testId?: string;
|
|
887
|
-
};
|
|
888
804
|
export type { FormWidth };
|
|
889
805
|
/** One entry of a form's "on this page" navigation: `id` matches the
|
|
890
806
|
* `SectionProps.id` of the section it scrolls to. */
|
|
@@ -16,7 +16,7 @@ function panelSpanClassName(panel) {
|
|
|
16
16
|
return HALF_PANEL;
|
|
17
17
|
return WIDE_PANEL;
|
|
18
18
|
}
|
|
19
|
-
function PanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
19
|
+
function PanelBody({ panel, label, featureName, screenId, screenParams, translate, }) {
|
|
20
20
|
if (panel.kind === "stat-group") {
|
|
21
21
|
return (_jsx(StatGroupPanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }));
|
|
22
22
|
}
|
|
@@ -36,7 +36,7 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
|
36
36
|
return (_jsx(ChartPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
|
|
37
37
|
}
|
|
38
38
|
if (panel.kind === "list") {
|
|
39
|
-
return (_jsx(ListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
|
|
39
|
+
return (_jsx(ListPanelBody, { panel: panel, label: text, featureName: featureName, screenParams: screenParams, translate: translate }));
|
|
40
40
|
}
|
|
41
41
|
if (panel.kind === "feed") {
|
|
42
42
|
return (_jsx(FeedPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
|
|
@@ -51,7 +51,7 @@ function renderDashboardPanel(panel, featureName, screenId, screenParams, transl
|
|
|
51
51
|
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, recordId: screenParams.recordId, translate: translate }, panel.id));
|
|
52
52
|
}
|
|
53
53
|
const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
|
|
54
|
-
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
|
|
54
|
+
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, featureName: featureName, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
|
|
55
55
|
}
|
|
56
56
|
export function WebDashboardBody({ featureName, screen, translate, }) {
|
|
57
57
|
const t = useTranslation();
|
|
@@ -23,9 +23,10 @@ export declare function ChartPanelBody({ panel, label, screenParams, translate,
|
|
|
23
23
|
readonly screenParams: ScreenParams;
|
|
24
24
|
readonly translate: Translate;
|
|
25
25
|
}): ReactNode;
|
|
26
|
-
export declare function ListPanelBody({ panel, label, screenParams, translate, }: {
|
|
26
|
+
export declare function ListPanelBody({ panel, label, featureName, screenParams, translate, }: {
|
|
27
27
|
readonly panel: DashboardListPanel;
|
|
28
28
|
readonly label: string;
|
|
29
|
+
readonly featureName: string;
|
|
29
30
|
readonly screenParams: ScreenParams;
|
|
30
31
|
readonly translate: Translate;
|
|
31
32
|
}): ReactNode;
|
|
@@ -8,13 +8,13 @@ import { EMPTY_VALUE_PLACEHOLDER } from "../../headless/index.js";
|
|
|
8
8
|
import { extensionSectionName, lastSegment, useExtensionSectionComponent, useNav, usePrimitives, useTranslation, useUserRoles, } from "../../index.js";
|
|
9
9
|
import { initialWindowSelection, } from "../widgets/chart-window.js";
|
|
10
10
|
import { chartToneColor, SegmentBarChart, StackedAreaChart, StackedBarChart, TimeseriesChart, } from "../widgets/charts.js";
|
|
11
|
-
import { DashboardListTable } from "../widgets/dashboard-list.js";
|
|
12
11
|
import { FeedList } from "../widgets/feed-list.js";
|
|
13
12
|
import { ModeSwitch } from "../widgets/mode-switch.js";
|
|
14
13
|
import { ProgressList } from "../widgets/progress-list.js";
|
|
15
14
|
import { SectionCard } from "../widgets/section-card.js";
|
|
16
15
|
import { STAT_CARD_GRID_CLASSES, StatCard, StatStripCell, } from "../widgets/stat.js";
|
|
17
16
|
import { EmptyState, LoadingState } from "../widgets/states.js";
|
|
17
|
+
import { DashboardReferenceListTable } from "./dashboard-reference-list-table.js";
|
|
18
18
|
import { DAY_MS, HOUR_MS, PanelError, PanelShell, panelPayload, QueryPanel, useDashboardFormats, useDashboardQuery, useReportPanelLoaded, } from "./dashboard-shared.js";
|
|
19
19
|
import { isRecord, resolveDashboardText } from "./dashboard-text.js";
|
|
20
20
|
function isSparkPoint(value) {
|
|
@@ -222,7 +222,7 @@ export function ChartPanelBody({ panel, label, screenParams, translate, }) {
|
|
|
222
222
|
}), ariaLabel: label, testId: testId }));
|
|
223
223
|
} }));
|
|
224
224
|
}
|
|
225
|
-
export function ListPanelBody({ panel, label, screenParams, translate, }) {
|
|
225
|
+
export function ListPanelBody({ panel, label, featureName, screenParams, translate, }) {
|
|
226
226
|
const { formatPercent, formatNumber, formatDateTime } = useDashboardFormats();
|
|
227
227
|
const columns = panel.columns.map((c) => {
|
|
228
228
|
const normalized = normalizeListColumn(c);
|
|
@@ -233,9 +233,17 @@ export function ListPanelBody({ panel, label, screenParams, translate, }) {
|
|
|
233
233
|
...(normalized.badgeToneField !== undefined && {
|
|
234
234
|
badgeToneField: normalized.badgeToneField,
|
|
235
235
|
}),
|
|
236
|
+
...(normalized.refEntity !== undefined && {
|
|
237
|
+
reference: {
|
|
238
|
+
refEntity: normalized.refEntity,
|
|
239
|
+
...(normalized.refLabelField !== undefined && {
|
|
240
|
+
refLabelField: normalized.refLabelField,
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
236
244
|
};
|
|
237
245
|
});
|
|
238
|
-
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(
|
|
246
|
+
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(DashboardReferenceListTable, { columns: columns, rows: data.rows ?? [], featureName: featureName, formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate })) }));
|
|
239
247
|
}
|
|
240
248
|
export function FeedPanelBody({ panel, label, screenParams, translate, }) {
|
|
241
249
|
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
2
|
+
import type { Translate } from "../../headless/index.ts";
|
|
3
|
+
import { type DashboardListColumn, DashboardListTable } from "../widgets/dashboard-list.tsx";
|
|
4
|
+
export type DashboardReferenceTarget = {
|
|
5
|
+
readonly refEntity: string;
|
|
6
|
+
readonly refLabelField?: string;
|
|
7
|
+
};
|
|
8
|
+
export type DashboardListTableColumn = DashboardListColumn & {
|
|
9
|
+
readonly reference?: DashboardReferenceTarget;
|
|
10
|
+
};
|
|
11
|
+
type Props = Omit<ComponentProps<typeof DashboardListTable>, "columns"> & {
|
|
12
|
+
readonly columns: readonly DashboardListTableColumn[];
|
|
13
|
+
readonly featureName: string;
|
|
14
|
+
readonly translate: Translate;
|
|
15
|
+
};
|
|
16
|
+
export declare function DashboardReferenceListTable({ columns, rows, featureName, translate, ...tableProps }: Props): ReactNode;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useReferenceLinkResolver } from "../../app/reference-link.js";
|
|
5
|
+
import { renderReferenceLookupBridges } from "../../components/render-list-reference-bridge.js";
|
|
6
|
+
import { enrichReferenceColumns, selectReferenceColumns, } from "../../components/render-list-reference-columns.js";
|
|
7
|
+
import { useReferenceColumnLookups } from "../../components/use-reference-column-lookups.js";
|
|
8
|
+
import { DashboardListTable } from "../widgets/dashboard-list.js";
|
|
9
|
+
function toReferenceViewColumn(column, featureName) {
|
|
10
|
+
if (column.reference === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const target = parseRefTarget(column.reference.refEntity, featureName);
|
|
13
|
+
return {
|
|
14
|
+
field: column.field,
|
|
15
|
+
label: column.label,
|
|
16
|
+
type: "reference",
|
|
17
|
+
sortable: false,
|
|
18
|
+
refEntity: target.entityName,
|
|
19
|
+
refFeature: target.featureName,
|
|
20
|
+
refLabelField: column.reference.refLabelField ?? "id",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Dashboard list rows carry raw ids; reference columns resolve them to the target's label
|
|
24
|
+
// the way entity lists do.
|
|
25
|
+
export function DashboardReferenceListTable({ columns, rows, featureName, translate, ...tableProps }) {
|
|
26
|
+
const viewColumns = useMemo(() => columns.flatMap((column) => {
|
|
27
|
+
const viewColumn = toReferenceViewColumn(column, featureName);
|
|
28
|
+
return viewColumn === undefined ? [] : [viewColumn];
|
|
29
|
+
}), [columns, featureName]);
|
|
30
|
+
const referenceColumns = useMemo(() => selectReferenceColumns(viewColumns, featureName), [viewColumns, featureName]);
|
|
31
|
+
const { referenceRequests, handleLookupMap, referenceLookups } = useReferenceColumnLookups(rows, referenceColumns);
|
|
32
|
+
const resolveReferenceLink = useReferenceLinkResolver();
|
|
33
|
+
const enriched = useMemo(() => enrichReferenceColumns({
|
|
34
|
+
columns: viewColumns,
|
|
35
|
+
referenceColumns,
|
|
36
|
+
referenceLookups,
|
|
37
|
+
resolveReferenceLink,
|
|
38
|
+
featureName,
|
|
39
|
+
translate,
|
|
40
|
+
}), [viewColumns, referenceColumns, referenceLookups, resolveReferenceLink, featureName, translate]);
|
|
41
|
+
const tableColumns = columns.map((column) => {
|
|
42
|
+
const renderer = enriched.find((c) => c.field === column.field)?.renderer;
|
|
43
|
+
const { reference: _reference, ...base } = column;
|
|
44
|
+
return typeof renderer === "function" ? { ...base, renderValue: renderer } : base;
|
|
45
|
+
});
|
|
46
|
+
return (_jsxs(_Fragment, { children: [renderReferenceLookupBridges(referenceRequests, handleLookupMap), _jsx(DashboardListTable, { columns: tableColumns, rows: rows, translate: translate, ...tableProps })] }));
|
|
47
|
+
}
|
|
@@ -3,6 +3,8 @@ export type FileUploadFormDataInput = {
|
|
|
3
3
|
readonly entityType?: string | undefined;
|
|
4
4
|
readonly entityId?: string | undefined;
|
|
5
5
|
readonly fieldName?: string | undefined;
|
|
6
|
+
/** File sub-field of the embedded `fieldName`. */
|
|
7
|
+
readonly subField?: string | undefined;
|
|
6
8
|
/** Screen QN of an actionForm: the upload is pending for that form's field, not for an entity. */
|
|
7
9
|
readonly actionForm?: string | undefined;
|
|
8
10
|
};
|
|
@@ -7,6 +7,8 @@ export function buildFileUploadFormData(input) {
|
|
|
7
7
|
formData.append("entityId", input.entityId);
|
|
8
8
|
if (input.fieldName !== undefined)
|
|
9
9
|
formData.append("fieldName", input.fieldName);
|
|
10
|
+
if (input.subField !== undefined)
|
|
11
|
+
formData.append("subField", input.subField);
|
|
10
12
|
if (input.actionForm !== undefined)
|
|
11
13
|
formData.append("actionForm", input.actionForm);
|
|
12
14
|
return formData;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FileUploadFormDataInput } from "./file-upload-form-data.ts";
|
|
2
|
-
export type FileRefTarget = Pick<FileUploadFormDataInput, "entityType" | "entityId" | "fieldName" | "actionForm">;
|
|
2
|
+
export type FileRefTarget = Pick<FileUploadFormDataInput, "entityType" | "entityId" | "fieldName" | "subField" | "actionForm">;
|
|
3
3
|
/** Carries the server's own `error` message when the upload endpoint sent one. */
|
|
4
4
|
export declare class FileUploadFailedError extends Error {
|
|
5
5
|
readonly fromServer: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { CellLink, CellLinkItem, ListColumnFileDisplay, ListRowViewModel, RuntimeCellLink, RuntimeCellLinkItems } from "../../headless/index.ts";
|
|
4
|
+
import { type PrimitivesRegistry } from "../../index.ts";
|
|
4
5
|
import "../react-css-custom-properties.ts";
|
|
5
6
|
export declare function isEmptyCellValue(value: unknown): boolean;
|
|
6
7
|
export declare function isUnlabeledFalse(type: string, value: unknown, renderer: unknown): boolean;
|
|
@@ -22,6 +23,7 @@ export declare function isComponentRendererRef(renderer: unknown): {
|
|
|
22
23
|
} | undefined;
|
|
23
24
|
export declare function defaultCellRender(value: unknown, type: string, optionLabels?: Readonly<Record<string, string>>, locale?: string, columnKey?: string, grouping?: boolean): string;
|
|
24
25
|
export declare function cellTitle(value: unknown): string | undefined;
|
|
26
|
+
export declare function selectPill(value: unknown, optionLabels: Readonly<Record<string, string>> | undefined, optionTones: Readonly<Partial<Record<string, SelectOptionTone>>> | undefined, locale: string | undefined, RegisteredStatusBadge: PrimitivesRegistry["StatusBadge"]): ReactNode;
|
|
25
27
|
type DataTableCellProps = {
|
|
26
28
|
readonly value: unknown;
|
|
27
29
|
readonly row: Readonly<Record<string, unknown>>;
|
|
@@ -201,7 +201,7 @@ export function cellTitle(value) {
|
|
|
201
201
|
return String(value);
|
|
202
202
|
return undefined;
|
|
203
203
|
}
|
|
204
|
-
function selectPill(value, optionLabels, optionTones, locale, RegisteredStatusBadge) {
|
|
204
|
+
export function selectPill(value, optionLabels, optionTones, locale, RegisteredStatusBadge) {
|
|
205
205
|
const label = defaultCellRender(value, "select", optionLabels, locale);
|
|
206
206
|
const declaredTone = typeof value === "string" && optionTones && Object.hasOwn(optionTones, value)
|
|
207
207
|
? optionTones[value]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { EmbeddedListColumn, EmbeddedListInputProps } from "../../index.ts";
|
|
3
|
+
export declare function EmbeddedFileCell({ cellId, label, column, value, readOnly, uploadTarget, onChange, }: {
|
|
4
|
+
readonly cellId: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly column: EmbeddedListColumn;
|
|
7
|
+
readonly value: unknown;
|
|
8
|
+
readonly readOnly: boolean;
|
|
9
|
+
readonly uploadTarget: EmbeddedListInputProps["uploadTarget"];
|
|
10
|
+
readonly onChange: (value: unknown) => void;
|
|
11
|
+
}): ReactNode;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// File/image cell of an embedded list row: thumbs plus an add tile. The value is a fileRef id
|
|
3
|
+
// array; uploads carry the list's field name and the cell's `subField` so the server can
|
|
4
|
+
// resolve the sub-field's accept/maxSize and attach the ref to the right slot.
|
|
5
|
+
import { Loader2, Plus, X } from "lucide-react";
|
|
6
|
+
import { useRef, useState } from "react";
|
|
7
|
+
import { fileIdsOf } from "../../file-ids-of.js";
|
|
8
|
+
import { useSessionEndedSignal, useTranslation } from "../../index.js";
|
|
9
|
+
import { toAcceptAttr } from "../lib/accept-attr.js";
|
|
10
|
+
import { cn } from "../lib/cn.js";
|
|
11
|
+
import { resizeImageBeforeUpload } from "../lib/resize-image.js";
|
|
12
|
+
import { uploadErrorKey } from "../lib/upload-error-key.js";
|
|
13
|
+
import { isSessionEndedUploadError, uploadFileRef } from "../lib/upload-file-ref.js";
|
|
14
|
+
import { FileNameLink, FileThumbnail } from "./file-thumbnail.js";
|
|
15
|
+
export function EmbeddedFileCell({ cellId, label, column, value, readOnly, uploadTarget, onChange, }) {
|
|
16
|
+
const t = useTranslation();
|
|
17
|
+
const sessionEnded = useSessionEndedSignal();
|
|
18
|
+
const inputRef = useRef(null);
|
|
19
|
+
const [uploading, setUploading] = useState(false);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
const ids = fileIdsOf(value);
|
|
22
|
+
const isImage = column.type === "images";
|
|
23
|
+
const canAdd = !readOnly &&
|
|
24
|
+
uploadTarget !== undefined &&
|
|
25
|
+
(column.maxCount === undefined || ids.length < column.maxCount);
|
|
26
|
+
const acceptAttr = toAcceptAttr(column.accept);
|
|
27
|
+
async function onPick(e) {
|
|
28
|
+
const file = e.target.files?.[0];
|
|
29
|
+
if (file === undefined || uploadTarget === undefined)
|
|
30
|
+
return;
|
|
31
|
+
setUploading(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
try {
|
|
34
|
+
const uploadFile = await resizeImageBeforeUpload(file);
|
|
35
|
+
const id = await uploadFileRef(uploadFile, { ...uploadTarget, subField: column.field });
|
|
36
|
+
onChange([...ids, id]);
|
|
37
|
+
}
|
|
38
|
+
catch (cause) {
|
|
39
|
+
if (isSessionEndedUploadError(cause))
|
|
40
|
+
sessionEnded?.notify();
|
|
41
|
+
setError(t(uploadErrorKey(cause)));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setUploading(false);
|
|
45
|
+
if (inputRef.current)
|
|
46
|
+
inputRef.current.value = "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return (_jsxs("div", { "data-cell-id": cellId, className: "flex min-w-0 flex-wrap items-center gap-2", children: [ids.map((fileId) => (_jsxs("div", { className: "relative", children: [isImage ? (_jsx(FileThumbnail, { fileId: fileId, className: "size-16" })) : (_jsx(FileNameLink, { fileId: fileId, fallbackLabel: t("kumiko.field.file-upload.attached") })), !readOnly && (_jsx("button", { type: "button", "aria-label": t("kumiko.field.file-upload.remove"), className: "absolute -right-2 -top-2 flex size-6 items-center justify-center rounded-full border bg-card text-muted-foreground", onClick: () => onChange(ids.filter((id) => id !== fileId)), children: _jsx(X, { className: "size-3", "aria-hidden": "true" }) }))] }, fileId))), canAdd && (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, type: "file", className: "hidden", ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(column.capture !== undefined && { capture: column.capture }), onChange: (e) => void onPick(e) }), _jsx("button", { type: "button", "aria-label": `${label}: ${t("kumiko.field.file-upload.upload")}`, disabled: uploading, className: cn("flex size-16 items-center justify-center rounded-md border border-dashed text-muted-foreground"), onClick: () => inputRef.current?.click(), children: uploading ? (_jsx(Loader2, { className: "size-5 animate-spin", "aria-hidden": "true" })) : (_jsx(Plus, { className: "size-5", "aria-hidden": "true" })) })] })), error !== null && _jsx("span", { className: "w-full text-xs text-destructive", children: error })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ClipboardEvent, KeyboardEvent, ReactNode } from "react";
|
|
2
|
+
import type { FieldIssue } from "../../headless/index.ts";
|
|
3
|
+
import type { EmbeddedListColumn, EmbeddedListInputProps } from "../../index.ts";
|
|
4
|
+
export type EmbeddedListCardsProps = {
|
|
5
|
+
readonly columns: readonly EmbeddedListColumn[];
|
|
6
|
+
readonly rows: EmbeddedListInputProps["rows"];
|
|
7
|
+
readonly titleColumn: EmbeddedListColumn | undefined;
|
|
8
|
+
readonly lastEditableIndex: number;
|
|
9
|
+
readonly rowIssues: EmbeddedListInputProps["rowIssues"];
|
|
10
|
+
readonly cellIssues: EmbeddedListInputProps["cellIssues"];
|
|
11
|
+
readonly listIssues: readonly FieldIssue[] | undefined;
|
|
12
|
+
readonly totalsNode: ReactNode;
|
|
13
|
+
readonly disabled: boolean;
|
|
14
|
+
readonly showControls: boolean;
|
|
15
|
+
readonly addDisabled: boolean;
|
|
16
|
+
readonly minItems: number | undefined;
|
|
17
|
+
readonly maxItems: number | undefined;
|
|
18
|
+
readonly currency: string;
|
|
19
|
+
readonly locale: string;
|
|
20
|
+
readonly uploadTarget: EmbeddedListInputProps["uploadTarget"];
|
|
21
|
+
readonly onCellChange: EmbeddedListInputProps["onCellChange"];
|
|
22
|
+
readonly onAddRow: () => void;
|
|
23
|
+
readonly onRemoveRow: (rowIndex: number) => void;
|
|
24
|
+
readonly onDuplicateRow: (rowIndex: number) => void;
|
|
25
|
+
readonly onMoveRow: (fromIndex: number, toIndex: number) => void;
|
|
26
|
+
readonly handlePaste: (rowIndex: number, columnIndex: number) => (event: ClipboardEvent<HTMLElement>) => void;
|
|
27
|
+
readonly handleLastCellKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
28
|
+
readonly pasteEnabled: boolean;
|
|
29
|
+
readonly cellId: (rowIndex: number, field: string) => string;
|
|
30
|
+
readonly cellLabel: (column: EmbeddedListColumn, rowIndex: number) => string;
|
|
31
|
+
readonly testId: string | undefined;
|
|
32
|
+
readonly addLabel: string;
|
|
33
|
+
readonly duplicateLabel: string;
|
|
34
|
+
readonly moveUpLabel: string;
|
|
35
|
+
readonly moveDownLabel: string;
|
|
36
|
+
readonly removeLabel: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function EmbeddedListCards(props: EmbeddedListCardsProps): ReactNode;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Mobile card layout for EmbeddedListInput. One card per row: the title, then
|
|
3
|
+
// one block per visible column. Columns marked hiddenInCard are skipped, and a
|
|
4
|
+
// column with cardAttachedTo sits unlabeled next to its anchor control.
|
|
5
|
+
import { Plus } from "lucide-react";
|
|
6
|
+
import { useTranslation } from "../../index.js";
|
|
7
|
+
import { Button as UiButton } from "../ui/button.js";
|
|
8
|
+
import { IssueMessages, isDerivedCell, RowActions, readOnlyCellText, renderCellControl, } from "./embedded-list-cell.js";
|
|
9
|
+
import { formatLocaleNumber } from "./locale-number.js";
|
|
10
|
+
function CardLimitsHint({ limits, row, locale, testId, }) {
|
|
11
|
+
const t = useTranslation();
|
|
12
|
+
const rawMin = row[limits.min];
|
|
13
|
+
const rawMax = row[limits.max];
|
|
14
|
+
const min = typeof rawMin === "number" ? formatLocaleNumber(rawMin, locale, { grouping: true }) : undefined;
|
|
15
|
+
const max = typeof rawMax === "number" ? formatLocaleNumber(rawMax, locale, { grouping: true }) : undefined;
|
|
16
|
+
let text;
|
|
17
|
+
if (min !== undefined && max !== undefined) {
|
|
18
|
+
text = t("kumiko.field.embedded-list.limit-range", { min, max });
|
|
19
|
+
}
|
|
20
|
+
else if (min !== undefined) {
|
|
21
|
+
text = t("kumiko.field.embedded-list.limit-min", { min });
|
|
22
|
+
}
|
|
23
|
+
else if (max !== undefined) {
|
|
24
|
+
text = t("kumiko.field.embedded-list.limit-max", { max });
|
|
25
|
+
}
|
|
26
|
+
if (text === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
return (_jsx("div", { "data-testid": testId, className: "text-xs text-muted-foreground", children: text }));
|
|
29
|
+
}
|
|
30
|
+
export function EmbeddedListCards(props) {
|
|
31
|
+
const { columns, rows, titleColumn, lastEditableIndex, rowIssues, cellIssues, listIssues, totalsNode, disabled, showControls, addDisabled, minItems, maxItems, currency, locale, uploadTarget, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, handlePaste, handleLastCellKeyDown, pasteEnabled, cellId, cellLabel, testId, addLabel, duplicateLabel, moveUpLabel, moveDownLabel, removeLabel, } = props;
|
|
32
|
+
const testIdFor = (suffix) => testId !== undefined ? `${testId}-${suffix}` : undefined;
|
|
33
|
+
return (_jsxs("div", { "data-testid": testIdFor("mobile"), className: "md:hidden flex flex-col gap-3", children: [rows.map((row, rowIndex) => {
|
|
34
|
+
const isLastRow = rowIndex === rows.length - 1;
|
|
35
|
+
const rowIssuesForRow = rowIssues?.[rowIndex];
|
|
36
|
+
const isVisible = (column) => column !== titleColumn &&
|
|
37
|
+
column.hiddenInCard !== true &&
|
|
38
|
+
column.isHiddenInRow?.(row) !== true;
|
|
39
|
+
const renderControl = (column) => renderCellControl({
|
|
40
|
+
cellId: cellId(rowIndex, column.field),
|
|
41
|
+
label: cellLabel(column, rowIndex),
|
|
42
|
+
column,
|
|
43
|
+
value: row[column.field],
|
|
44
|
+
disabled,
|
|
45
|
+
onChange: (value) => onCellChange(rowIndex, column.field, value),
|
|
46
|
+
currency,
|
|
47
|
+
locale,
|
|
48
|
+
derived: isDerivedCell(column, row),
|
|
49
|
+
touch: true,
|
|
50
|
+
uploadTarget,
|
|
51
|
+
});
|
|
52
|
+
const renderIssues = (column) => (_jsx(IssueMessages, { issues: cellIssues?.[`${rowIndex}.${column.field}`], testId: testIdFor(`cell-${rowIndex}-${column.field}-errors`) }));
|
|
53
|
+
const anchorHasAttachment = (anchor) => columns.find((other) => other.cardAttachedTo === anchor.field &&
|
|
54
|
+
other.hiddenInCard !== true &&
|
|
55
|
+
other.field !== anchor.field);
|
|
56
|
+
return (_jsxs("div", { "data-testid": testIdFor(`row-${rowIndex}`), className: "flex flex-col gap-3 rounded-lg border bg-card p-4", children: [titleColumn !== undefined && (_jsxs("div", { "data-testid": testIdFor(`row-${rowIndex}-title`), className: "font-medium", children: [rowIndex + 1, " ", readOnlyCellText(titleColumn, row[titleColumn.field], currency, locale)] })), columns.map((column, columnIndex) => {
|
|
57
|
+
if (!isVisible(column))
|
|
58
|
+
return null;
|
|
59
|
+
const anchor = column.cardAttachedTo !== undefined
|
|
60
|
+
? columns.find((other) => other.field === column.cardAttachedTo)
|
|
61
|
+
: undefined;
|
|
62
|
+
if (anchor !== undefined && isVisible(anchor))
|
|
63
|
+
return null;
|
|
64
|
+
const attached = anchorHasAttachment(column);
|
|
65
|
+
const isLastCell = isLastRow && columnIndex === lastEditableIndex;
|
|
66
|
+
const showLabel = column.cardAttachedTo === undefined;
|
|
67
|
+
const control = (_jsxs("div", { className: attached !== undefined ? "min-w-0 flex-1" : undefined, children: [renderControl(column), column.cardLimits !== undefined && (_jsx(CardLimitsHint, { limits: column.cardLimits, row: row, locale: locale, testId: testIdFor(`cell-${rowIndex}-${column.field}-limits`) }))] }));
|
|
68
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", onPaste: pasteEnabled ? handlePaste(rowIndex, columnIndex) : undefined, onKeyDown: isLastCell ? handleLastCellKeyDown : undefined, children: [showLabel && (_jsxs("span", { className: "text-xs font-medium text-muted-foreground", children: [column.label, column.isRequiredInRow?.(row) === true && " *"] })), attached !== undefined ? (_jsxs("div", { className: "flex items-start gap-3", children: [control, _jsx("div", { className: "shrink-0", children: renderControl(attached) })] })) : (control), renderIssues(column), attached !== undefined && renderIssues(attached)] }, column.field));
|
|
69
|
+
}), rowIssuesForRow !== undefined && rowIssuesForRow.length > 0 && (_jsx(IssueMessages, { issues: rowIssuesForRow, testId: testIdFor(`row-${rowIndex}-issues`) })), showControls && (_jsx("div", { className: "flex items-center justify-end gap-1 border-t pt-3", children: _jsx(RowActions, { rowIndex: rowIndex, rowsLength: rows.length, minItems: minItems, maxItems: maxItems, onDuplicateRow: onDuplicateRow, onMoveRow: onMoveRow, onRemoveRow: onRemoveRow, duplicateLabel: duplicateLabel, moveUpLabel: moveUpLabel, moveDownLabel: moveDownLabel, removeLabel: removeLabel, testIdPrefix: testIdFor(`row-${rowIndex}`), buttonSizeClass: "size-10" }) }))] }, rowIndex));
|
|
70
|
+
}), showControls && (_jsxs(UiButton, { type: "button", variant: "outline", onClick: onAddRow, disabled: addDisabled, "data-testid": testIdFor("add"), children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), addLabel] })), totalsNode, _jsx(IssueMessages, { issues: listIssues, testId: testIdFor("list-issues") })] }));
|
|
71
|
+
}
|