@protobase/ui 0.21.2 → 0.21.3
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/current-address.js +2 -2
- package/dist/app/displays/display-controls.js +3 -4
- package/dist/app/displays/list-displays.js +58 -4
- package/dist/app/displays/record-card.js +2 -2
- package/dist/app/embedded-list/embedded-columns.js +1 -1
- package/dist/app/embedded-list/embedded-display.js +6 -2
- package/dist/app/embedded-list/embedded-list.js +1 -1
- package/dist/app/field-editor.js +1 -1
- package/dist/app/list-page.js +6 -4
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/record-page.js +2 -2
- package/dist/app/related-dots/related-dots-columns.js +2 -1
- package/dist/app/related-dots/related-dots.js +1 -1
- package/dist/data-grid/cell-text.js +30 -0
- package/dist/data-grid/cells.js +27 -35
- package/dist/data-grid/content-widths.js +81 -0
- package/dist/data-grid/data-grid.js +10 -5
- package/dist/data-grid/default-pins.js +13 -0
- package/dist/data-grid/pinned-storage.js +2 -1
- package/dist/data-grid/truncated-text.js +16 -0
- package/dist/data-grid/use-fitted-columns.js +28 -0
- package/dist/data-grid/use-pinned-columns.js +6 -5
- package/dist/live/columns-from-model.js +6 -6
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-wZRscg1o.js +69 -0
- package/dist/shell/assets/index-C1fzTv_X.css +2 -0
- package/dist/shell/assets/{index-7lQIOMrK.js → index-RHKnIOOP.js} +1 -1
- package/dist/shell/assets/{maplibre-DEejTZ7I.js → maplibre-DN_v_fcJ.js} +1 -1
- package/dist/shell/assets/{markdown-content-ZzP9i4q4.js → markdown-content-Be4vIiWm.js} +1 -1
- package/dist/shell/assets/{record-map-B-zsmXL2.js → record-map-CVeshts3.js} +1 -1
- package/dist/shell/assets/{records-map-Lj1MQgsW.js → records-map-ClnWq-RH.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-Bnd7T1j4.js → shared-protobase-ui-DWCBNg3c.js} +1 -1
- 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-BnuJWZWd.js +0 -69
- package/dist/shell/assets/index-B48ty05p.css +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultDisplay } from "./displays/list-displays.js";
|
|
1
|
+
import { defaultDisplay, listDefaultDisplay } from "./displays/list-displays.js";
|
|
2
2
|
import { defaultOrderBy } from "./list-order.js";
|
|
3
3
|
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/current-address.ts
|
|
@@ -22,7 +22,7 @@ const listedResource = (resources, views, resource) => {
|
|
|
22
22
|
};
|
|
23
23
|
/** What a list shows when its address names nothing: its default display for the screen, the list's sort, the filters in a bar. */
|
|
24
24
|
const listDefaults = (model, view, xs = false) => ({
|
|
25
|
-
display: defaultDisplay(view, xs),
|
|
25
|
+
display: model ? listDefaultDisplay(model, view, xs) : defaultDisplay(view, xs),
|
|
26
26
|
orderBy: model ? defaultOrderBy(model, view) : "",
|
|
27
27
|
filterLayout: "bar"
|
|
28
28
|
});
|
|
@@ -18,11 +18,10 @@ const icons = {
|
|
|
18
18
|
map: Map
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* The list page's switch between the table and the view's displays, as buttons or, on a phone, as a menu;
|
|
22
|
-
* list with only its table.
|
|
21
|
+
* The list page's switch between the table and the view's displays, or `choices`, as buttons or, on a phone, as a menu;
|
|
22
|
+
* nothing for a list with only its table.
|
|
23
23
|
*/
|
|
24
|
-
const DisplaySwitch = ({ view, value, onChange, menu = false }) => {
|
|
25
|
-
const choices = displayChoices(view);
|
|
24
|
+
const DisplaySwitch = ({ view, value, onChange, menu = false, choices = displayChoices(view) }) => {
|
|
26
25
|
if (choices.length < 2) return null;
|
|
27
26
|
const options = choices.map((choice) => ({
|
|
28
27
|
value: choice.name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
|
+
import { listColumnNames } from "../../live/columns-from-model.js";
|
|
3
4
|
import { displayValue } from "../../live/display-value.js";
|
|
4
5
|
import { photosKey } from "../../live/row-photos.js";
|
|
5
6
|
import { filledFields } from "../filled-fields.js";
|
|
@@ -18,6 +19,40 @@ const defaultDisplay = (view, xs, preferred) => {
|
|
|
18
19
|
const cards = displaysOf(view).filter((display) => display.kind === "cards");
|
|
19
20
|
return cards.find((display) => display.name === fallback)?.name ?? cards[0]?.name ?? fallback;
|
|
20
21
|
};
|
|
22
|
+
/** The name of the list a phone shows for a list without a cards display. */
|
|
23
|
+
const rowsDisplay = "rows";
|
|
24
|
+
/**
|
|
25
|
+
* The list a phone shows for a list without a cards display, as a cards display of a row per record: the title, the
|
|
26
|
+
* first enum column as a badge on the right, and the next two columns, booleans left out, on a muted line below.
|
|
27
|
+
* `undefined` for a view with a cards display, or a display of its own named `rows`.
|
|
28
|
+
*/
|
|
29
|
+
const phoneRows = (model, view) => {
|
|
30
|
+
if (displaysOf(view).some((display) => display.kind === "cards" || display.name === "rows")) return void 0;
|
|
31
|
+
const title = recordTitleField(model, view);
|
|
32
|
+
const candidates = listColumnNames(model, view).filter((name) => name !== title && model.fields[name] !== void 0);
|
|
33
|
+
const badge = candidates.find((name) => model.fields[name].type === "enum");
|
|
34
|
+
const footer = candidates.filter((name) => name !== badge && model.fields[name].type !== "boolean").slice(0, 2);
|
|
35
|
+
return {
|
|
36
|
+
kind: "cards",
|
|
37
|
+
name: rowsDisplay,
|
|
38
|
+
label: "List",
|
|
39
|
+
icon: "list",
|
|
40
|
+
title,
|
|
41
|
+
...badge !== void 0 && { badge },
|
|
42
|
+
footer
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const isPhoneRows = (view, display) => display.name === "rows" && !displaysOf(view).some((other) => other.name === "rows");
|
|
46
|
+
/**
|
|
47
|
+
* The display a list page opens in when its address names none: `defaultDisplay`, but below the `sm` breakpoint the
|
|
48
|
+
* phone's rows (`phoneRows`) in place of the table.
|
|
49
|
+
*/
|
|
50
|
+
const listDefaultDisplay = (model, view, xs) => {
|
|
51
|
+
const display = defaultDisplay(view, xs);
|
|
52
|
+
return xs && display === tableDisplay && phoneRows(model, view) ? rowsDisplay : display;
|
|
53
|
+
};
|
|
54
|
+
/** The display a list page shows for the address's `name`: `shownDisplay`, or the phone's rows when it names them, on any screen. */
|
|
55
|
+
const shownListDisplay = (model, view, name) => (name === "rows" ? phoneRows(model, view) : void 0) ?? shownDisplay(view, name);
|
|
21
56
|
/** The display a list shows for the address's `name`: `undefined` for the table, and the view's default for a name it does not have. */
|
|
22
57
|
const shownDisplay = (view, name) => {
|
|
23
58
|
if (name === tableDisplay) return void 0;
|
|
@@ -38,6 +73,15 @@ const displayChoices = (view) => [{
|
|
|
38
73
|
label: display.label ?? humanize(display.name),
|
|
39
74
|
icon: display.icon ?? defaultIcons[display.kind]
|
|
40
75
|
}))];
|
|
76
|
+
/** The list page's display switch: `displayChoices`, and below the `sm` breakpoint the phone's rows when it has them. */
|
|
77
|
+
const listDisplayChoices = (model, view, xs) => {
|
|
78
|
+
const choices = displayChoices(view);
|
|
79
|
+
return xs && phoneRows(model, view) ? [...choices, {
|
|
80
|
+
name: rowsDisplay,
|
|
81
|
+
label: "List",
|
|
82
|
+
icon: "list"
|
|
83
|
+
}] : choices;
|
|
84
|
+
};
|
|
41
85
|
/** The fields a cards display shows, its title the view's title field when it names none. */
|
|
42
86
|
const cardFields = (model, view, display) => ({
|
|
43
87
|
image: display.image,
|
|
@@ -101,9 +145,19 @@ const shownFields = (model, view, display) => {
|
|
|
101
145
|
].filter((name) => name !== void 0);
|
|
102
146
|
};
|
|
103
147
|
/** The fields to label the list's rows for: the table's columns and every display's, so one load serves them all. */
|
|
104
|
-
const loadedFields = (model, view, columns) =>
|
|
105
|
-
|
|
106
|
-
const
|
|
148
|
+
const loadedFields = (model, view, columns) => {
|
|
149
|
+
const rows = phoneRows(model, view);
|
|
150
|
+
const displays = rows ? [...displaysOf(view), rows] : displaysOf(view);
|
|
151
|
+
return [.../* @__PURE__ */ new Set([...columns, ...displays.flatMap((display) => shownFields(model, view, display))])];
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The sortable fields a display shows, for the sort menu that stands in for column headers; for the phone's rows, those
|
|
155
|
+
* of the table's columns and the title, as the table's headers sort by.
|
|
156
|
+
*/
|
|
157
|
+
const sortFields = (model, view, display) => {
|
|
158
|
+
const shown = isPhoneRows(view, display) ? [recordTitleField(model, view), ...listColumnNames(model, view)] : shownFields(model, view, display);
|
|
159
|
+
return [...new Set(shown)].filter((name) => model.fields[name]?.sortable);
|
|
160
|
+
};
|
|
107
161
|
const isLabelled = (value) => typeof value === "object" && value !== null && "id" in value && "name" in value;
|
|
108
162
|
/**
|
|
109
163
|
* A loaded row, its relations as `{ id, name }`, split into the record as the API gives it, the relations' names and
|
|
@@ -128,4 +182,4 @@ const displayRow = (model, row) => {
|
|
|
128
182
|
};
|
|
129
183
|
};
|
|
130
184
|
//#endregion
|
|
131
|
-
export { amountParts, cardFields, defaultDisplay, displayChoices, displayRow, displaysOf, filledAmount, filledMeta, loadedFields, mapCards, shownDisplay, shownFields, sortFields };
|
|
185
|
+
export { amountParts, cardFields, defaultDisplay, displayChoices, displayRow, displaysOf, filledAmount, filledMeta, listDefaultDisplay, listDisplayChoices, loadedFields, mapCards, phoneRows, rowsDisplay, shownDisplay, shownFields, shownListDisplay, sortFields };
|
|
@@ -3,14 +3,14 @@ import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
|
3
3
|
import { recordKey } from "../../live/model-helpers.js";
|
|
4
4
|
import { humanize } from "../../live/naming.js";
|
|
5
5
|
import { useProjectUi } from "../pages/project-ui.js";
|
|
6
|
+
import { markdownExcerpt } from "../../markdown/markdown-excerpt.js";
|
|
7
|
+
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
6
8
|
import { displayValue } from "../../live/display-value.js";
|
|
7
9
|
import { amountParts, cardFields, filledAmount, filledMeta } from "./list-displays.js";
|
|
8
10
|
import { AppLink } from "../use-app-address.js";
|
|
9
11
|
import { shownWidget } from "../field-types/shown-widget.js";
|
|
10
12
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
13
|
import { Markdown } from "../../markdown/markdown.js";
|
|
12
|
-
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
13
|
-
import { markdownExcerpt } from "../../markdown/markdown-excerpt.js";
|
|
14
14
|
import { ValueBadge } from "../value-badge.js";
|
|
15
15
|
import { useSeedRecord } from "../record-preview.js";
|
|
16
16
|
import { namedIcon } from "../named-icon.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
|
+
import { columnsFromModel } from "../../live/columns-from-model.js";
|
|
2
3
|
import { displayRow } from "../displays/list-displays.js";
|
|
3
4
|
import { scopeOf } from "../pages/use-layout-data.js";
|
|
4
|
-
import { columnsFromModel } from "../../live/columns-from-model.js";
|
|
5
5
|
import { FieldValue } from "../pages/field-value.js";
|
|
6
6
|
import { usePathValue } from "../pages/use-path-value.js";
|
|
7
7
|
import { withFieldTypeCells } from "../field-types/field-type-cells.js";
|
|
@@ -18,13 +18,17 @@ const embeddedDisplay = (view, xs, { picked, preferred }) => {
|
|
|
18
18
|
return defaultDisplay(view, xs, preferred !== void 0 && names.includes(preferred) ? preferred : void 0);
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* The resource's list with the embedded list's filter, order and display, leaving out what the list does by default
|
|
21
|
+
* The resource's list with the embedded list's filter, order and display, leaving out what the list does by default (an
|
|
22
|
+
* embedded list's own default display among it, so a phone opens the list in its rows rather than the table), and
|
|
22
23
|
* the saved search whose results it shows, if any. `resource` names another list of the same records, such as the
|
|
23
24
|
* favorites that list a user's saved listings.
|
|
24
25
|
*/
|
|
25
26
|
const viewAllAddress = (model, view, xs, { resource = model.name, ...list }) => changedList({
|
|
26
27
|
kind: "list",
|
|
27
28
|
resource
|
|
28
|
-
}, list,
|
|
29
|
+
}, list, {
|
|
30
|
+
...listDefaults(model, view, xs),
|
|
31
|
+
display: defaultDisplay(view, xs)
|
|
32
|
+
});
|
|
29
33
|
//#endregion
|
|
30
34
|
export { embeddedDisplay, embeddedDisplayKey, readPickedDisplay, viewAllAddress, writePickedDisplay };
|
|
@@ -5,10 +5,10 @@ import { humanize } from "../../live/naming.js";
|
|
|
5
5
|
import { useProjectUi } from "../pages/project-ui.js";
|
|
6
6
|
import { useAdminMeta } from "../meta-gate.js";
|
|
7
7
|
import { ErrorBanner } from "../error-banner.js";
|
|
8
|
+
import { listColumnNames } from "../../live/columns-from-model.js";
|
|
8
9
|
import { displayRow, loadedFields, shownDisplay } from "../displays/list-displays.js";
|
|
9
10
|
import { defaultOrderBy, parseSort, sortToOrderBy } from "../list-order.js";
|
|
10
11
|
import { AppLink, useAppAddress } from "../use-app-address.js";
|
|
11
|
-
import { listColumnNames } from "../../live/columns-from-model.js";
|
|
12
12
|
import { DataGrid } from "../../data-grid/data-grid.js";
|
|
13
13
|
import { viewAllAddress } from "./embedded-display.js";
|
|
14
14
|
import { BoardDisplay } from "../displays/board-display.js";
|
package/dist/app/field-editor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { humanize } from "../live/naming.js";
|
|
|
6
6
|
import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
|
|
7
7
|
import { Switch } from "../primitives/switch.js";
|
|
8
8
|
import { Textarea } from "../primitives/textarea.js";
|
|
9
|
+
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
9
10
|
import { displayValue } from "../live/display-value.js";
|
|
10
11
|
import { AppLink } from "./use-app-address.js";
|
|
11
12
|
import { RelationPicker } from "./relation-picker.js";
|
|
@@ -15,7 +16,6 @@ import { useShownWidget } from "./field-types/shown-widget.js";
|
|
|
15
16
|
import { WidgetPart } from "./field-types/widget-part.js";
|
|
16
17
|
import { Markdown } from "../markdown/markdown.js";
|
|
17
18
|
import { MarkdownEditor } from "../markdown/markdown-editor.js";
|
|
18
|
-
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
19
19
|
import { ArrowUpRight } from "lucide-react";
|
|
20
20
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
21
21
|
import { enumLabel } from "@protobase/schema";
|
package/dist/app/list-page.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
1
|
+
import { useIsDesktop, useIsXs } from "../lib/use-media-query.js";
|
|
2
2
|
import { usePageScroll } from "../app-shell/page-scroll.js";
|
|
3
3
|
import { PageHeader } from "../app-shell/page-header.js";
|
|
4
4
|
import { useAuth } from "../auth/auth-provider.js";
|
|
@@ -9,12 +9,12 @@ import { useProjectUi } from "./pages/project-ui.js";
|
|
|
9
9
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
10
10
|
import { Sheet } from "../primitives/sheet.js";
|
|
11
11
|
import { ErrorBanner } from "./error-banner.js";
|
|
12
|
-
import {
|
|
12
|
+
import { columnsFromModel, listColumnNames } from "../live/columns-from-model.js";
|
|
13
|
+
import { displayRow, listDisplayChoices, loadedFields, shownListDisplay } from "./displays/list-displays.js";
|
|
13
14
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
14
15
|
import { listedResource } from "./current-address.js";
|
|
15
16
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
16
17
|
import { can, rowAllows } from "./permissions.js";
|
|
17
|
-
import { columnsFromModel, listColumnNames } from "../live/columns-from-model.js";
|
|
18
18
|
import { pinnedStorageKey } from "../data-grid/pinned-storage.js";
|
|
19
19
|
import { DataGrid } from "../data-grid/data-grid.js";
|
|
20
20
|
import { useLiveRows } from "../live/use-live-rows.js";
|
|
@@ -131,6 +131,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
131
131
|
const extras = parsed.ok ? parsed.extras : [];
|
|
132
132
|
const { orderBy, filterLayout: layout } = address;
|
|
133
133
|
const desktop = useIsDesktop();
|
|
134
|
+
const xs = useIsXs();
|
|
134
135
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
135
136
|
const scoped = scope ? [...extras, scope] : extras;
|
|
136
137
|
const config = useFilterConfig({
|
|
@@ -162,7 +163,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
162
163
|
orderBy,
|
|
163
164
|
related
|
|
164
165
|
});
|
|
165
|
-
const display =
|
|
166
|
+
const display = shownListDisplay(model, view, address.display);
|
|
166
167
|
const permissions = usePermissions(resource);
|
|
167
168
|
const handOrder = scope ? void 0 : listHandOrder(model, permissions);
|
|
168
169
|
const [sorting, setSorting] = useState(false);
|
|
@@ -321,6 +322,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
321
322
|
});
|
|
322
323
|
const displaySwitch = (menu) => !handSorting && /* @__PURE__ */ jsx(DisplaySwitch, {
|
|
323
324
|
view,
|
|
325
|
+
choices: listDisplayChoices(model, view, xs),
|
|
324
326
|
value: display?.name ?? tableDisplay,
|
|
325
327
|
onChange: (name) => set({ display: name }),
|
|
326
328
|
menu
|
|
@@ -3,13 +3,13 @@ import { keys } from "../../data/query-keys.js";
|
|
|
3
3
|
import { useClient } from "../../data/api-provider.js";
|
|
4
4
|
import { useAdminMeta } from "../meta-gate.js";
|
|
5
5
|
import { fileShown, shownImage } from "../../live/file-values.js";
|
|
6
|
+
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
6
7
|
import { displayValue } from "../../live/display-value.js";
|
|
7
8
|
import { AppLink } from "../use-app-address.js";
|
|
8
9
|
import { FileImage } from "../../live/file-image.js";
|
|
9
10
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
10
11
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
12
|
import { Markdown } from "../../markdown/markdown.js";
|
|
12
|
-
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
13
13
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
14
14
|
import { ValueBadge } from "../value-badge.js";
|
|
15
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
package/dist/app/record-page.js
CHANGED
|
@@ -22,16 +22,16 @@ import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
|
|
|
22
22
|
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
23
23
|
import { isChosen, keyValue } from "./create/create-values.js";
|
|
24
24
|
import { ErrorBanner } from "./error-banner.js";
|
|
25
|
+
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
26
|
+
import { toneFor } from "../live/columns-from-model.js";
|
|
25
27
|
import { displayValue } from "../live/display-value.js";
|
|
26
28
|
import { filledFields } from "./filled-fields.js";
|
|
27
29
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
28
30
|
import { shownWidget } from "./field-types/shown-widget.js";
|
|
29
31
|
import { WidgetPart } from "./field-types/widget-part.js";
|
|
30
32
|
import { Markdown } from "../markdown/markdown.js";
|
|
31
|
-
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
32
33
|
import { FieldEditor } from "./field-editor.js";
|
|
33
34
|
import { can, fieldAccess } from "./permissions.js";
|
|
34
|
-
import { toneFor } from "../live/columns-from-model.js";
|
|
35
35
|
import { ValueBadge } from "./value-badge.js";
|
|
36
36
|
import { FieldValue } from "./pages/field-value.js";
|
|
37
37
|
import { fieldMessage } from "./field-errors.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
|
-
import { headerWidth
|
|
2
|
+
import { headerWidth } from "../../data-grid/content-widths.js";
|
|
3
|
+
import { listColumnNames } from "../../live/columns-from-model.js";
|
|
3
4
|
import { RelatedDots } from "./related-dots.js";
|
|
4
5
|
import { RecordCard } from "../displays/record-card.js";
|
|
5
6
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -2,10 +2,10 @@ import { cn } from "../../lib/cn.js";
|
|
|
2
2
|
import { toneDots } from "../../primitives/badge.js";
|
|
3
3
|
import { recordTitleField } from "../../live/model-helpers.js";
|
|
4
4
|
import { humanize } from "../../live/naming.js";
|
|
5
|
+
import { toneFor } from "../../live/columns-from-model.js";
|
|
5
6
|
import { displayValue } from "../../live/display-value.js";
|
|
6
7
|
import { displayRow } from "../displays/list-displays.js";
|
|
7
8
|
import { AppLink } from "../use-app-address.js";
|
|
8
|
-
import { toneFor } from "../../live/columns-from-model.js";
|
|
9
9
|
import { valueColor } from "../../live/value-color.js";
|
|
10
10
|
import { placeCard } from "./card-place.js";
|
|
11
11
|
import { dotCard } from "./dot-card.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatMoney } from "../format/money.js";
|
|
2
|
+
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
3
|
+
import { formatDate, formatDateTime } from "../format/date.js";
|
|
4
|
+
import { userById } from "../mocks/users.js";
|
|
5
|
+
import { fileShown } from "../live/file-values.js";
|
|
6
|
+
import { enumLabel } from "@protobase/schema";
|
|
7
|
+
//#region src/data-grid/cell-text.ts
|
|
8
|
+
/** A relation cell's text: the related record's name when the row carries it, else its key. */
|
|
9
|
+
const relationLabel = (value) => typeof value === "object" && value !== null && "name" in value ? String(value.name) : String(value);
|
|
10
|
+
/** The text a cell of `spec` shows for `value`; `undefined` when it shows only an icon, such as a ticked boolean. */
|
|
11
|
+
const cellText = (spec, value) => {
|
|
12
|
+
if (value === null || value === void 0) return "—";
|
|
13
|
+
switch (spec.kind) {
|
|
14
|
+
case "text":
|
|
15
|
+
case "id": return String(value);
|
|
16
|
+
case "money": return `${formatMoney(Number(value), spec.currency, spec.decimals)}${spec.suffix ?? ""}`;
|
|
17
|
+
case "date": return formatDate(Number(value));
|
|
18
|
+
case "datetime": return formatDateTime(Number(value));
|
|
19
|
+
case "number": return `${formatInt(Number(value))}${spec.suffix ?? ""}`;
|
|
20
|
+
case "signed": return `${formatSigned(Number(value))}${spec.suffix ?? ""}`;
|
|
21
|
+
case "percent": return Number(value) === 0 ? "—" : formatPercent(Number(value));
|
|
22
|
+
case "status": return enumLabel(String(value), spec.labels);
|
|
23
|
+
case "relation": return relationLabel(value);
|
|
24
|
+
case "boolean": return value ? void 0 : "No";
|
|
25
|
+
case "file": return fileShown(value)?.name ?? "—";
|
|
26
|
+
case "user": return userById(String(value)).name;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { cellText, relationLabel };
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -1,66 +1,61 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Avatar } from "../primitives/avatar.js";
|
|
3
3
|
import { Badge } from "../primitives/badge.js";
|
|
4
|
-
import { formatMoney } from "../format/money.js";
|
|
5
|
-
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
6
|
-
import { formatDate, formatDateTime } from "../format/date.js";
|
|
7
4
|
import { userById } from "../mocks/users.js";
|
|
8
5
|
import { fileShown, shownImage } from "../live/file-values.js";
|
|
6
|
+
import { cellText } from "./cell-text.js";
|
|
9
7
|
import { FileImage } from "../live/file-image.js";
|
|
10
8
|
import { valueColor } from "../live/value-color.js";
|
|
9
|
+
import { TruncatedText } from "./truncated-text.js";
|
|
11
10
|
import { Check, Paperclip } from "lucide-react";
|
|
12
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
-
import { enumLabel } from "@protobase/schema";
|
|
14
12
|
//#region src/data-grid/cells.tsx
|
|
15
13
|
const muted = (text) => /* @__PURE__ */ jsx("span", {
|
|
16
14
|
className: "text-faint-foreground",
|
|
17
15
|
children: text
|
|
18
16
|
});
|
|
19
|
-
const relationLabel = (value) => typeof value === "object" && value !== null && "name" in value ? String(value.name) : String(value);
|
|
20
17
|
const renderCell = (spec, value) => {
|
|
21
18
|
if (value === null || value === void 0) return muted("—");
|
|
19
|
+
const text = cellText(spec, value) ?? "";
|
|
22
20
|
switch (spec.kind) {
|
|
23
|
-
case "text": return /* @__PURE__ */ jsx(
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
case "text": return /* @__PURE__ */ jsx(TruncatedText, { text });
|
|
22
|
+
case "id": return /* @__PURE__ */ jsx(TruncatedText, {
|
|
23
|
+
text,
|
|
24
|
+
className: "font-mono text-xs text-muted-foreground"
|
|
26
25
|
});
|
|
27
|
-
case "
|
|
28
|
-
className: "truncate font-mono text-xs text-muted-foreground",
|
|
29
|
-
children: String(value)
|
|
30
|
-
});
|
|
31
|
-
case "money": return /* @__PURE__ */ jsxs("span", {
|
|
26
|
+
case "money": return /* @__PURE__ */ jsx("span", {
|
|
32
27
|
className: "tabular-nums",
|
|
33
|
-
children:
|
|
28
|
+
children: text
|
|
34
29
|
});
|
|
35
30
|
case "date": return /* @__PURE__ */ jsx("span", {
|
|
36
31
|
className: "tabular-nums text-muted-foreground",
|
|
37
|
-
children:
|
|
32
|
+
children: text
|
|
38
33
|
});
|
|
39
34
|
case "datetime": return /* @__PURE__ */ jsx("span", {
|
|
40
35
|
className: "tabular-nums text-muted-foreground",
|
|
41
|
-
children:
|
|
36
|
+
children: text
|
|
42
37
|
});
|
|
43
|
-
case "number": return /* @__PURE__ */
|
|
38
|
+
case "number": return /* @__PURE__ */ jsx("span", {
|
|
44
39
|
className: "tabular-nums",
|
|
45
|
-
children:
|
|
40
|
+
children: text
|
|
46
41
|
});
|
|
47
|
-
case "signed": return /* @__PURE__ */
|
|
42
|
+
case "signed": return /* @__PURE__ */ jsx("span", {
|
|
48
43
|
className: cn("tabular-nums", Number(value) < 0 ? "text-danger-text" : "text-success-text"),
|
|
49
|
-
children:
|
|
44
|
+
children: text
|
|
50
45
|
});
|
|
51
46
|
case "percent": return Number(value) === 0 ? muted("—") : /* @__PURE__ */ jsx("span", {
|
|
52
47
|
className: "tabular-nums",
|
|
53
|
-
children:
|
|
48
|
+
children: text
|
|
54
49
|
});
|
|
55
50
|
case "status": return /* @__PURE__ */ jsx(Badge, {
|
|
56
51
|
tone: spec.tones?.[String(value)] ?? "neutral",
|
|
57
52
|
color: valueColor(spec.colors, value),
|
|
58
53
|
shape: spec.shape,
|
|
59
|
-
children:
|
|
54
|
+
children: text
|
|
60
55
|
});
|
|
61
|
-
case "relation": return /* @__PURE__ */ jsx(
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
case "relation": return /* @__PURE__ */ jsx(TruncatedText, {
|
|
57
|
+
text,
|
|
58
|
+
className: "font-medium text-primary-text hover:underline"
|
|
64
59
|
});
|
|
65
60
|
case "boolean": return value ? /* @__PURE__ */ jsx("span", {
|
|
66
61
|
className: "inline-flex size-4 items-center justify-center rounded-full bg-success-soft text-success-text",
|
|
@@ -72,9 +67,9 @@ const renderCell = (spec, value) => {
|
|
|
72
67
|
case "file": {
|
|
73
68
|
const file = fileShown(value);
|
|
74
69
|
if (!file) return muted("—");
|
|
75
|
-
if ("missing" in file) return /* @__PURE__ */ jsx(
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
if ("missing" in file) return /* @__PURE__ */ jsx(TruncatedText, {
|
|
71
|
+
text: file.name,
|
|
72
|
+
className: "text-danger-text"
|
|
78
73
|
});
|
|
79
74
|
const image = shownImage(value, {
|
|
80
75
|
width: 20,
|
|
@@ -85,10 +80,7 @@ const renderCell = (spec, value) => {
|
|
|
85
80
|
children: [image ? /* @__PURE__ */ jsx(FileImage, {
|
|
86
81
|
image,
|
|
87
82
|
className: "size-5 shrink-0 rounded-sm object-cover"
|
|
88
|
-
}) : /* @__PURE__ */ jsx(Paperclip, { className: "size-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx(
|
|
89
|
-
className: "truncate",
|
|
90
|
-
children: file.name
|
|
91
|
-
})]
|
|
83
|
+
}) : /* @__PURE__ */ jsx(Paperclip, { className: "size-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx(TruncatedText, { text: file.name })]
|
|
92
84
|
});
|
|
93
85
|
}
|
|
94
86
|
case "user": {
|
|
@@ -100,9 +92,9 @@ const renderCell = (spec, value) => {
|
|
|
100
92
|
hue: user.hue,
|
|
101
93
|
size: "xs",
|
|
102
94
|
className: "ring-0"
|
|
103
|
-
}), /* @__PURE__ */ jsx(
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
}), /* @__PURE__ */ jsx(TruncatedText, {
|
|
96
|
+
text: user.name,
|
|
97
|
+
className: "text-muted-foreground"
|
|
106
98
|
})]
|
|
107
99
|
});
|
|
108
100
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cellText } from "./cell-text.js";
|
|
2
|
+
//#region src/data-grid/content-widths.ts
|
|
3
|
+
/** Room for a header at 12px plus cell padding, the sort arrow and the column menu button. */
|
|
4
|
+
const headerWidth = (header, sortable) => Math.ceil(header.length * 7.2) + 24 + (sortable ? 16 : 0) + 28;
|
|
5
|
+
/** A status badge: dot, padding and the longest label. */
|
|
6
|
+
const badgeWidth = (labels) => Math.ceil(Math.max(0, ...labels.map((label) => label.length)) * 7.2) + 56;
|
|
7
|
+
/** Pixels per character: body text, the small monospace of identifiers, and tabular numbers. */
|
|
8
|
+
const charWidth = {
|
|
9
|
+
body: 7.5,
|
|
10
|
+
mono: 7.2,
|
|
11
|
+
numeric: 8
|
|
12
|
+
};
|
|
13
|
+
const cellPadding = 24;
|
|
14
|
+
const iconRoom = 24;
|
|
15
|
+
const checkWidth = 16;
|
|
16
|
+
/** Values cut short would read wrong, so their columns take their measured width whole. */
|
|
17
|
+
const wholeKinds = /* @__PURE__ */ new Set([
|
|
18
|
+
"status",
|
|
19
|
+
"money",
|
|
20
|
+
"number",
|
|
21
|
+
"signed",
|
|
22
|
+
"percent",
|
|
23
|
+
"date",
|
|
24
|
+
"datetime",
|
|
25
|
+
"boolean"
|
|
26
|
+
]);
|
|
27
|
+
const numericKinds = /* @__PURE__ */ new Set([
|
|
28
|
+
"money",
|
|
29
|
+
"number",
|
|
30
|
+
"signed",
|
|
31
|
+
"percent",
|
|
32
|
+
"date",
|
|
33
|
+
"datetime"
|
|
34
|
+
]);
|
|
35
|
+
const minWeight = 80;
|
|
36
|
+
const maxWeight = 400;
|
|
37
|
+
const maxIdWidth = 240;
|
|
38
|
+
const perChar = (kind) => kind === "id" ? charWidth.mono : numericKinds.has(kind) ? charWidth.numeric : charWidth.body;
|
|
39
|
+
const valueOf = (spec, row) => spec.value ? spec.value(row) : row[spec.id];
|
|
40
|
+
/** The width the widest of `rows`' values of `spec` takes, cell padding included. */
|
|
41
|
+
const contentWidth = (spec, rows) => {
|
|
42
|
+
const texts = rows.map((row) => cellText(spec, valueOf(spec, row)));
|
|
43
|
+
if (spec.kind === "status") return badgeWidth(texts.map((text) => text ?? ""));
|
|
44
|
+
const icon = spec.kind === "user" || spec.kind === "file" ? iconRoom : 0;
|
|
45
|
+
return Math.max(0, ...texts.map((text) => text === void 0 ? checkWidth : Math.ceil(text.length * perChar(spec.kind)))) + icon + cellPadding;
|
|
46
|
+
};
|
|
47
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
48
|
+
const fitted = (spec, content) => {
|
|
49
|
+
if (wholeKinds.has(spec.kind) && !spec.title) return {
|
|
50
|
+
...spec,
|
|
51
|
+
fixed: true,
|
|
52
|
+
width: Math.max(spec.minWidth ?? 0, content)
|
|
53
|
+
};
|
|
54
|
+
const weighted = {
|
|
55
|
+
...spec,
|
|
56
|
+
fixed: false,
|
|
57
|
+
width: clamp(content, minWeight, maxWeight)
|
|
58
|
+
};
|
|
59
|
+
if (spec.kind !== "id") return weighted;
|
|
60
|
+
return {
|
|
61
|
+
...weighted,
|
|
62
|
+
minWidth: Math.max(headerWidth(spec.header, spec.sortable ?? false), Math.min(content, maxIdWidth))
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Columns sized by the values in `rows`: values that must stay whole get their widest value's width, text a share of the
|
|
67
|
+
* leftover space as large as its widest value, and the title column the largest share. Columns drawn by their own
|
|
68
|
+
* `render` keep their width, as their content cannot be estimated.
|
|
69
|
+
*/
|
|
70
|
+
const fitColumns = (specs, rows) => {
|
|
71
|
+
if (rows.length === 0) return specs;
|
|
72
|
+
const sized = specs.map((spec) => spec.render ? spec : fitted(spec, contentWidth(spec, rows)));
|
|
73
|
+
const shares = sized.filter((spec, index) => !spec.fixed && !spec.title && !specs[index].render).map((spec) => spec.width);
|
|
74
|
+
const largest = Math.max(0, ...shares);
|
|
75
|
+
return sized.map((spec, index) => spec.title && !specs[index].render ? {
|
|
76
|
+
...spec,
|
|
77
|
+
width: Math.max(spec.width, largest)
|
|
78
|
+
} : spec);
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { badgeWidth, contentWidth, fitColumns, headerWidth };
|
|
@@ -6,6 +6,8 @@ import { RowMenu, availableActions } from "./row-menu.js";
|
|
|
6
6
|
import { desktopMinScale, gridMinWidth, gutterWidthFor } from "./grid-template.js";
|
|
7
7
|
import { useHorizontalScroll } from "./use-horizontal-scroll.js";
|
|
8
8
|
import { usePinnedColumns } from "./use-pinned-columns.js";
|
|
9
|
+
import { defaultPins } from "./default-pins.js";
|
|
10
|
+
import { useFittedColumns } from "./use-fitted-columns.js";
|
|
9
11
|
import { GridHeader } from "./grid-header.js";
|
|
10
12
|
import { DataRow, SkeletonRow } from "./grid-row.js";
|
|
11
13
|
import { usePagedRows } from "./use-paged-rows.js";
|
|
@@ -20,7 +22,10 @@ const headerHeight = 32;
|
|
|
20
22
|
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable: selecting = true, reorder, fit = false, scroll = "own", className }) => {
|
|
21
23
|
const selectable = selecting && !reorder;
|
|
22
24
|
const rowHeight = compact ? 28 : 36;
|
|
23
|
-
const
|
|
25
|
+
const narrow = !useIsDesktop();
|
|
26
|
+
const pinsByDefault = (defaultPinned ?? (narrow ? [] : defaultPins(given))).join(" ");
|
|
27
|
+
const initialPins = useMemo(() => pinsByDefault ? pinsByDefault.split(" ") : [], [pinsByDefault]);
|
|
28
|
+
const [pinned, setPinned] = usePinnedColumns(pinKey, initialPins);
|
|
24
29
|
const columns = useMemo(() => {
|
|
25
30
|
return [...pinned.flatMap((id) => given.find((spec) => spec.id === id) ?? []), ...given.filter((spec) => !pinned.includes(spec.id))];
|
|
26
31
|
}, [given, pinned]);
|
|
@@ -60,7 +65,6 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
60
65
|
initialIndex: startIndex,
|
|
61
66
|
inset: headerHeight
|
|
62
67
|
});
|
|
63
|
-
const narrow = !useIsDesktop();
|
|
64
68
|
const gutter = narrow || !rowNumbers ? 40 : gutterWidthFor(total);
|
|
65
69
|
const topIndex = Math.floor(Math.max(0, top) / rowHeight);
|
|
66
70
|
useEffect(() => onScrollIndex?.(topIndex), [topIndex, onScrollIndex]);
|
|
@@ -89,6 +93,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
89
93
|
firstIndex,
|
|
90
94
|
lastIndex
|
|
91
95
|
]);
|
|
96
|
+
const sized = useFittedColumns(columns, data);
|
|
92
97
|
const table = useTable({
|
|
93
98
|
features,
|
|
94
99
|
columns: tableColumns,
|
|
@@ -145,7 +150,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
145
150
|
onOpenRow,
|
|
146
151
|
container: gridRef
|
|
147
152
|
});
|
|
148
|
-
const minWidth = gridMinWidth(
|
|
153
|
+
const minWidth = gridMinWidth(sized, gutter, trailing, minScale);
|
|
149
154
|
const own = scroll === "own";
|
|
150
155
|
const header = /* @__PURE__ */ jsx("div", {
|
|
151
156
|
ref: headerRef,
|
|
@@ -154,7 +159,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
154
159
|
style: { minWidth },
|
|
155
160
|
children: /* @__PURE__ */ jsx(GridHeader, {
|
|
156
161
|
cells: headerCells,
|
|
157
|
-
specs:
|
|
162
|
+
specs: sized,
|
|
158
163
|
gutter,
|
|
159
164
|
trailing,
|
|
160
165
|
minScale,
|
|
@@ -170,7 +175,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
170
175
|
});
|
|
171
176
|
const rowsInView = loaded.map(({ index, offset, row }) => {
|
|
172
177
|
const frame = {
|
|
173
|
-
specs:
|
|
178
|
+
specs: sized,
|
|
174
179
|
gutter,
|
|
175
180
|
index,
|
|
176
181
|
offset,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/data-grid/default-pins.ts
|
|
2
|
+
/**
|
|
3
|
+
* The columns pinned until the user chooses: the leading columns through the title column, when it is first or second.
|
|
4
|
+
* Pinned columns move to the front, so a title further right stays unpinned rather than reorder the list; pins only
|
|
5
|
+
* take effect once the grid is too wide and scrolls sideways.
|
|
6
|
+
*/
|
|
7
|
+
const defaultPins = (specs) => {
|
|
8
|
+
const index = specs.findIndex((spec) => spec.title);
|
|
9
|
+
if (index < 0 || index > 1) return [];
|
|
10
|
+
return specs.slice(0, index + 1).map((spec) => spec.id);
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { defaultPins };
|
|
@@ -8,6 +8,7 @@ const readPinned = (storage, key) => guarded(() => {
|
|
|
8
8
|
const parsed = JSON.parse(raw);
|
|
9
9
|
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed : void 0;
|
|
10
10
|
}, void 0);
|
|
11
|
-
|
|
11
|
+
/** Keeps an empty list too, so a column pinned by default and then unpinned stays unpinned. */
|
|
12
|
+
const writePinned = (storage, key, pinned) => guarded(() => storage.setItem(key, JSON.stringify(pinned)), void 0);
|
|
12
13
|
//#endregion
|
|
13
14
|
export { pinnedStorageKey, readPinned, writePinned };
|