@protobase/ui 0.19.14 → 0.19.16
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/app.js +2 -2
- package/dist/app/create/create-page.js +3 -3
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/board-column.js +114 -0
- package/dist/app/displays/board-columns.js +45 -0
- package/dist/app/displays/board-display.js +96 -0
- package/dist/app/displays/board-dnd.js +32 -0
- package/dist/app/displays/list-cards.js +1 -1
- package/dist/app/displays/list-displays.js +13 -1
- package/dist/app/displays/map-display.js +1 -1
- package/dist/app/displays/record-card.js +18 -7
- package/dist/app/displays/record-link.js +1 -1
- package/dist/app/displays/use-board-column.js +23 -0
- package/dist/app/displays/use-board-move.js +60 -0
- package/dist/app/displays/virtual-cards.js +1 -0
- package/dist/app/embedded-list/embedded-list.js +10 -1
- package/dist/app/export/export-options.js +2 -2
- package/dist/app/field-editor.js +6 -6
- package/dist/app/gallery/gallery-section.js +1 -1
- package/dist/app/gallery/lightbox.js +1 -1
- package/dist/app/gallery/photo-details.js +3 -3
- package/dist/app/gallery/use-gallery-photos.js +2 -2
- package/dist/app/import/import-dialog.js +1 -1
- package/dist/app/import/lookups.js +2 -2
- package/dist/app/list-edit/cell-field-editor.js +61 -0
- package/dist/app/list-edit/use-list-cell-editor.js +30 -0
- package/dist/app/list-edit/use-list-edits.js +75 -0
- package/dist/app/list-page.js +31 -13
- package/dist/app/map/map-area.js +6 -6
- package/dist/app/pages/action-href.js +4 -2
- package/dist/app/pages/action-inputs/action-inputs-dialog.d.ts +13 -0
- package/dist/app/pages/action-inputs/action-inputs-dialog.js +111 -0
- package/dist/app/pages/action-inputs/input-answers.js +88 -0
- package/dist/app/pages/action-inputs/input-errors.d.ts +11 -0
- package/dist/app/pages/action-inputs/input-errors.js +25 -0
- package/dist/app/pages/action-prompt.js +29 -0
- package/dist/app/pages/blocks/action.js +8 -4
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/link.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +5 -5
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/blocks/stat.js +1 -1
- package/dist/app/pages/field-value.js +2 -2
- package/dist/app/pages/project-ui.d.ts +5 -0
- package/dist/app/pages/record-actions.js +8 -4
- package/dist/app/pages/use-run-action.js +20 -9
- package/dist/app/record-lines.js +1 -1
- package/dist/app/record-page.js +41 -27
- package/dist/app/record-preview.js +25 -0
- package/dist/app/record-row/record-row-body.js +3 -3
- package/dist/app/record-row/record-row.js +1 -0
- package/dist/app/record-row/saved-record-row.js +1 -1
- package/dist/app/related-dots/card-dots-lines.js +16 -0
- package/dist/app/related-dots/card-dots.js +34 -0
- package/dist/app/related-dots/dot-card.js +3 -3
- package/dist/app/related-dots/related-dots-columns.js +3 -1
- package/dist/app/related-dots/related-dots.js +22 -9
- package/dist/app/related-section.js +5 -5
- package/dist/app/relation-picker.js +2 -2
- package/dist/app/saved/saved-records.js +2 -2
- package/dist/app/search/search-model.js +2 -2
- package/dist/app/searches/saved-search-rows.js +1 -1
- package/dist/app/searches/saved-search.js +3 -3
- package/dist/app/searches/saved-searches.js +2 -2
- package/dist/app/searches/search-controls.js +1 -1
- package/dist/app/searches/search-diff.js +8 -8
- package/dist/app/searches/search-name-dialog.js +1 -1
- package/dist/app/searches/update-search-dialog.js +1 -1
- package/dist/app/searches/use-save-search.js +1 -1
- package/dist/app/sensitive-field.js +2 -2
- package/dist/app/use-nav-recent.js +1 -2
- package/dist/app/use-record-editor.js +1 -1
- package/dist/auth/sign-in-prompt.js +1 -1
- package/dist/data/api-provider.d.ts +2 -1
- package/dist/data/api-provider.js +12 -5
- package/dist/data/focus-refresh.js +44 -0
- package/dist/data/query-keys.js +13 -0
- package/dist/data/use-record.js +17 -2
- package/dist/data-grid/cell-moves.js +26 -0
- package/dist/data-grid/cells.js +1 -1
- package/dist/data-grid/data-grid.js +32 -7
- package/dist/data-grid/grid-header.js +1 -1
- package/dist/data-grid/grid-row.js +17 -13
- package/dist/data-grid/page-cache.js +3 -0
- package/dist/data-grid/use-cell-editing.js +113 -0
- package/dist/data-grid/use-paged-rows.js +25 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -4
- package/dist/live/columns-from-model.js +1 -1
- package/dist/live/field-values.js +3 -1
- package/dist/live/related-column-rows.js +3 -3
- package/dist/live/relation-labels.js +2 -2
- package/dist/live/use-live-rows.js +17 -4
- package/dist/live/use-reference-search.js +2 -2
- package/dist/markdown/markdown-editor.js +1 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-xQmLJM07.js +69 -0
- package/dist/shell/assets/index-CmJl5dIO.css +2 -0
- package/dist/shell/assets/{index-DF5WCYEC.js → index-CmmYBR7Y.js} +1 -1
- package/dist/shell/assets/{maplibre-DYbWlNbK.js → maplibre-Siom2Zwj.js} +2 -2
- package/dist/shell/assets/{markdown-content-o9fxfOx0.js → markdown-content-ChyGEubR.js} +1 -1
- package/dist/shell/assets/{record-map-CmVH3wtS.js → record-map-B_QnHF99.js} +1 -1
- package/dist/shell/assets/{records-map-XibwyiR4.js → records-map-CcHhEDBC.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-zRmreKZS.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-Bet1bgM_.js +0 -69
- package/dist/shell/assets/index-DEEIUcYb.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-DNhP23mn.js +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isApiError } from "@protobase/client";
|
|
2
|
+
//#region src/app/pages/action-inputs/input-errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a handler throws to refuse answers, each message under the input it is about, such as
|
|
5
|
+
* `throw new ActionInputError({ email: 'No user has this address' })`: the dialog stays open and shows each under its input.
|
|
6
|
+
*/
|
|
7
|
+
var ActionInputError = class extends Error {
|
|
8
|
+
errors;
|
|
9
|
+
constructor(errors) {
|
|
10
|
+
super(Object.values(errors).join("; "));
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
this.name = "ActionInputError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The messages of a failed action about its inputs, by input name: an `ActionInputError`'s, or those of an API error
|
|
17
|
+
* whose field is an input's name, as a function the handler invokes reports them. Empty when none is about an input.
|
|
18
|
+
*/
|
|
19
|
+
const inputErrors = (error, inputs) => {
|
|
20
|
+
const names = new Set(inputs.map((input) => input.name));
|
|
21
|
+
const entries = error instanceof ActionInputError ? Object.entries(error.errors) : isApiError(error) ? error.errors.flatMap((entry) => entry.field ? [[entry.field, entry.message]] : []) : [];
|
|
22
|
+
return Object.fromEntries(entries.filter(([name]) => names.has(name)));
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ActionInputError, inputErrors };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ActionInputsDialog } from "./action-inputs/action-inputs-dialog.js";
|
|
2
|
+
import { ConfirmAction } from "./confirm-action.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app/pages/action-prompt.tsx
|
|
5
|
+
/** Whether pressing the action's button opens a dialog first: to ask its question, or for its inputs. */
|
|
6
|
+
const asksFirst = (action) => Boolean(action.confirm || action.inputs?.length);
|
|
7
|
+
/** The dialog an action that asks first opens: its inputs, or else its question. */
|
|
8
|
+
const ActionPrompt = ({ action, label, danger, onCancel, onConfirm, onSubmit }) => {
|
|
9
|
+
const { inputs } = action;
|
|
10
|
+
if (inputs?.length) return /* @__PURE__ */ jsx(ActionInputsDialog, {
|
|
11
|
+
action: {
|
|
12
|
+
...action,
|
|
13
|
+
inputs
|
|
14
|
+
},
|
|
15
|
+
label,
|
|
16
|
+
danger,
|
|
17
|
+
onCancel,
|
|
18
|
+
onSubmit
|
|
19
|
+
});
|
|
20
|
+
return /* @__PURE__ */ jsx(ConfirmAction, {
|
|
21
|
+
action,
|
|
22
|
+
label,
|
|
23
|
+
danger,
|
|
24
|
+
onCancel,
|
|
25
|
+
onConfirm
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { ActionPrompt, asksFirst };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Button } from "../../../primitives/button.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ActionPrompt, asksFirst } from "../action-prompt.js";
|
|
3
3
|
import { useLayoutRecord } from "../record-scope.js";
|
|
4
4
|
import { useRunAction } from "../use-run-action.js";
|
|
5
5
|
import { propsOf } from "./block-props.js";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/app/pages/blocks/action.tsx
|
|
9
|
-
/** A button for a named action of the resource's view, asking first when the action has `confirm`. */
|
|
9
|
+
/** A button for a named action of the resource's view, asking first when the action has `confirm` or `inputs`. */
|
|
10
10
|
const ActionBlock = ({ node }) => {
|
|
11
11
|
const { name, resource, label, variant } = propsOf(node);
|
|
12
12
|
const scope = useLayoutRecord();
|
|
@@ -14,7 +14,7 @@ const ActionBlock = ({ node }) => {
|
|
|
14
14
|
const [asking, setAsking] = useState(false);
|
|
15
15
|
if (!action.action) return null;
|
|
16
16
|
const text = label ?? action.action.label;
|
|
17
|
-
const start = () => action.action
|
|
17
|
+
const start = () => asksFirst(action.action) ? setAsking(true) : void action.run();
|
|
18
18
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
19
19
|
size: "sm",
|
|
20
20
|
variant: variant ?? "secondary",
|
|
@@ -23,7 +23,7 @@ const ActionBlock = ({ node }) => {
|
|
|
23
23
|
title: action.blocked,
|
|
24
24
|
onClick: start,
|
|
25
25
|
children: text
|
|
26
|
-
}), asking && /* @__PURE__ */ jsx(
|
|
26
|
+
}), asking && /* @__PURE__ */ jsx(ActionPrompt, {
|
|
27
27
|
action: action.action,
|
|
28
28
|
label: text,
|
|
29
29
|
danger: variant === "danger",
|
|
@@ -31,6 +31,10 @@ const ActionBlock = ({ node }) => {
|
|
|
31
31
|
onConfirm: () => {
|
|
32
32
|
setAsking(false);
|
|
33
33
|
action.run();
|
|
34
|
+
},
|
|
35
|
+
onSubmit: async (values) => {
|
|
36
|
+
await action.submit(values);
|
|
37
|
+
setAsking(false);
|
|
34
38
|
}
|
|
35
39
|
})] });
|
|
36
40
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Skeleton } from "../../../primitives/skeleton.js";
|
|
2
2
|
import { useAdminMeta } from "../../meta-gate.js";
|
|
3
|
+
import { ErrorBanner } from "../../error-banner.js";
|
|
3
4
|
import { RecordScope } from "../record-scope.js";
|
|
4
5
|
import { scopeOf, useListPage } from "../use-layout-data.js";
|
|
5
6
|
import { propsOf } from "./block-props.js";
|
|
6
7
|
import { CardFrame, Quiet } from "./card-frame.js";
|
|
7
|
-
import { ErrorBanner } from "../../error-banner.js";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
9
9
|
//#region src/app/pages/blocks/card-row.tsx
|
|
10
10
|
const LoadedCardRow = ({ model, filter, sort, limit = 12, title, description, empty, actions, children }) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useLayoutRecord } from "../record-scope.js";
|
|
2
1
|
import { AppLink } from "../../use-app-address.js";
|
|
2
|
+
import { useLayoutRecord } from "../record-scope.js";
|
|
3
3
|
import { propsOf } from "./block-props.js";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { appPath } from "@protobase/schema";
|
|
@@ -4,15 +4,15 @@ import { useClient } from "../../../data/api-provider.js";
|
|
|
4
4
|
import { humanize } from "../../../live/naming.js";
|
|
5
5
|
import { useToast } from "../../../toasts/toast-provider.js";
|
|
6
6
|
import { useAdminMeta, usePermissions } from "../../meta-gate.js";
|
|
7
|
+
import { Field } from "../../../record-view/field.js";
|
|
8
|
+
import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
|
|
9
|
+
import { createBody, isChosen } from "../../create/create-values.js";
|
|
10
|
+
import { ErrorBanner } from "../../error-banner.js";
|
|
11
|
+
import { FieldEditor } from "../../field-editor.js";
|
|
7
12
|
import { useLayoutRecord } from "../record-scope.js";
|
|
8
13
|
import { can } from "../../permissions.js";
|
|
9
14
|
import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
|
|
10
15
|
import { propsOf } from "./block-props.js";
|
|
11
|
-
import { ErrorBanner } from "../../error-banner.js";
|
|
12
|
-
import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
|
|
13
|
-
import { Field } from "../../../record-view/field.js";
|
|
14
|
-
import { createBody, isChosen } from "../../create/create-values.js";
|
|
15
|
-
import { FieldEditor } from "../../field-editor.js";
|
|
16
16
|
import { useState } from "react";
|
|
17
17
|
import { isApiError } from "@protobase/client";
|
|
18
18
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Skeleton } from "../../../primitives/skeleton.js";
|
|
2
2
|
import { useAdminMeta } from "../../meta-gate.js";
|
|
3
|
+
import { ErrorBanner } from "../../error-banner.js";
|
|
3
4
|
import { RecordScope } from "../record-scope.js";
|
|
4
5
|
import { useFirstRecord } from "../use-layout-data.js";
|
|
5
6
|
import { propsOf } from "./block-props.js";
|
|
6
7
|
import { CardFrame, Quiet } from "./card-frame.js";
|
|
7
|
-
import { ErrorBanner } from "../../error-banner.js";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/app/pages/blocks/record-card.tsx
|
|
10
10
|
const LoadedRecordCard = ({ model, filter, sort, recordKey, title, description, empty, actions, children }) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Skeleton } from "../../../primitives/skeleton.js";
|
|
2
2
|
import { useAdminMeta } from "../../meta-gate.js";
|
|
3
|
-
import { useLayoutRecord } from "../record-scope.js";
|
|
4
3
|
import { AppLink } from "../../use-app-address.js";
|
|
4
|
+
import { useLayoutRecord } from "../record-scope.js";
|
|
5
5
|
import { useCount } from "../use-layout-data.js";
|
|
6
6
|
import { propsOf } from "./block-props.js";
|
|
7
7
|
import { CardFrame } from "./card-frame.js";
|
|
@@ -6,12 +6,12 @@ import { fileShown, shownImage } from "../../live/file-values.js";
|
|
|
6
6
|
import { displayValue } from "../../live/display-value.js";
|
|
7
7
|
import { AppLink } from "../use-app-address.js";
|
|
8
8
|
import { FileImage } from "../../live/file-image.js";
|
|
9
|
-
import { fetchLabels } from "../../live/relation-labels.js";
|
|
10
9
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
11
10
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
|
+
import { Markdown } from "../../markdown/markdown.js";
|
|
12
12
|
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
13
|
+
import { fetchLabels } from "../../live/relation-labels.js";
|
|
13
14
|
import { ValueBadge } from "../value-badge.js";
|
|
14
|
-
import { Markdown } from "../../markdown/markdown.js";
|
|
15
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
16
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
17
17
|
//#region src/app/pages/field-value.tsx
|
|
@@ -18,6 +18,11 @@ export type ActionContext = {
|
|
|
18
18
|
client: Client;
|
|
19
19
|
/** Moves the app to an address, such as `{ kind: 'record', resource: 'orders', key: 42 }`. */
|
|
20
20
|
navigate: (to: AppAddress) => void;
|
|
21
|
+
/**
|
|
22
|
+
* The answers to the action's inputs, by input name, for an action with `inputs`: those that show, null when left
|
|
23
|
+
* empty. Throwing `ActionInputError` refuses them, each message under its input.
|
|
24
|
+
*/
|
|
25
|
+
values?: Record<string, unknown>;
|
|
21
26
|
};
|
|
22
27
|
export type ActionHandler = (context: ActionContext) => void | Promise<void>;
|
|
23
28
|
/** What a plugin's `beforeSignIn` component gets from the app, drawn around the pages someone signed out sees. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionRail } from "../../record-view/action-rail.js";
|
|
2
2
|
import { AppLink, useAppAddress } from "../use-app-address.js";
|
|
3
|
+
import { ActionPrompt, asksFirst } from "./action-prompt.js";
|
|
3
4
|
import { useActionRunner } from "./use-run-action.js";
|
|
4
|
-
import { ActionRail } from "../../record-view/action-rail.js";
|
|
5
5
|
import { actionIcon } from "../action-icon.js";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { Pencil, Trash2 } from "lucide-react";
|
|
@@ -32,7 +32,7 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
|
32
32
|
blocked: runner.blocked(action),
|
|
33
33
|
running: runner.running === action.name,
|
|
34
34
|
...action.run?.kind === "delete" && { tone: "danger" },
|
|
35
|
-
onRun: () => action
|
|
35
|
+
onRun: () => asksFirst(action) ? setAsking(action) : void runner.run(action)
|
|
36
36
|
}));
|
|
37
37
|
const deletes = runner.actions.some((action) => action.run?.kind === "delete");
|
|
38
38
|
const actions = [
|
|
@@ -54,7 +54,7 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
|
54
54
|
}] : []
|
|
55
55
|
];
|
|
56
56
|
if (actions.length === 0) return null;
|
|
57
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionRail, { actions }), asking && /* @__PURE__ */ jsx(
|
|
57
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionRail, { actions }), asking && /* @__PURE__ */ jsx(ActionPrompt, {
|
|
58
58
|
action: asking,
|
|
59
59
|
label: asking.label,
|
|
60
60
|
danger: asking.run?.kind === "delete",
|
|
@@ -62,6 +62,10 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
|
62
62
|
onConfirm: () => {
|
|
63
63
|
setAsking(void 0);
|
|
64
64
|
runner.run(asking);
|
|
65
|
+
},
|
|
66
|
+
onSubmit: async (values) => {
|
|
67
|
+
await runner.submit(asking, values);
|
|
68
|
+
setAsking(void 0);
|
|
65
69
|
}
|
|
66
70
|
})] });
|
|
67
71
|
};
|
|
@@ -2,8 +2,8 @@ import { useClient, useResourceClient } from "../../data/api-provider.js";
|
|
|
2
2
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
3
3
|
import { useProjectUi } from "./project-ui.js";
|
|
4
4
|
import { useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
5
|
-
import { can } from "../permissions.js";
|
|
6
5
|
import { useAppAddress } from "../use-app-address.js";
|
|
6
|
+
import { can } from "../permissions.js";
|
|
7
7
|
import { actionFor, linkTarget, openLink } from "./action-href.js";
|
|
8
8
|
import { useRefreshResource } from "./use-layout-data.js";
|
|
9
9
|
import { useState } from "react";
|
|
@@ -12,13 +12,14 @@ import { fillAddress } from "@protobase/schema/internal";
|
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
14
14
|
* The named actions of `resource`'s view, ready to run on `scope` (the record they act on): built-in behaviour first
|
|
15
|
-
* (`update`, `delete`, `link`), else the handler the project registered under the action's name.
|
|
16
|
-
* record holds is left out while it holds none. `blocked` says
|
|
17
|
-
* that succeeded.
|
|
15
|
+
* (`update`, `delete`, `link`), else the handler the project registered under the action's name. An update whose values
|
|
16
|
+
* the record holds already is left out, and so is a link to a URL the record holds while it holds none. `blocked` says
|
|
17
|
+
* why one cannot run; `afterRun` hears about every action that succeeded. `submit` runs one with the answers to its
|
|
18
|
+
* inputs and throws its failure, for the dialog to show.
|
|
18
19
|
*/
|
|
19
20
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
20
21
|
const { views } = useAdminMeta();
|
|
21
|
-
const { actions: handlers } = useProjectUi();
|
|
22
|
+
const { actions: handlers, components } = useProjectUi();
|
|
22
23
|
const permissions = usePermissions(resource);
|
|
23
24
|
const client = useClient();
|
|
24
25
|
const handlerClient = useResourceClient(resource);
|
|
@@ -33,8 +34,10 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
33
34
|
if (kind === "update" && (!can(permissions, "update") || scope?.permissions?.update === false)) return "You cannot change this record";
|
|
34
35
|
if (kind === "delete" && (!can(permissions, "delete") || scope?.permissions?.delete === false)) return "You cannot delete this record";
|
|
35
36
|
if (!action.run && !handlers?.[action.name]) return `The app has no handler for "${action.name}"`;
|
|
37
|
+
const custom = action.inputs?.find((input) => input.kind === "custom" && !components?.[input.component ?? ""]);
|
|
38
|
+
if (custom) return `The app has no component "${custom.component}" for input "${custom.name}"`;
|
|
36
39
|
};
|
|
37
|
-
const perform = async (action) => {
|
|
40
|
+
const perform = async (action, values) => {
|
|
38
41
|
const { run } = action;
|
|
39
42
|
if (run?.kind === "link") {
|
|
40
43
|
if ("to" in run) go(fillAddress(run.to, scope?.record));
|
|
@@ -49,7 +52,8 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
49
52
|
recordKey: scope?.key,
|
|
50
53
|
etag: scope?.etag,
|
|
51
54
|
client: handlerClient,
|
|
52
|
-
navigate: go
|
|
55
|
+
navigate: go,
|
|
56
|
+
...values && { values }
|
|
53
57
|
});
|
|
54
58
|
await refresh(resource);
|
|
55
59
|
toast.show({
|
|
@@ -68,11 +72,17 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
68
72
|
}));
|
|
69
73
|
setRunning(void 0);
|
|
70
74
|
};
|
|
75
|
+
const submit = async (action, values) => {
|
|
76
|
+
if (blocked(action)) return;
|
|
77
|
+
setRunning(action.name);
|
|
78
|
+
await perform(action, values).finally(() => setRunning(void 0));
|
|
79
|
+
};
|
|
71
80
|
return {
|
|
72
81
|
actions,
|
|
73
82
|
blocked,
|
|
74
83
|
running,
|
|
75
|
-
run
|
|
84
|
+
run,
|
|
85
|
+
submit
|
|
76
86
|
};
|
|
77
87
|
};
|
|
78
88
|
/** One named action of `resource`'s view, for a button. */
|
|
@@ -83,7 +93,8 @@ const useRunAction = (resource, name, scope) => {
|
|
|
83
93
|
action,
|
|
84
94
|
blocked: action ? runner.blocked(action) : `No action "${name}"`,
|
|
85
95
|
running: runner.running === name,
|
|
86
|
-
run: () => action ? runner.run(action) : Promise.resolve()
|
|
96
|
+
run: () => action ? runner.run(action) : Promise.resolve(),
|
|
97
|
+
submit: (values) => action ? runner.submit(action, values) : Promise.resolve()
|
|
87
98
|
};
|
|
88
99
|
};
|
|
89
100
|
//#endregion
|
package/dist/app/record-lines.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { usePermissions } from "./meta-gate.js";
|
|
2
|
-
import { can } from "./permissions.js";
|
|
3
2
|
import { InlineChildTable } from "../inline-child-table/inline-child-table.js";
|
|
4
3
|
import { Section } from "../record-view/section.js";
|
|
4
|
+
import { can } from "./permissions.js";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
//#region src/app/record-lines.tsx
|
|
7
7
|
/** A resource that holds numbered lines of this one: it points at the parent and has `position` and `quantity`. */
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Skeleton } from "../primitives/skeleton.js";
|
|
2
3
|
import { inApp } from "../app-shell/breadcrumb.js";
|
|
3
4
|
import { useAuth } from "../auth/auth-provider.js";
|
|
4
5
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
6
|
+
import { usePauseFocusRefresh } from "../data/focus-refresh.js";
|
|
5
7
|
import { keys } from "../data/query-keys.js";
|
|
6
8
|
import { useClient } from "../data/api-provider.js";
|
|
7
9
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
8
|
-
import {
|
|
10
|
+
import { useOpenedRecord } from "../data/use-record.js";
|
|
9
11
|
import { recordTitle } from "../live/model-helpers.js";
|
|
10
12
|
import { humanize } from "../live/naming.js";
|
|
11
13
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
12
14
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
13
|
-
import {
|
|
15
|
+
import { Field } from "../record-view/field.js";
|
|
16
|
+
import { Section } from "../record-view/section.js";
|
|
17
|
+
import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record-sidebar.js";
|
|
18
|
+
import { RecordHeader } from "../record-view/record-header.js";
|
|
19
|
+
import { SpecTable } from "../record-view/spec-table.js";
|
|
20
|
+
import { KeyFacts } from "../record-view/key-facts.js";
|
|
21
|
+
import { isRequiredOnEdit } from "../live/field-values.js";
|
|
22
|
+
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
23
|
+
import { ErrorBanner } from "./error-banner.js";
|
|
14
24
|
import { displayValue } from "../live/display-value.js";
|
|
15
25
|
import { filledFields } from "./filled-fields.js";
|
|
16
26
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
17
|
-
import { ErrorBanner } from "./error-banner.js";
|
|
18
27
|
import { shownWidget } from "./field-types/shown-widget.js";
|
|
19
28
|
import { WidgetPart } from "./field-types/widget-part.js";
|
|
29
|
+
import { Markdown } from "../markdown/markdown.js";
|
|
20
30
|
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
31
|
+
import { FieldEditor } from "./field-editor.js";
|
|
32
|
+
import { can, fieldAccess } from "./permissions.js";
|
|
21
33
|
import { toneFor } from "../live/columns-from-model.js";
|
|
22
34
|
import { ValueBadge } from "./value-badge.js";
|
|
23
|
-
import { Markdown } from "../markdown/markdown.js";
|
|
24
35
|
import { FieldValue } from "./pages/field-value.js";
|
|
25
|
-
import { Field } from "../record-view/field.js";
|
|
26
|
-
import { Section } from "../record-view/section.js";
|
|
27
|
-
import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record-sidebar.js";
|
|
28
|
-
import { RecordHeader } from "../record-view/record-header.js";
|
|
29
|
-
import { SpecTable } from "../record-view/spec-table.js";
|
|
30
|
-
import { KeyFacts } from "../record-view/key-facts.js";
|
|
31
|
-
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
32
|
-
import { FieldEditor } from "./field-editor.js";
|
|
33
36
|
import { namedIcon } from "./named-icon.js";
|
|
34
37
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
35
38
|
import { useRestore } from "./delete/use-restore.js";
|
|
@@ -70,7 +73,7 @@ const layoutFor = (model, view, read) => {
|
|
|
70
73
|
const RecordPage = ({ resource, recordKey }) => {
|
|
71
74
|
const { resources } = useAdminMeta();
|
|
72
75
|
const model = resources[resource];
|
|
73
|
-
const record =
|
|
76
|
+
const record = useOpenedRecord(resource, recordKey);
|
|
74
77
|
if (!model) return /* @__PURE__ */ jsxs(Notice, {
|
|
75
78
|
title: "Not found",
|
|
76
79
|
children: [
|
|
@@ -96,10 +99,11 @@ const RecordPage = ({ resource, recordKey }) => {
|
|
|
96
99
|
return /* @__PURE__ */ jsx(LoadedRecord, {
|
|
97
100
|
model,
|
|
98
101
|
recordKey,
|
|
99
|
-
stored: record.data
|
|
100
|
-
|
|
102
|
+
stored: record.data,
|
|
103
|
+
preview: record.isPlaceholderData
|
|
104
|
+
}, record.isPlaceholderData ? "preview" : "loaded");
|
|
101
105
|
};
|
|
102
|
-
const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
106
|
+
const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
103
107
|
const meta = useAdminMeta();
|
|
104
108
|
const { basePath } = useRouter();
|
|
105
109
|
const { address, go, open } = useAppAddress();
|
|
@@ -115,7 +119,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
115
119
|
recordKey,
|
|
116
120
|
mode: view?.saveFeedback ?? "toast",
|
|
117
121
|
subject: title,
|
|
118
|
-
draftKey: auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
122
|
+
draftKey: !preview && auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
119
123
|
lines: linesDraft
|
|
120
124
|
});
|
|
121
125
|
const queryClient = useQueryClient();
|
|
@@ -125,6 +129,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
125
129
|
const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
|
|
126
130
|
const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
|
|
127
131
|
useNavigationGuard(editor.dirty && shown === "form");
|
|
132
|
+
usePauseFocusRefresh(editor.dirty);
|
|
128
133
|
const readAddress = {
|
|
129
134
|
kind: "record",
|
|
130
135
|
resource: model.name,
|
|
@@ -156,6 +161,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
156
161
|
const updatedAt = auditField(model, "updated");
|
|
157
162
|
const hint = (name) => view?.fields[name];
|
|
158
163
|
const visible = (name) => fieldAccess(stored.permissions, name) !== "hidden" && whenShown(hint(name)?.when, editing);
|
|
164
|
+
const known = (name) => !preview || Object.hasOwn(stored.record, name);
|
|
159
165
|
const fieldView = (name) => {
|
|
160
166
|
const field = model.fields[name];
|
|
161
167
|
const access = fieldAccess(stored.permissions, name);
|
|
@@ -163,6 +169,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
163
169
|
const conflict = editor.conflicts[name];
|
|
164
170
|
return /* @__PURE__ */ jsx(Field, {
|
|
165
171
|
label: hint(name)?.label ?? humanize(name),
|
|
172
|
+
required: shown === "form" && access !== "read" && isRequiredOnEdit(field),
|
|
166
173
|
help: fieldHelp(hint(name), view, editing),
|
|
167
174
|
error: editor.errors[name],
|
|
168
175
|
wide: Boolean(hint(name)?.multiline || hint(name)?.markdown),
|
|
@@ -177,7 +184,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
177
184
|
onKeepMine: () => editor.keepMine(name),
|
|
178
185
|
onUseTheirs: () => editor.useTheirs(name)
|
|
179
186
|
},
|
|
180
|
-
children: /* @__PURE__ */ jsx(FieldEditor, {
|
|
187
|
+
children: known(name) ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
181
188
|
field,
|
|
182
189
|
hints: hint(name),
|
|
183
190
|
value: editor.value(name),
|
|
@@ -189,12 +196,12 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
189
196
|
reveal: field.sensitive ? () => client.reveal(model.name, recordKey, name) : void 0,
|
|
190
197
|
resource: model.name,
|
|
191
198
|
onChange: (next) => editor.change(name, next)
|
|
192
|
-
})
|
|
199
|
+
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-9" })
|
|
193
200
|
}, field.sensitive ? `${name}:${stored.etag}` : name);
|
|
194
201
|
};
|
|
195
202
|
const readRow = (name) => {
|
|
196
203
|
const field = model.fields[name];
|
|
197
|
-
const value = field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
204
|
+
const value = !known(name) ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }) : field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
198
205
|
field,
|
|
199
206
|
hints: hint(name),
|
|
200
207
|
value: "",
|
|
@@ -222,6 +229,10 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
222
229
|
rows: sidebarFields.flatMap((name) => {
|
|
223
230
|
const field = model.fields[name];
|
|
224
231
|
if (!field || !visible(name)) return [];
|
|
232
|
+
if (!known(name)) return [{
|
|
233
|
+
label: hint(name)?.label ?? humanize(name),
|
|
234
|
+
value: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
235
|
+
}];
|
|
225
236
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
226
237
|
const value = stored.record[name];
|
|
227
238
|
const markdown = isMarkdown(field, hint(name)) && typeof value === "string" && value !== "";
|
|
@@ -284,6 +295,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
284
295
|
] });
|
|
285
296
|
return /* @__PURE__ */ jsxs("div", {
|
|
286
297
|
className: "flex flex-1 flex-col",
|
|
298
|
+
inert: preview,
|
|
287
299
|
children: [
|
|
288
300
|
remove.dialogs,
|
|
289
301
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -355,6 +367,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
355
367
|
recordKey,
|
|
356
368
|
owner: model.name
|
|
357
369
|
}, `related:${item.title}`)];
|
|
370
|
+
if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
|
|
358
371
|
if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
|
|
359
372
|
item,
|
|
360
373
|
scope
|
|
@@ -385,7 +398,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
385
398
|
children: fields.map(fieldView)
|
|
386
399
|
})
|
|
387
400
|
}, item.title)];
|
|
388
|
-
}), linesModel && /* @__PURE__ */ jsx(RecordLines, {
|
|
401
|
+
}), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
|
|
389
402
|
lines: linesModel,
|
|
390
403
|
draft: linesDraft,
|
|
391
404
|
vatRate: Number(stored.record.vatRate ?? 21) / 100,
|
|
@@ -399,7 +412,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
399
412
|
className: "hidden flex-col items-center gap-2 md:flex",
|
|
400
413
|
children: formControls
|
|
401
414
|
}),
|
|
402
|
-
/* @__PURE__ */ jsx(RecordActions, {
|
|
415
|
+
!preview && /* @__PURE__ */ jsx(RecordActions, {
|
|
403
416
|
model,
|
|
404
417
|
stored,
|
|
405
418
|
recordKey,
|
|
@@ -408,11 +421,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
408
421
|
}),
|
|
409
422
|
firstSidebar === -1 && summary,
|
|
410
423
|
layout.map((item, index) => {
|
|
411
|
-
if (item.kind === "custom" && item.sidebar) return /* @__PURE__ */ jsx(CustomSection, {
|
|
424
|
+
if (item.kind === "custom" && item.sidebar && !preview) return /* @__PURE__ */ jsx(CustomSection, {
|
|
412
425
|
item,
|
|
413
426
|
scope
|
|
414
427
|
}, `custom:${index}`);
|
|
415
|
-
if (item.kind === "map" && item.sidebar) return /* @__PURE__ */ jsx(MapSection, {
|
|
428
|
+
if (item.kind === "map" && item.sidebar && !preview) return /* @__PURE__ */ jsx(MapSection, {
|
|
416
429
|
item,
|
|
417
430
|
record: stored.record
|
|
418
431
|
}, `map:${index}`);
|
|
@@ -422,10 +435,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
422
435
|
title: "Details",
|
|
423
436
|
rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
|
|
424
437
|
const field = name ? model.fields[name] : void 0;
|
|
425
|
-
|
|
438
|
+
if (!name || !field || !visible(name)) return [];
|
|
439
|
+
return [{
|
|
426
440
|
label: hint(name)?.label ?? humanize(name),
|
|
427
|
-
value: displayValue(field, stored.record[name], hint(name))
|
|
428
|
-
}]
|
|
441
|
+
value: known(name) ? displayValue(field, stored.record[name], hint(name)) : /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
442
|
+
}];
|
|
429
443
|
})
|
|
430
444
|
}),
|
|
431
445
|
related.records.length > 0 && /* @__PURE__ */ jsx(SidebarCard, {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { keys } from "../data/query-keys.js";
|
|
2
|
+
import { recordKey } from "../live/model-helpers.js";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
6
|
+
//#region src/app/record-preview.ts
|
|
7
|
+
/**
|
|
8
|
+
* A record as far as a list row knows it: the fields the row loaded, its ETag and permissions as the list gives them.
|
|
9
|
+
* `row` holds relations as their keys, as `displayRow` splits a loaded row.
|
|
10
|
+
*/
|
|
11
|
+
const recordPreview = (model, row) => ({
|
|
12
|
+
record: Object.fromEntries(Object.entries(row).filter(([name]) => Object.hasOwn(model.fields, name))),
|
|
13
|
+
etag: typeof row.etag === "string" ? row.etag : "",
|
|
14
|
+
...typeof row.permissions === "object" && row.permissions !== null && { permissions: row.permissions }
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Keeps what a list row knows of its record for the record's page, which shows it until the record has loaded
|
|
18
|
+
* (`useRecord`), so a record opened from a list shows its fields at once.
|
|
19
|
+
*/
|
|
20
|
+
const useSeedRecord = (model) => {
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
return useCallback((row) => queryClient.setQueryData(keys.recordPreview(model.name, encodeKey(recordKey(model, row))), recordPreview(model, row)), [queryClient, model]);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { recordPreview, useSeedRecord };
|
|
@@ -2,10 +2,10 @@ import { useIsXs } from "../../lib/use-media-query.js";
|
|
|
2
2
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
4
|
import { useAdminMeta } from "../meta-gate.js";
|
|
5
|
+
import { ErrorBanner } from "../error-banner.js";
|
|
5
6
|
import { displaysOf, loadedFields } from "../displays/list-displays.js";
|
|
6
7
|
import { defaultOrderBy } from "../list-order.js";
|
|
7
8
|
import { AppLink } from "../use-app-address.js";
|
|
8
|
-
import { ErrorBanner } from "../error-banner.js";
|
|
9
9
|
import { useLiveRows } from "../../live/use-live-rows.js";
|
|
10
10
|
import { viewAllAddress } from "../embedded-list/embedded-display.js";
|
|
11
11
|
import { FilterChipList } from "../searches/filter-chip-list.js";
|
|
@@ -16,12 +16,12 @@ import { ArrowRight } from "lucide-react";
|
|
|
16
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { createWhere, parseFilter, printFilter } from "@protobase/schema";
|
|
18
18
|
//#region src/app/record-row/record-row-body.tsx
|
|
19
|
-
const where = createWhere();
|
|
19
|
+
const where$1 = createWhere();
|
|
20
20
|
const scoped = (filter, scope) => {
|
|
21
21
|
if (!scope) return filter;
|
|
22
22
|
const parsed = parseFilter(filter);
|
|
23
23
|
if (!parsed.ok) throw new Error(`The filter cannot be read: ${parsed.errors.map((error) => error.message).join("; ")}`);
|
|
24
|
-
return printFilter(parsed.ast ? where.and(parsed.ast, scope) : scope);
|
|
24
|
+
return printFilter(parsed.ast ? where$1.and(parsed.ast, scope) : scope);
|
|
25
25
|
};
|
|
26
26
|
/** The display a row draws its cards with: the one named, else the view's first cards display, else a card of the title. */
|
|
27
27
|
const rowDisplay = (view, name) => {
|
|
@@ -26,6 +26,7 @@ const RecordRow = ({ model, view, display, source, label, isNew }) => {
|
|
|
26
26
|
total,
|
|
27
27
|
rowAt: source.rowAt,
|
|
28
28
|
loadRows: source.loadRows,
|
|
29
|
+
revision: source.revision,
|
|
29
30
|
latencyMs: 0
|
|
30
31
|
}, scroll.start, Math.max(scroll.start, scroll.end - 1));
|
|
31
32
|
const indexes = Array.from({ length: scroll.end - scroll.start }, (_, offset) => scroll.start + offset);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
3
|
import { useAdminMeta } from "../meta-gate.js";
|
|
4
|
-
import { listedResource } from "../current-address.js";
|
|
5
4
|
import { ErrorBanner } from "../error-banner.js";
|
|
5
|
+
import { listedResource } from "../current-address.js";
|
|
6
6
|
import { savedScope } from "../saved/saved-records.js";
|
|
7
7
|
import { useSavedRecords } from "../saved/use-saved-records.js";
|
|
8
8
|
import { RecordRowBody } from "./record-row-body.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/app/related-dots/card-dots-lines.ts
|
|
2
|
+
/**
|
|
3
|
+
* The lines of dots a card of `display` shows for a row, in the order it names their columns: those of the list's
|
|
4
|
+
* related columns that were loaded with the row and hold a record. A row loaded without them, as a map's or a saved
|
|
5
|
+
* search's, shows none.
|
|
6
|
+
*/
|
|
7
|
+
const cardDotsLines = (view, display, row) => (display.dots ?? []).flatMap((name) => {
|
|
8
|
+
const column = view?.list?.relatedColumns?.find((candidate) => candidate.name === name);
|
|
9
|
+
const records = row.record[name];
|
|
10
|
+
return column && Array.isArray(records) && records.length > 0 ? [{
|
|
11
|
+
column,
|
|
12
|
+
records
|
|
13
|
+
}] : [];
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { cardDotsLines };
|