@protobase/ui 0.11.1 → 0.12.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/app.js +1 -1
- package/dist/app/custom-section.js +2 -2
- package/dist/app/displays/cards-display.js +57 -32
- package/dist/app/displays/list-displays.d.ts +4 -0
- package/dist/app/field-editor.js +31 -2
- package/dist/app/field-types/field-type-cells.js +26 -0
- package/dist/app/field-types/field-widget.d.ts +28 -0
- package/dist/app/field-types/shown-widget.js +21 -0
- package/dist/app/field-types/widget-part.js +33 -0
- package/dist/app/list-page.js +8 -1
- package/dist/app/pages/field-value.js +19 -2
- package/dist/app/pages/merge-ui.d.ts +3 -3
- package/dist/app/pages/merge-ui.js +5 -3
- package/dist/app/pages/project-ui.d.ts +10 -6
- package/dist/app/pages/project-ui.js +3 -3
- package/dist/app/record-page.js +16 -4
- package/dist/data-grid/columns.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/live/field-values.js +9 -1
- package/dist/package.js +1 -1
- package/dist/plugins/plugin-ui.js +10 -2
- package/dist/shell/assets/app-GUjJKojE.css +1 -0
- package/dist/shell/assets/app-heHm3H5D.js +65 -0
- package/dist/shell/assets/index-B6FP_5El.js +9 -0
- package/dist/shell/assets/index-DheSSTn2.css +2 -0
- package/dist/shell/assets/jsx-runtime-NZYk81nU.js +1 -0
- package/dist/shell/assets/react-Cvdyeg_0.js +1 -0
- package/dist/shell/assets/react-dom-B-VxWsjA.js +1 -0
- package/dist/shell/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/shell/assets/shared-protobase-ui-C4sf7CCI.js +1 -0
- package/dist/shell/assets/shared-react-DhkZ2M4l.js +1 -0
- package/dist/shell/assets/shared-react-dom-D508a1cq.js +1 -0
- package/dist/shell/assets/shared-react-jsx-runtime-xPQrWk0g.js +1 -0
- package/dist/shell/index.html +24 -0
- package/dist/shell/protobase.shell.json +8 -0
- package/dist/styles.css +1 -62
- package/dist/theme.css +9 -6
- package/dist/tokens.css +71 -0
- package/package.json +6 -6
package/dist/app/app.js
CHANGED
|
@@ -18,8 +18,8 @@ import { breadcrumbFor } from "./breadcrumb-for.js";
|
|
|
18
18
|
import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
|
|
19
19
|
import { Router, useRouter } from "./router.js";
|
|
20
20
|
import { useAppAddress } from "./use-app-address.js";
|
|
21
|
-
import { CreatePage } from "./create/create-page.js";
|
|
22
21
|
import { CustomBoundary } from "./pages/custom-boundary.js";
|
|
22
|
+
import { CreatePage } from "./create/create-page.js";
|
|
23
23
|
import { ListPage } from "./list-page.js";
|
|
24
24
|
import { MembersPage } from "../organizations/members-page.js";
|
|
25
25
|
import { InvitationsPage } from "../organizations/invitations-page.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
2
|
-
import { Section } from "../record-view/section.js";
|
|
3
|
-
import { SidebarCard } from "../record-view/record-sidebar.js";
|
|
4
2
|
import { Problem } from "./pages/problem.js";
|
|
5
3
|
import { CustomBoundary } from "./pages/custom-boundary.js";
|
|
4
|
+
import { Section } from "../record-view/section.js";
|
|
5
|
+
import { SidebarCard } from "../record-view/record-sidebar.js";
|
|
6
6
|
import { RecordScope } from "./pages/record-scope.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { unmarkFieldRefs } from "@protobase/schema";
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { Badge } from "../../primitives/badge.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
|
+
import { useProjectUi } from "../pages/project-ui.js";
|
|
3
4
|
import { AppLink } from "../use-app-address.js";
|
|
4
5
|
import { displayValue } from "../../live/display-value.js";
|
|
6
|
+
import { shownWidget } from "../field-types/shown-widget.js";
|
|
7
|
+
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
8
|
+
import { WidgetPart } from "../field-types/widget-part.js";
|
|
5
9
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
6
10
|
import { toneFor } from "../../live/columns-from-model.js";
|
|
7
11
|
import { DisplayFooter, DisplayState } from "./display-frame.js";
|
|
8
12
|
import { cardFields } from "./list-displays.js";
|
|
9
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
14
|
//#region src/app/displays/cards-display.tsx
|
|
11
15
|
const grid = "grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-3";
|
|
12
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A card per record, opening its page: the title over the subtitle, the badge beside it and small meta fields below;
|
|
18
|
+
* or, with a `card` component, what it draws of the row, the built-in content again when it is missing or throws.
|
|
19
|
+
*/
|
|
13
20
|
const CardsDisplay = ({ model, view, display, list }) => {
|
|
14
21
|
const { title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
15
22
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
@@ -18,6 +25,19 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
18
25
|
if (!field) return "";
|
|
19
26
|
return row.labels[name] ?? displayValue(field, row.record[name], view?.fields[name]);
|
|
20
27
|
};
|
|
28
|
+
const { fields: fieldTypes, components } = useProjectUi();
|
|
29
|
+
const Card = display.card === void 0 ? void 0 : components?.[display.card];
|
|
30
|
+
const shownValue = (row, name) => {
|
|
31
|
+
const field = model.fields[name];
|
|
32
|
+
const shown = field && shownWidget(fieldTypes, field, view?.fields[name]);
|
|
33
|
+
return shown ? /* @__PURE__ */ jsx(WidgetPart, {
|
|
34
|
+
shown,
|
|
35
|
+
part: "display",
|
|
36
|
+
field,
|
|
37
|
+
value: row.record[name],
|
|
38
|
+
fallback: text(row, name)
|
|
39
|
+
}) : text(row, name);
|
|
40
|
+
};
|
|
21
41
|
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
22
42
|
return /* @__PURE__ */ jsxs(DisplayState, {
|
|
23
43
|
list,
|
|
@@ -31,42 +51,47 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
31
51
|
"aria-label": view?.names?.plural ?? humanize(model.name),
|
|
32
52
|
children: list.rows.map((row) => {
|
|
33
53
|
const status = badge === void 0 ? void 0 : row.record[badge];
|
|
34
|
-
|
|
54
|
+
const builtIn = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
55
|
+
/* @__PURE__ */ jsxs("span", {
|
|
56
|
+
className: "flex items-start justify-between gap-2",
|
|
57
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
58
|
+
className: "min-w-0 truncate font-medium",
|
|
59
|
+
children: shownValue(row, title)
|
|
60
|
+
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(Badge, {
|
|
61
|
+
tone: toneFor(String(status)),
|
|
62
|
+
children: text(row, badge)
|
|
63
|
+
})]
|
|
64
|
+
}),
|
|
65
|
+
subtitle !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
66
|
+
className: "truncate text-muted-foreground",
|
|
67
|
+
children: shownValue(row, subtitle)
|
|
68
|
+
}),
|
|
69
|
+
meta.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
70
|
+
className: "mt-auto flex flex-wrap gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
71
|
+
children: meta.map((name) => /* @__PURE__ */ jsxs("div", {
|
|
72
|
+
className: "flex gap-1",
|
|
73
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
74
|
+
className: "sr-only",
|
|
75
|
+
children: label(name)
|
|
76
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
77
|
+
className: "tabular-nums",
|
|
78
|
+
children: shownValue(row, name)
|
|
79
|
+
})]
|
|
80
|
+
}, name))
|
|
81
|
+
})
|
|
82
|
+
] });
|
|
83
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AppLink, {
|
|
35
84
|
to: {
|
|
36
85
|
kind: "record",
|
|
37
86
|
resource: model.name,
|
|
38
87
|
key: row.key
|
|
39
88
|
},
|
|
40
89
|
className: "flex h-full flex-col gap-1.5 rounded-lg border border-border bg-background p-3 text-[13px] hover:border-border-strong hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
|
|
41
|
-
children:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
children: text(row, title)
|
|
47
|
-
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(Badge, {
|
|
48
|
-
tone: toneFor(String(status)),
|
|
49
|
-
children: text(row, badge)
|
|
50
|
-
})]
|
|
51
|
-
}),
|
|
52
|
-
subtitle !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
53
|
-
className: "truncate text-muted-foreground",
|
|
54
|
-
children: text(row, subtitle)
|
|
55
|
-
}),
|
|
56
|
-
meta.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
57
|
-
className: "mt-auto flex flex-wrap gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
58
|
-
children: meta.map((name) => /* @__PURE__ */ jsxs("div", {
|
|
59
|
-
className: "flex gap-1",
|
|
60
|
-
children: [/* @__PURE__ */ jsx("dt", {
|
|
61
|
-
className: "sr-only",
|
|
62
|
-
children: label(name)
|
|
63
|
-
}), /* @__PURE__ */ jsx("dd", {
|
|
64
|
-
className: "tabular-nums",
|
|
65
|
-
children: text(row, name)
|
|
66
|
-
})]
|
|
67
|
-
}, name))
|
|
68
|
-
})
|
|
69
|
-
]
|
|
90
|
+
children: Card ? /* @__PURE__ */ jsx(CustomBoundary, {
|
|
91
|
+
name: display.card,
|
|
92
|
+
fallback: builtIn,
|
|
93
|
+
children: /* @__PURE__ */ jsx(Card, { row })
|
|
94
|
+
}) : builtIn
|
|
70
95
|
}) }, row.key);
|
|
71
96
|
})
|
|
72
97
|
}), /* @__PURE__ */ jsx(DisplayFooter, { list })]
|
|
@@ -8,4 +8,8 @@ export type ListDisplayRow = {
|
|
|
8
8
|
/** The name of the record a relation field points at, for the relations the list shows. */
|
|
9
9
|
labels: Record<string, string>;
|
|
10
10
|
};
|
|
11
|
+
/** What a card component of a cards display (`d.cards(..., { card })`) gets: the record it draws, of the display's fields. */
|
|
12
|
+
export type RecordCardProps = {
|
|
13
|
+
row: ListDisplayRow;
|
|
14
|
+
};
|
|
11
15
|
//#endregion
|
package/dist/app/field-editor.js
CHANGED
|
@@ -4,9 +4,11 @@ import { Switch } from "../primitives/switch.js";
|
|
|
4
4
|
import { AppLink } from "./use-app-address.js";
|
|
5
5
|
import { RelationPicker } from "./relation-picker.js";
|
|
6
6
|
import { displayValue } from "../live/display-value.js";
|
|
7
|
-
import { isEditable } from "../live/field-values.js";
|
|
7
|
+
import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
|
|
8
8
|
import { SensitiveField } from "./sensitive-field.js";
|
|
9
9
|
import { FileField } from "./file-field.js";
|
|
10
|
+
import { useShownWidget } from "./field-types/shown-widget.js";
|
|
11
|
+
import { WidgetPart } from "./field-types/widget-part.js";
|
|
10
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
13
|
import { enumLabel } from "@protobase/schema";
|
|
12
14
|
//#region src/app/field-editor.tsx
|
|
@@ -15,7 +17,7 @@ const numeric = /* @__PURE__ */ new Set([
|
|
|
15
17
|
"bigint",
|
|
16
18
|
"decimal"
|
|
17
19
|
]);
|
|
18
|
-
const
|
|
20
|
+
const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo, relationPicker, invalid, locked, placeholder, reveal, resource, onChange }) => {
|
|
19
21
|
if (field.type === "file") return /* @__PURE__ */ jsx(FileField, {
|
|
20
22
|
field,
|
|
21
23
|
hints,
|
|
@@ -94,5 +96,32 @@ const FieldEditor = ({ field, hints, value, stored, relationLabel, relationTo, r
|
|
|
94
96
|
onChange: (event) => onChange(event.target.value)
|
|
95
97
|
});
|
|
96
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* A field on a form: its field type's editor when the view chooses one that has an editor and the field may change,
|
|
101
|
+
* its display when it may not, and the built-in input or value otherwise, and when the field type throws.
|
|
102
|
+
*/
|
|
103
|
+
const FieldEditor = (props) => {
|
|
104
|
+
const { field, hints, value, stored, invalid, locked, onChange } = props;
|
|
105
|
+
const shown = useShownWidget(field, hints);
|
|
106
|
+
const builtIn = /* @__PURE__ */ jsx(BuiltInEditor, { ...props });
|
|
107
|
+
if (!shown) return builtIn;
|
|
108
|
+
if (locked || !isEditable(field)) return /* @__PURE__ */ jsx(WidgetPart, {
|
|
109
|
+
shown,
|
|
110
|
+
part: "display",
|
|
111
|
+
field,
|
|
112
|
+
value: stored,
|
|
113
|
+
fallback: builtIn
|
|
114
|
+
});
|
|
115
|
+
if (field.type === "file" || field.type === "relation") return builtIn;
|
|
116
|
+
return /* @__PURE__ */ jsx(WidgetPart, {
|
|
117
|
+
shown,
|
|
118
|
+
part: "editor",
|
|
119
|
+
field,
|
|
120
|
+
value: fromDraft(field, value),
|
|
121
|
+
invalid: Boolean(invalid),
|
|
122
|
+
onChange: (next) => onChange(toFieldDraft(field, next)),
|
|
123
|
+
fallback: builtIn
|
|
124
|
+
});
|
|
125
|
+
};
|
|
97
126
|
//#endregion
|
|
98
127
|
export { FieldEditor };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { shownWidget } from "./shown-widget.js";
|
|
2
|
+
import { WidgetPart } from "./widget-part.js";
|
|
3
|
+
import { renderCell } from "../../data-grid/cells.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/app/field-types/field-type-cells.tsx
|
|
6
|
+
/**
|
|
7
|
+
* The list's columns, where the view shows a field with a field type drawn by the field type's cell (else its
|
|
8
|
+
* display), with the built-in cell as its fallback.
|
|
9
|
+
*/
|
|
10
|
+
const withFieldTypeCells = (columns, model, view, fieldTypes) => columns.map((spec) => {
|
|
11
|
+
const field = model.fields[spec.id];
|
|
12
|
+
const shown = field && shownWidget(fieldTypes, field, view?.fields[spec.id]);
|
|
13
|
+
if (!shown) return spec;
|
|
14
|
+
return {
|
|
15
|
+
...spec,
|
|
16
|
+
render: (row) => /* @__PURE__ */ jsx(WidgetPart, {
|
|
17
|
+
shown,
|
|
18
|
+
part: "cell",
|
|
19
|
+
field,
|
|
20
|
+
value: row[spec.id],
|
|
21
|
+
fallback: renderCell(spec, spec.value ? spec.value(row) : row[spec.id])
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { withFieldTypeCells };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { FieldModel, FieldType, JsonValue } from "@protobase/schema";
|
|
3
|
+
//#region src/app/field-types/field-widget.d.ts
|
|
4
|
+
/** What a field type's display and cell get: the field, its value as the API gives it (null when empty) and the view's props. */
|
|
5
|
+
export type FieldWidgetProps<V = unknown, P = Record<string, JsonValue>> = {
|
|
6
|
+
field: FieldModel;
|
|
7
|
+
value: V | null;
|
|
8
|
+
/** What the view gives in `.widget(name, props)`; `{}` without props. */
|
|
9
|
+
props: P;
|
|
10
|
+
};
|
|
11
|
+
/** What a field type's editor gets besides: a way to set the value, and whether the value it holds is refused. */
|
|
12
|
+
export type FieldWidgetEditorProps<V = unknown, P = Record<string, JsonValue>> = FieldWidgetProps<V, P> & {
|
|
13
|
+
onChange: (value: V | null) => void;
|
|
14
|
+
invalid: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A field type a plugin registers in `definePlugin({ fields: { rating: ... } })`, which a view chooses for a field with
|
|
18
|
+
* `r.score.widget('rating', props)`: the storage types it shows (`of`), how it shows a value on record pages, cards and
|
|
19
|
+
* page blocks (`display`), how it edits one on forms (`editor`; else the built-in input) and in a table (`cell`; else
|
|
20
|
+
* `display`). On a field of another storage type, and when it throws, the field shows as it would without it.
|
|
21
|
+
*/
|
|
22
|
+
export type FieldWidget = {
|
|
23
|
+
of: FieldType[];
|
|
24
|
+
display: ComponentType<FieldWidgetProps<any, any>>;
|
|
25
|
+
editor?: ComponentType<FieldWidgetEditorProps<any, any>>;
|
|
26
|
+
cell?: ComponentType<FieldWidgetProps<any, any>>;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useProjectUi } from "../pages/project-ui.js";
|
|
2
|
+
//#region src/app/field-types/shown-widget.ts
|
|
3
|
+
/**
|
|
4
|
+
* The field type the view's `hints` choose for `field`, when one of `fields` has that name and shows the field's
|
|
5
|
+
* storage type; otherwise undefined, and the field shows as it would without one. A sensitive field never comes with
|
|
6
|
+
* its value, so it keeps its own.
|
|
7
|
+
*/
|
|
8
|
+
const shownWidget = (fields, field, hints) => {
|
|
9
|
+
const chosen = hints?.widget;
|
|
10
|
+
if (!chosen || field.sensitive || !fields || !Object.hasOwn(fields, chosen.name)) return void 0;
|
|
11
|
+
const widget = fields[chosen.name];
|
|
12
|
+
return widget.of.includes(field.type) ? {
|
|
13
|
+
name: chosen.name,
|
|
14
|
+
widget,
|
|
15
|
+
props: chosen.props ?? {}
|
|
16
|
+
} : void 0;
|
|
17
|
+
};
|
|
18
|
+
/** `shownWidget` with the field types of the project's UI and its plugins. */
|
|
19
|
+
const useShownWidget = (field, hints) => shownWidget(useProjectUi().fields, field, hints);
|
|
20
|
+
//#endregion
|
|
21
|
+
export { shownWidget, useShownWidget };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/app/field-types/widget-part.tsx
|
|
4
|
+
const partOf = ({ shown, part, field, value, onChange, invalid = false }) => {
|
|
5
|
+
const { widget, props } = shown;
|
|
6
|
+
const shared = {
|
|
7
|
+
field,
|
|
8
|
+
value: value ?? null,
|
|
9
|
+
props
|
|
10
|
+
};
|
|
11
|
+
if (part !== "editor") {
|
|
12
|
+
const Show = part === "cell" ? widget.cell ?? widget.display : widget.display;
|
|
13
|
+
return /* @__PURE__ */ jsx(Show, { ...shared });
|
|
14
|
+
}
|
|
15
|
+
const Editor = widget.editor;
|
|
16
|
+
return Editor && onChange ? /* @__PURE__ */ jsx(Editor, {
|
|
17
|
+
...shared,
|
|
18
|
+
onChange,
|
|
19
|
+
invalid
|
|
20
|
+
}) : void 0;
|
|
21
|
+
};
|
|
22
|
+
/** One part of a field type for a field, inside a boundary that draws the built-in rendering instead when it throws. */
|
|
23
|
+
const WidgetPart = ({ fallback, ...rest }) => {
|
|
24
|
+
const element = partOf(rest);
|
|
25
|
+
if (!element) return fallback;
|
|
26
|
+
return /* @__PURE__ */ jsx(CustomBoundary, {
|
|
27
|
+
name: `The field type ${rest.shown.name}`,
|
|
28
|
+
fallback,
|
|
29
|
+
children: element
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { WidgetPart };
|
package/dist/app/list-page.js
CHANGED
|
@@ -4,6 +4,7 @@ import { useAuth } from "../auth/auth-provider.js";
|
|
|
4
4
|
import { Button } from "../primitives/button.js";
|
|
5
5
|
import { recordKey, recordTitleField } from "../live/model-helpers.js";
|
|
6
6
|
import { humanize } from "../live/naming.js";
|
|
7
|
+
import { useProjectUi } from "./pages/project-ui.js";
|
|
7
8
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
8
9
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
9
10
|
import { useAppAddress } from "./use-app-address.js";
|
|
@@ -29,6 +30,7 @@ import { DisplaySwitch, SortMenu } from "./displays/display-controls.js";
|
|
|
29
30
|
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
30
31
|
import { ListChart } from "./list-chart.js";
|
|
31
32
|
import { ListSearch } from "./list-search.js";
|
|
33
|
+
import { withFieldTypeCells } from "./field-types/field-type-cells.js";
|
|
32
34
|
import { useMemo, useState } from "react";
|
|
33
35
|
import { Plus, SlidersHorizontal } from "lucide-react";
|
|
34
36
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -58,7 +60,12 @@ const LoadedList = ({ model, address }) => {
|
|
|
58
60
|
const resource = model.name;
|
|
59
61
|
const view = meta.views[resource];
|
|
60
62
|
const fields = useMemo(() => widgetFields(view, model), [view, model]);
|
|
61
|
-
const
|
|
63
|
+
const { fields: fieldTypes } = useProjectUi();
|
|
64
|
+
const columns = useMemo(() => withFieldTypeCells(columnsFromModel(model, view), model, view, fieldTypes), [
|
|
65
|
+
model,
|
|
66
|
+
view,
|
|
67
|
+
fieldTypes
|
|
68
|
+
]);
|
|
62
69
|
const filterText = address.filter;
|
|
63
70
|
const parsed = useMemo(() => textToState(fields, filterText), [fields, filterText]);
|
|
64
71
|
const state = parsed.ok ? parsed.state : emptyFilterState;
|
|
@@ -6,6 +6,8 @@ import { useAdminMeta } from "../meta-gate.js";
|
|
|
6
6
|
import { AppLink } from "../use-app-address.js";
|
|
7
7
|
import { fileShown } from "../../live/file-values.js";
|
|
8
8
|
import { displayValue } from "../../live/display-value.js";
|
|
9
|
+
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
10
|
+
import { WidgetPart } from "../field-types/widget-part.js";
|
|
9
11
|
import { toneFor } from "../../live/columns-from-model.js";
|
|
10
12
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
11
13
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -33,8 +35,7 @@ const RelationValue = ({ field, value, known }) => {
|
|
|
33
35
|
children: known ?? label.data ?? id
|
|
34
36
|
});
|
|
35
37
|
};
|
|
36
|
-
|
|
37
|
-
const FieldValue = ({ model, field, value, format, relationLabel }) => {
|
|
38
|
+
const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
38
39
|
const { views } = useAdminMeta();
|
|
39
40
|
const hints = {
|
|
40
41
|
...views[model.name]?.fields[field.name],
|
|
@@ -67,5 +68,21 @@ const FieldValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
67
68
|
children: text
|
|
68
69
|
});
|
|
69
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* A stored value as the record page shows it: the display of the field type the view chooses, else the view's hints,
|
|
73
|
+
* badges for statuses, names for relations (`relationLabel` when already known).
|
|
74
|
+
*/
|
|
75
|
+
const FieldValue = (props) => {
|
|
76
|
+
const { views } = useAdminMeta();
|
|
77
|
+
const shown = useShownWidget(props.field, views[props.model.name]?.fields[props.field.name]);
|
|
78
|
+
const builtIn = /* @__PURE__ */ jsx(BuiltInValue, { ...props });
|
|
79
|
+
return shown ? /* @__PURE__ */ jsx(WidgetPart, {
|
|
80
|
+
shown,
|
|
81
|
+
part: "display",
|
|
82
|
+
field: props.field,
|
|
83
|
+
value: props.value,
|
|
84
|
+
fallback: builtIn
|
|
85
|
+
}) : builtIn;
|
|
86
|
+
};
|
|
70
87
|
//#endregion
|
|
71
88
|
export { FieldValue };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ProjectUi } from "./project-ui.js";
|
|
2
2
|
//#region src/app/pages/merge-ui.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* One UI config from several, each after the ones it `extends`, the last being the app's own: components
|
|
5
|
-
* handlers are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
6
|
-
* the extended configs' first.
|
|
4
|
+
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
5
|
+
* handlers and field types are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
6
|
+
* is drawn, the extended configs' first.
|
|
7
7
|
*/
|
|
8
8
|
export declare const mergeUi: (...uis: ProjectUi[]) => ProjectUi;
|
|
9
9
|
//#endregion
|
|
@@ -26,14 +26,15 @@ const slot = (slots) => {
|
|
|
26
26
|
return All;
|
|
27
27
|
};
|
|
28
28
|
/**
|
|
29
|
-
* One UI config from several, each after the ones it `extends`, the last being the app's own: components
|
|
30
|
-
* handlers are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
31
|
-
* the extended configs' first.
|
|
29
|
+
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
30
|
+
* handlers and field types are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
31
|
+
* is drawn, the extended configs' first.
|
|
32
32
|
*/
|
|
33
33
|
const mergeUi = (...uis) => {
|
|
34
34
|
const named = uis.flatMap((ui, index) => flatten(ui, ui.name ?? `UI config ${index + 1}`));
|
|
35
35
|
const components = union("The component", named, (ui) => ui.components);
|
|
36
36
|
const actions = union("The action handler", named, (ui) => ui.actions);
|
|
37
|
+
const fields = union("The field type", named, (ui) => ui.fields);
|
|
37
38
|
const actionSlot = slot(named.flatMap(({ ui }) => ui.shell?.actions ? [ui.shell.actions] : []));
|
|
38
39
|
const rightPanel = slot(named.flatMap(({ ui }) => ui.shell?.rightPanel ? [ui.shell.rightPanel] : []));
|
|
39
40
|
const name = uis.at(-1)?.name;
|
|
@@ -41,6 +42,7 @@ const mergeUi = (...uis) => {
|
|
|
41
42
|
...name && { name },
|
|
42
43
|
components,
|
|
43
44
|
actions,
|
|
45
|
+
...Object.keys(fields).length > 0 && { fields },
|
|
44
46
|
...(actionSlot || rightPanel) && { shell: {
|
|
45
47
|
...actionSlot && { actions: actionSlot },
|
|
46
48
|
...rightPanel && { rightPanel }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FieldWidget } from "../field-types/field-widget.js";
|
|
1
2
|
import { ComponentType } from "react";
|
|
2
3
|
import { Client } from "@protobase/client";
|
|
3
4
|
import { AppAddress } from "@protobase/schema";
|
|
@@ -16,15 +17,18 @@ export type ActionContext = {
|
|
|
16
17
|
export type ActionHandler = (context: ActionContext) => void | Promise<void>;
|
|
17
18
|
/**
|
|
18
19
|
* The project's own React code for composed pages: custom components by the name their layout declares
|
|
19
|
-
* (`component('UsageChart')`) or a widget part names, handlers for named actions that have no built-in behaviour,
|
|
20
|
-
* the shell's slots. `extends` lists UI configs merged in
|
|
21
|
-
* (`import * as tasksUi from '...'`), their exported
|
|
20
|
+
* (`component('UsageChart')`) or a widget part names, handlers for named actions that have no built-in behaviour,
|
|
21
|
+
* field types by the name a view's `.widget(name)` chooses, and the shell's slots. `extends` lists UI configs merged in
|
|
22
|
+
* before it (see `mergeUi`), or whole `protobase.ui.tsx` modules (`import * as tasksUi from '...'`), their exported
|
|
23
|
+
* components included; `name` names it in their errors.
|
|
22
24
|
*/
|
|
23
25
|
export type ProjectUi = {
|
|
24
26
|
name?: string;
|
|
25
27
|
extends?: Array<ProjectUi | UiModule>;
|
|
26
28
|
components?: Record<string, ComponentType<any>>;
|
|
27
29
|
actions?: Record<string, ActionHandler>;
|
|
30
|
+
/** Field types by name, each with the storage types it shows and its components (see `FieldWidget`). */
|
|
31
|
+
fields?: Record<string, FieldWidget>;
|
|
28
32
|
/** Rendered in the shell on every page: `actions` at the end of the top bar, `rightPanel` beside the page. */
|
|
29
33
|
shell?: {
|
|
30
34
|
actions?: ComponentType;
|
|
@@ -39,9 +43,9 @@ export type UiModule = {
|
|
|
39
43
|
/** Typed identity, for `export default defineUi({ extends, components, actions })` in `protobase.ui.tsx`. */
|
|
40
44
|
export declare const defineUi: (ui: ProjectUi) => ProjectUi;
|
|
41
45
|
/**
|
|
42
|
-
* Typed identity, for `export default definePlugin({ name, components, actions, shell })` in a plugin's
|
|
43
|
-
* `protobase.ui.tsx`: a plugin is a UI config the app loads at runtime
|
|
44
|
-
*
|
|
46
|
+
* Typed identity, for `export default definePlugin({ name, components, actions, fields, shell })` in a plugin's
|
|
47
|
+
* `protobase.ui.tsx`: a plugin is a UI config the app loads at runtime, its components exported by name as in a
|
|
48
|
+
* project's. A project's own protobase.ui.tsx is one too, which `protobase build` builds as the plugin `app`.
|
|
45
49
|
*/
|
|
46
50
|
export declare const definePlugin: (plugin: ProjectUi) => ProjectUi;
|
|
47
51
|
//#endregion
|
|
@@ -3,9 +3,9 @@ import { createContext, useContext } from "react";
|
|
|
3
3
|
/** Typed identity, for `export default defineUi({ extends, components, actions })` in `protobase.ui.tsx`. */
|
|
4
4
|
const defineUi = (ui) => ui;
|
|
5
5
|
/**
|
|
6
|
-
* Typed identity, for `export default definePlugin({ name, components, actions, shell })` in a plugin's
|
|
7
|
-
* `protobase.ui.tsx`: a plugin is a UI config the app loads at runtime
|
|
8
|
-
*
|
|
6
|
+
* Typed identity, for `export default definePlugin({ name, components, actions, fields, shell })` in a plugin's
|
|
7
|
+
* `protobase.ui.tsx`: a plugin is a UI config the app loads at runtime, its components exported by name as in a
|
|
8
|
+
* project's. A project's own protobase.ui.tsx is one too, which `protobase build` builds as the plugin `app`.
|
|
9
9
|
*/
|
|
10
10
|
const definePlugin = (plugin) => plugin;
|
|
11
11
|
/** The project's UI, merged with the UI configs it extends and the plugins loaded; `PluginGate` or `ProjectUiProvider` provides it. */
|
package/dist/app/record-page.js
CHANGED
|
@@ -7,11 +7,14 @@ import { useClient } from "../data/api-provider.js";
|
|
|
7
7
|
import { useRecord } from "../data/use-record.js";
|
|
8
8
|
import { recordTitle } from "../live/model-helpers.js";
|
|
9
9
|
import { humanize } from "../live/naming.js";
|
|
10
|
+
import { useProjectUi } from "./pages/project-ui.js";
|
|
10
11
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
11
12
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
12
13
|
import { useAppAddress } from "./use-app-address.js";
|
|
13
14
|
import { displayValue } from "../live/display-value.js";
|
|
14
15
|
import { ErrorBanner } from "./error-banner.js";
|
|
16
|
+
import { shownWidget } from "./field-types/shown-widget.js";
|
|
17
|
+
import { WidgetPart } from "./field-types/widget-part.js";
|
|
15
18
|
import { FieldEditor } from "./field-editor.js";
|
|
16
19
|
import { can, fieldAccess } from "./permissions.js";
|
|
17
20
|
import { Field } from "../record-view/field.js";
|
|
@@ -122,6 +125,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
122
125
|
onReview: () => void queryClient.invalidateQueries({ queryKey: keys.record(model.name, recordKey) })
|
|
123
126
|
});
|
|
124
127
|
const related = useRelatedRecords(model, view, meta.resources, meta.views, stored.record, basePath);
|
|
128
|
+
const { fields: fieldTypes } = useProjectUi();
|
|
125
129
|
const layout = layoutFor(model, view);
|
|
126
130
|
const sidebarFields = layout.flatMap((item) => item.kind === "sidebar" ? item.fields : []);
|
|
127
131
|
const status = model.fields.status?.type === "enum" ? String(stored.record.status) : void 0;
|
|
@@ -171,12 +175,20 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
171
175
|
const field = model.fields[name];
|
|
172
176
|
if (!field || !visible(name)) return [];
|
|
173
177
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
178
|
+
const builtIn = field.type === "enum" ? /* @__PURE__ */ jsx(Badge, {
|
|
179
|
+
tone: toneFor(String(stored.record[name])),
|
|
180
|
+
children: text
|
|
181
|
+
}) : /* @__PURE__ */ jsx("strong", { children: text });
|
|
182
|
+
const shown = shownWidget(fieldTypes, field, hint(name));
|
|
174
183
|
return [{
|
|
175
184
|
label: hint(name)?.label ?? humanize(name),
|
|
176
|
-
value:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
value: shown ? /* @__PURE__ */ jsx(WidgetPart, {
|
|
186
|
+
shown,
|
|
187
|
+
part: "display",
|
|
188
|
+
field,
|
|
189
|
+
value: stored.record[name],
|
|
190
|
+
fallback: builtIn
|
|
191
|
+
}) : builtIn
|
|
180
192
|
}];
|
|
181
193
|
})
|
|
182
194
|
});
|
|
@@ -10,7 +10,7 @@ const buildColumns = (specs) => helper.columns(specs.map((spec) => helper.access
|
|
|
10
10
|
id: spec.id,
|
|
11
11
|
header: spec.header,
|
|
12
12
|
enableSorting: spec.sortable ?? false,
|
|
13
|
-
cell: (context) => renderCell(spec, context.getValue())
|
|
13
|
+
cell: (context) => spec.render ? spec.render(context.row.original) : renderCell(spec, context.getValue())
|
|
14
14
|
})));
|
|
15
15
|
//#endregion
|
|
16
16
|
export { buildColumns, features };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FieldWidget, FieldWidgetEditorProps, FieldWidgetProps } from "./app/field-types/field-widget.js";
|
|
1
2
|
import { ActionContext, ActionHandler, ProjectUi, definePlugin, defineUi } from "./app/pages/project-ui.js";
|
|
2
3
|
import { PluginInfo, usePlugin } from "./plugins/plugin-context.js";
|
|
3
4
|
import { App, AppProps } from "./app/app.js";
|
|
@@ -15,7 +16,7 @@ import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
|
15
16
|
import { LayoutRecord, useLayoutRecord } from "./app/pages/record-scope.js";
|
|
16
17
|
import { CurrentAddress, ListAddress, ListChange } from "./app/current-address.js";
|
|
17
18
|
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
18
|
-
import { ListDisplayRow } from "./app/displays/list-displays.js";
|
|
19
|
+
import { ListDisplayRow, RecordCardProps } from "./app/displays/list-displays.js";
|
|
19
20
|
import { ListDisplay, useListDisplay } from "./app/displays/display-context.js";
|
|
20
21
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
21
22
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
@@ -38,4 +39,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
|
|
|
38
39
|
import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
|
|
39
40
|
import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
|
|
40
41
|
import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
|
|
41
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useSeries, useUpdateRecord };
|
|
42
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useSeries, useUpdateRecord };
|
|
@@ -22,6 +22,14 @@ const toPatchValue = (field, draft) => {
|
|
|
22
22
|
if (textEdited.has(field.type) || field.type === "enum") return text;
|
|
23
23
|
throw new Error(`Not implemented: editing ${field.type} fields`);
|
|
24
24
|
};
|
|
25
|
+
/** The value a draft stands for, as the API gives one: null when empty, a boolean or number where the field holds one. */
|
|
26
|
+
const fromDraft = (field, draft) => {
|
|
27
|
+
if (field.type === "boolean") return Boolean(draft);
|
|
28
|
+
if (String(draft ?? "").trim() === "") return null;
|
|
29
|
+
return toPatchValue(field, draft);
|
|
30
|
+
};
|
|
31
|
+
/** The draft for a value, as the editor holds it: a boolean for a boolean field, else its text. */
|
|
32
|
+
const toFieldDraft = (field, value) => field.type === "boolean" ? Boolean(value) : toDraft(value);
|
|
25
33
|
/** Whether the draft differs from what the server holds, comparing numbers by value. */
|
|
26
34
|
const differs = (field, draft, stored) => {
|
|
27
35
|
if (field.type === "boolean") return Boolean(draft) !== Boolean(stored);
|
|
@@ -32,4 +40,4 @@ const differs = (field, draft, stored) => {
|
|
|
32
40
|
return a !== b;
|
|
33
41
|
};
|
|
34
42
|
//#endregion
|
|
35
|
-
export { differs, isEditable, toDraft, toPatchValue };
|
|
43
|
+
export { differs, fromDraft, isEditable, toDraft, toFieldDraft, toPatchValue };
|
package/dist/package.js
CHANGED
|
@@ -11,10 +11,16 @@ const within = (plugin, Component, name) => {
|
|
|
11
11
|
Within.displayName = `${plugin.name}:${name}`;
|
|
12
12
|
return Within;
|
|
13
13
|
};
|
|
14
|
+
const fieldWithin = (plugin, name, widget) => ({
|
|
15
|
+
of: widget.of,
|
|
16
|
+
display: within(plugin, widget.display, `fields.${name}.display`),
|
|
17
|
+
...widget.editor && { editor: within(plugin, widget.editor, `fields.${name}.editor`) },
|
|
18
|
+
...widget.cell && { cell: within(plugin, widget.cell, `fields.${name}.cell`) }
|
|
19
|
+
});
|
|
14
20
|
/**
|
|
15
21
|
* A plugin's UI config from its module, read as a `protobase.ui.tsx` module and merged with the configs it extends,
|
|
16
|
-
* named after the plugin: its components and shell slots draw inside the plugin. Throws `UiConflictError`
|
|
17
|
-
* configs it merges define a name twice.
|
|
22
|
+
* named after the plugin: its components, field types and shell slots draw inside the plugin. Throws `UiConflictError`
|
|
23
|
+
* when the configs it merges define a name twice.
|
|
18
24
|
*/
|
|
19
25
|
const pluginUi = (plugin, module) => {
|
|
20
26
|
const ui = mergeUi({
|
|
@@ -22,6 +28,7 @@ const pluginUi = (plugin, module) => {
|
|
|
22
28
|
name: plugin.name
|
|
23
29
|
});
|
|
24
30
|
const components = Object.fromEntries(Object.entries(ui.components ?? {}).map(([name, Component]) => [name, within(plugin, Component, name)]));
|
|
31
|
+
const fields = ui.fields && Object.fromEntries(Object.entries(ui.fields).map(([name, widget]) => [name, fieldWithin(plugin, name, widget)]));
|
|
25
32
|
const shell = ui.shell && {
|
|
26
33
|
...ui.shell.actions && { actions: within(plugin, ui.shell.actions, "shell.actions") },
|
|
27
34
|
...ui.shell.rightPanel && { rightPanel: within(plugin, ui.shell.rightPanel, "shell.rightPanel") }
|
|
@@ -30,6 +37,7 @@ const pluginUi = (plugin, module) => {
|
|
|
30
37
|
name: plugin.name,
|
|
31
38
|
components,
|
|
32
39
|
...ui.actions && { actions: ui.actions },
|
|
40
|
+
...fields && { fields },
|
|
33
41
|
...shell && { shell }
|
|
34
42
|
};
|
|
35
43
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.pb-range{appearance:none;pointer-events:none;background:0 0;width:100%;height:100%;margin:0;position:absolute;inset:0}.pb-range::-webkit-slider-thumb{appearance:none;pointer-events:auto;background:var(--background);border:2px solid var(--primary);cursor:grab;border-radius:9999px;width:14px;height:14px;box-shadow:0 1px 2px #0003}.pb-range::-moz-range-thumb{pointer-events:auto;background:var(--background);border:2px solid var(--primary);cursor:grab;border-radius:9999px;width:10px;height:10px}.pb-range::-webkit-slider-runnable-track{appearance:none;background:0 0}.pb-range::-moz-range-track{background:0 0}
|