@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,34 @@
|
|
|
1
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
2
|
+
import { DotsRoom, RelatedDots } from "./related-dots.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app/related-dots/card-dots.tsx
|
|
5
|
+
const lines = "flex flex-col gap-1.5";
|
|
6
|
+
/** The room a card's lines of dots take at its bottom, inside its link, which the dots are drawn over. */
|
|
7
|
+
const CardDotsRoom = ({ dots }) => /* @__PURE__ */ jsx("span", {
|
|
8
|
+
className: lines,
|
|
9
|
+
children: dots.map(({ column, records }) => /* @__PURE__ */ jsx(DotsRoom, { count: records.length }, column.name))
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* A card's lines of dots, over the room its link holds for them at its bottom, inside its border and padding: beside
|
|
13
|
+
* the link in the document, so a dot is a link of its own and the pointer between the dots reaches the card's.
|
|
14
|
+
* The card's list item must be `relative`.
|
|
15
|
+
*/
|
|
16
|
+
const CardDots = ({ dots, Card }) => {
|
|
17
|
+
const { resources, views } = useAdminMeta();
|
|
18
|
+
return /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: `pointer-events-none absolute inset-x-[calc(0.75rem+1px)] bottom-[calc(0.75rem+1px)] ${lines}`,
|
|
20
|
+
children: dots.map(({ column, records }) => {
|
|
21
|
+
const target = resources[column.resource];
|
|
22
|
+
return target && /* @__PURE__ */ jsx(RelatedDots, {
|
|
23
|
+
column,
|
|
24
|
+
records,
|
|
25
|
+
target,
|
|
26
|
+
view: views[target.name],
|
|
27
|
+
Card,
|
|
28
|
+
wrap: true
|
|
29
|
+
}, column.name);
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CardDots, CardDotsRoom };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { displaysOf, shownFields } from "../displays/list-displays.js";
|
|
2
2
|
//#region src/app/related-dots/dot-card.ts
|
|
3
3
|
/**
|
|
4
|
-
* The card a dot shows of its record: the first cards display of the record's view, without the photos
|
|
5
|
-
* button a small card has no room for, else the record's title with its status as the badge.
|
|
4
|
+
* The card a dot shows of its record: the first cards display of the record's view, without the photos, favorite
|
|
5
|
+
* button and dots a small card has no room for, else the record's title with its status as the badge.
|
|
6
6
|
*/
|
|
7
7
|
const dotCard = (view, status) => {
|
|
8
8
|
const cards = displaysOf(view).find((display) => display.kind === "cards");
|
|
@@ -11,7 +11,7 @@ const dotCard = (view, status) => {
|
|
|
11
11
|
name: "dot",
|
|
12
12
|
badge: status
|
|
13
13
|
};
|
|
14
|
-
const { photos: _, favorite: __, ...card } = cards;
|
|
14
|
+
const { photos: _, favorite: __, dots: ___, ...card } = cards;
|
|
15
15
|
return card;
|
|
16
16
|
};
|
|
17
17
|
/** What the list loads for the related columns of its view among `columns`: each with the fields its dots' cards show. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
2
|
import { headerWidth, listColumnNames } from "../../live/columns-from-model.js";
|
|
3
3
|
import { RelatedDots } from "./related-dots.js";
|
|
4
|
+
import { RecordCard } from "../displays/record-card.js";
|
|
4
5
|
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
//#region src/app/related-dots/related-dots-columns.tsx
|
|
6
7
|
/**
|
|
@@ -29,7 +30,8 @@ const withRelatedDotsColumns = (columns, model, view, resources, views) => {
|
|
|
29
30
|
column,
|
|
30
31
|
records: row[column.name] ?? [],
|
|
31
32
|
target,
|
|
32
|
-
view: views[target.name]
|
|
33
|
+
view: views[target.name],
|
|
34
|
+
Card: RecordCard
|
|
33
35
|
})
|
|
34
36
|
}];
|
|
35
37
|
});
|
|
@@ -7,21 +7,24 @@ import { displayRow } from "../displays/list-displays.js";
|
|
|
7
7
|
import { AppLink } from "../use-app-address.js";
|
|
8
8
|
import { toneFor } from "../../live/columns-from-model.js";
|
|
9
9
|
import { valueColor } from "../../live/value-color.js";
|
|
10
|
-
import { RecordCard } from "../displays/record-card.js";
|
|
11
|
-
import { dotCard } from "./dot-card.js";
|
|
12
10
|
import { placeCard } from "./card-place.js";
|
|
11
|
+
import { dotCard } from "./dot-card.js";
|
|
13
12
|
import { dotsSummary } from "./dots-summary.js";
|
|
14
13
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
15
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
15
|
import { createPortal } from "react-dom";
|
|
17
16
|
//#region src/app/related-dots/related-dots.tsx
|
|
17
|
+
/** A dot's box and its mark, which a card's room for its dots (`DotsRoom`) takes the size of. */
|
|
18
|
+
const dotBox = "flex shrink-0 items-center justify-center rounded-full p-0.5 pointer-coarse:p-1.5";
|
|
19
|
+
const dotMark = "size-2.5 rounded-full";
|
|
20
|
+
const dotsLine = "flex min-w-0 items-center gap-0.5";
|
|
18
21
|
const hideMs = 150;
|
|
19
22
|
const cardWidth = 288;
|
|
20
23
|
/**
|
|
21
24
|
* The card of the dot that is hovered, focused or tapped, portalled to the body: every grid row is its own stacking
|
|
22
25
|
* context and the grid body clips, so a card inside the row would sit under the rows below it.
|
|
23
26
|
*/
|
|
24
|
-
const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave }) => {
|
|
27
|
+
const DotCardPopover = ({ shown, row, target, view, column, Card, onEnter, onLeave }) => {
|
|
25
28
|
const card = useRef(null);
|
|
26
29
|
const [place, setPlace] = useState();
|
|
27
30
|
useLayoutEffect(() => {
|
|
@@ -48,7 +51,7 @@ const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave })
|
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
className: "fixed z-50 rounded-lg shadow-pop",
|
|
51
|
-
children: /* @__PURE__ */ jsx(
|
|
54
|
+
children: /* @__PURE__ */ jsx(Card, {
|
|
52
55
|
model: target,
|
|
53
56
|
view,
|
|
54
57
|
display: dotCard(view, column.status),
|
|
@@ -61,7 +64,7 @@ const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave })
|
|
|
61
64
|
* or focusing a dot shows the record's card; on a touch screen the first tap shows it, and the card opens the record.
|
|
62
65
|
* The dots say how many records have each status to a screen reader.
|
|
63
66
|
*/
|
|
64
|
-
const RelatedDots = ({ column, records, target, view }) => {
|
|
67
|
+
const RelatedDots = ({ column, records, target, view, wrap = false, Card }) => {
|
|
65
68
|
const rows = useMemo(() => records.map((record) => displayRow(target, record)), [records, target]);
|
|
66
69
|
const [shown, setShown] = useState();
|
|
67
70
|
const hideTimer = useRef(void 0);
|
|
@@ -123,7 +126,7 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
123
126
|
const shownRow = shown && rows.find((row) => row.key === shown.key);
|
|
124
127
|
return /* @__PURE__ */ jsxs("div", {
|
|
125
128
|
ref: box,
|
|
126
|
-
className: "
|
|
129
|
+
className: cn(dotsLine, wrap && "pointer-events-none flex-wrap"),
|
|
127
130
|
onClick: (event) => event.stopPropagation(),
|
|
128
131
|
onPointerDown: (event) => pointer.current = event.pointerType,
|
|
129
132
|
children: [
|
|
@@ -146,11 +149,11 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
146
149
|
onFocus: (event) => event.currentTarget.matches(":focus-visible") && show(row.key, event.currentTarget),
|
|
147
150
|
onBlur: blur,
|
|
148
151
|
onClick: tap(row.key),
|
|
149
|
-
className: "
|
|
152
|
+
className: cn(dotBox, "pointer-events-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"),
|
|
150
153
|
children: /* @__PURE__ */ jsx("span", {
|
|
151
154
|
"aria-hidden": true,
|
|
152
155
|
style: color === void 0 ? void 0 : { backgroundColor: color },
|
|
153
|
-
className: cn(
|
|
156
|
+
className: cn(dotMark, color === void 0 && toneDots[toneFor(String(value))], shown?.key === row.key && "ring-2 ring-foreground/30")
|
|
154
157
|
})
|
|
155
158
|
}, row.key);
|
|
156
159
|
}),
|
|
@@ -160,11 +163,21 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
160
163
|
target,
|
|
161
164
|
view,
|
|
162
165
|
column,
|
|
166
|
+
Card,
|
|
163
167
|
onEnter: cancelHide,
|
|
164
168
|
onLeave: hideSoon
|
|
165
169
|
})
|
|
166
170
|
]
|
|
167
171
|
});
|
|
168
172
|
};
|
|
173
|
+
/** Room for `count` dots that wrap as `RelatedDots` with `wrap` does, for a card to hold the dots drawn over it. */
|
|
174
|
+
const DotsRoom = ({ count }) => /* @__PURE__ */ jsx("span", {
|
|
175
|
+
"aria-hidden": true,
|
|
176
|
+
className: cn(dotsLine, "invisible flex-wrap"),
|
|
177
|
+
children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx("span", {
|
|
178
|
+
className: dotBox,
|
|
179
|
+
children: /* @__PURE__ */ jsx("span", { className: dotMark })
|
|
180
|
+
}, index))
|
|
181
|
+
});
|
|
169
182
|
//#endregion
|
|
170
|
-
export { RelatedDots };
|
|
183
|
+
export { DotsRoom, RelatedDots };
|
|
@@ -2,16 +2,16 @@ import { Skeleton } from "../primitives/skeleton.js";
|
|
|
2
2
|
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
|
+
import { Section } from "../record-view/section.js";
|
|
5
6
|
import { ErrorBanner } from "./error-banner.js";
|
|
6
7
|
import { listColumnNames } from "../live/columns-from-model.js";
|
|
7
|
-
import { Section } from "../record-view/section.js";
|
|
8
8
|
import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
|
|
9
9
|
import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
10
10
|
import { jsx } from "react/jsx-runtime";
|
|
11
11
|
import { useQuery } from "@tanstack/react-query";
|
|
12
12
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
13
13
|
//#region src/app/related-section.tsx
|
|
14
|
-
const where = createWhere();
|
|
14
|
+
const where$1 = createWhere();
|
|
15
15
|
/** The most join records a section reads to find its records; the list API's largest page. */
|
|
16
16
|
const maxThrough = 500;
|
|
17
17
|
const and = (...parts) => {
|
|
@@ -28,7 +28,7 @@ const useMatch = (item, recordKey) => {
|
|
|
28
28
|
const through = item.through;
|
|
29
29
|
const via = through ? resources[through.resource] : void 0;
|
|
30
30
|
const key = through?.key ?? via?.primaryKey[0];
|
|
31
|
-
const viaFilter = through ? and(printFilter(where.eq(through.field, recordKey)), through.filter) : "";
|
|
31
|
+
const viaFilter = through ? and(printFilter(where$1.eq(through.field, recordKey)), through.filter) : "";
|
|
32
32
|
const collected = useQuery({
|
|
33
33
|
queryKey: keys.list(via?.name ?? "", viaFilter, "", maxThrough, `related:${key}`),
|
|
34
34
|
enabled: Boolean(via && key),
|
|
@@ -42,7 +42,7 @@ const useMatch = (item, recordKey) => {
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
if (!through) return {
|
|
45
|
-
filter: and(printFilter(where.eq(item.field, recordKey)), item.filter),
|
|
45
|
+
filter: and(printFilter(where$1.eq(item.field, recordKey)), item.filter),
|
|
46
46
|
isPending: false,
|
|
47
47
|
error: null
|
|
48
48
|
};
|
|
@@ -52,7 +52,7 @@ const useMatch = (item, recordKey) => {
|
|
|
52
52
|
error: collected.error
|
|
53
53
|
};
|
|
54
54
|
return {
|
|
55
|
-
filter: collected.data.length === 0 ? null : and(printFilter(where.in(item.field, collected.data)), item.filter),
|
|
55
|
+
filter: collected.data.length === 0 ? null : and(printFilter(where$1.in(item.field, collected.data)), item.filter),
|
|
56
56
|
isPending: false,
|
|
57
57
|
error: null
|
|
58
58
|
};
|
|
@@ -6,7 +6,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
import { useQuery } from "@tanstack/react-query";
|
|
7
7
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
8
8
|
//#region src/app/relation-picker.tsx
|
|
9
|
-
const where = createWhere();
|
|
9
|
+
const where$1 = createWhere();
|
|
10
10
|
/** Picks a record of another resource: type to search (when the target has search fields), choose from the first matches. */
|
|
11
11
|
const RelationPicker = ({ target, view, value, label, invalid, onChange }) => {
|
|
12
12
|
const client = useClient();
|
|
@@ -22,7 +22,7 @@ const RelationPicker = ({ target, view, value, label, invalid, onChange }) => {
|
|
|
22
22
|
],
|
|
23
23
|
enabled: open,
|
|
24
24
|
queryFn: () => client.list(target.name, {
|
|
25
|
-
filter: text && target.search?.length ? printFilter(where.search(text)) : void 0,
|
|
25
|
+
filter: text && target.search?.length ? printFilter(where$1.search(text)) : void 0,
|
|
26
26
|
fields: [keyField, labelField],
|
|
27
27
|
pageSize: 8
|
|
28
28
|
})
|
|
@@ -2,7 +2,7 @@ import { recordKey } from "../../live/model-helpers.js";
|
|
|
2
2
|
import { createWhere } from "@protobase/schema";
|
|
3
3
|
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/saved/saved-records.ts
|
|
5
|
-
const where = createWhere();
|
|
5
|
+
const where$1 = createWhere();
|
|
6
6
|
const relationValue = (value) => {
|
|
7
7
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
8
8
|
if (value && typeof value === "object" && "id" in value) return relationValue(value.id);
|
|
@@ -32,7 +32,7 @@ const savedScope = (target, saved) => {
|
|
|
32
32
|
const [key] = target.primaryKey;
|
|
33
33
|
if (target.primaryKey.length !== 1 || key === void 0) throw new Error(`${target.name} has a composite key, so its saved records cannot be listed`);
|
|
34
34
|
const values = [...new Set(saved.map((entry) => entry.target))];
|
|
35
|
-
return values.length === 0 ? void 0 : where.in(key, values);
|
|
35
|
+
return values.length === 0 ? void 0 : where$1.in(key, values);
|
|
36
36
|
};
|
|
37
37
|
//#endregion
|
|
38
38
|
export { savedFor, savedRecordsOf, savedScope, withSaved, withoutSaved };
|
|
@@ -3,7 +3,7 @@ import { humanize } from "../../live/naming.js";
|
|
|
3
3
|
import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
|
|
4
4
|
import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/app/search/search-model.ts
|
|
6
|
-
const where = createWhere();
|
|
6
|
+
const where$1 = createWhere();
|
|
7
7
|
const searchTargets = (meta) => Object.values(meta.resources).flatMap((model) => {
|
|
8
8
|
const view = meta.views[model.name];
|
|
9
9
|
if (!model.search?.length || !view || view.nav?.hidden || meta.permissions[model.name]?.read === false) return [];
|
|
@@ -22,7 +22,7 @@ const searchPlaceholder = (targets) => {
|
|
|
22
22
|
return `Search ${names[0]}, ${names[1]} and more`;
|
|
23
23
|
};
|
|
24
24
|
/** The list filter for the text: the server's partial-word `search(...)` over the resource's search fields. */
|
|
25
|
-
const searchFilter = (text) => printFilter(where.search(text.trim()));
|
|
25
|
+
const searchFilter = (text) => printFilter(where$1.search(text.trim()));
|
|
26
26
|
/** The fields a result's bold line joins: the view's `searchResult.title`, else the record's title field. */
|
|
27
27
|
const titleFields = (target) => {
|
|
28
28
|
const fields = target.view?.searchResult?.title;
|
|
@@ -2,8 +2,8 @@ import { Skeleton } from "../../primitives/skeleton.js";
|
|
|
2
2
|
import { recordKey } from "../../live/model-helpers.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
4
|
import { useAdminMeta } from "../meta-gate.js";
|
|
5
|
-
import { AppLink } from "../use-app-address.js";
|
|
6
5
|
import { ErrorBanner } from "../error-banner.js";
|
|
6
|
+
import { AppLink } from "../use-app-address.js";
|
|
7
7
|
import { InlineFilterChips } from "./filter-chip-list.js";
|
|
8
8
|
import { RecordRowBody } from "../record-row/record-row-body.js";
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
@@ -4,7 +4,7 @@ import { textToState } from "../../live/filter-string.js";
|
|
|
4
4
|
import { rangeFormat, widgetFields } from "../../live/view-fields.js";
|
|
5
5
|
import { createWhere, enumLabel, parseFilter, printFilter } from "@protobase/schema";
|
|
6
6
|
//#region src/app/searches/saved-search.ts
|
|
7
|
-
const where = createWhere();
|
|
7
|
+
const where$1 = createWhere();
|
|
8
8
|
const span = {
|
|
9
9
|
start: 0,
|
|
10
10
|
end: 0
|
|
@@ -79,14 +79,14 @@ const facetNames = (config) => Object.fromEntries(config.facets.filter((group) =
|
|
|
79
79
|
*/
|
|
80
80
|
const unseenFilter = (text, field, seenAt) => {
|
|
81
81
|
if (typeof seenAt !== "string" || seenAt === "") return text;
|
|
82
|
-
const after = where.gt(field, {
|
|
82
|
+
const after = where$1.gt(field, {
|
|
83
83
|
kind: "timestamp",
|
|
84
84
|
value: new Date(seenAt).toISOString(),
|
|
85
85
|
span
|
|
86
86
|
});
|
|
87
87
|
const parsed = parseFilter(text);
|
|
88
88
|
if (!parsed.ok) throw new Error(`The saved filter cannot be read: ${parsed.errors.map((error) => error.message).join("; ")}`);
|
|
89
|
-
return printFilter(parsed.ast ? where.and(parsed.ast, after) : after);
|
|
89
|
+
return printFilter(parsed.ast ? where$1.and(parsed.ast, after) : after);
|
|
90
90
|
};
|
|
91
91
|
/** Whether a record whose `field` holds `value` is new to the user, as `unseenFilter` finds it: after `seenAt`, or any. */
|
|
92
92
|
const isUnseen = (value, seenAt) => {
|
|
@@ -3,9 +3,9 @@ import { PageHeader } from "../../app-shell/page-header.js";
|
|
|
3
3
|
import { recordKey } from "../../live/model-helpers.js";
|
|
4
4
|
import { humanize } from "../../live/naming.js";
|
|
5
5
|
import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
6
|
-
import { can } from "../permissions.js";
|
|
7
|
-
import { AppLink } from "../use-app-address.js";
|
|
8
6
|
import { ErrorBanner } from "../error-banner.js";
|
|
7
|
+
import { AppLink } from "../use-app-address.js";
|
|
8
|
+
import { can } from "../permissions.js";
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
10
10
|
import { useDeleteFlow } from "../delete/use-delete-flow.js";
|
|
11
11
|
import { SavedSearchSection } from "./saved-search-section.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useClient } from "../../data/api-provider.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
3
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
|
-
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
5
4
|
import { Switch } from "../../primitives/switch.js";
|
|
5
|
+
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
6
6
|
import { useId } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { useMutation } from "@tanstack/react-query";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createWhere, parseFilter, printFilter } from "@protobase/schema";
|
|
2
2
|
//#region src/app/searches/search-diff.ts
|
|
3
|
-
const where = createWhere();
|
|
3
|
+
const where$1 = createWhere();
|
|
4
4
|
const span = {
|
|
5
5
|
start: 0,
|
|
6
6
|
end: 0
|
|
7
7
|
};
|
|
8
|
-
const guarded = (atom, condition) => where.and(condition, {
|
|
8
|
+
const guarded = (atom, condition) => where$1.and(condition, {
|
|
9
9
|
kind: "present",
|
|
10
10
|
field: atom.field,
|
|
11
11
|
span
|
|
@@ -32,13 +32,13 @@ const surely = (expr) => {
|
|
|
32
32
|
/** The filter that holds exactly where `expr` fails, and fails, never unknown, everywhere else. */
|
|
33
33
|
const surelyNot = (expr) => {
|
|
34
34
|
switch (expr.kind) {
|
|
35
|
-
case "and": return where.or(...expr.args.map(surelyNot));
|
|
36
|
-
case "or": return where.and(...expr.args.map(surelyNot));
|
|
35
|
+
case "and": return where$1.or(...expr.args.map(surelyNot));
|
|
36
|
+
case "or": return where$1.and(...expr.args.map(surelyNot));
|
|
37
37
|
case "not": return surely(expr.arg);
|
|
38
38
|
case "search":
|
|
39
39
|
case "present":
|
|
40
|
-
case "isNull": return where.not(expr);
|
|
41
|
-
default: return guarded(expr, where.not(expr));
|
|
40
|
+
case "isNull": return where$1.not(expr);
|
|
41
|
+
default: return guarded(expr, where$1.not(expr));
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
const read = (text) => {
|
|
@@ -55,8 +55,8 @@ const addedFilter = (before, after) => {
|
|
|
55
55
|
const old = read(before);
|
|
56
56
|
if (!old) return null;
|
|
57
57
|
const next = read(after);
|
|
58
|
-
const notOld = where.not(surely(old));
|
|
59
|
-
return printFilter(next ? where.and(next, notOld) : notOld);
|
|
58
|
+
const notOld = where$1.not(surely(old));
|
|
59
|
+
return printFilter(next ? where$1.and(next, notOld) : notOld);
|
|
60
60
|
};
|
|
61
61
|
/** The filter of the records `before` finds and `after` no longer does; `null` when none can be. */
|
|
62
62
|
const removedFilter = (before, after) => addedFilter(after, before);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Input } from "../../primitives/input.js";
|
|
2
2
|
import { Button } from "../../primitives/button.js";
|
|
3
|
-
import { FilterChipList } from "./filter-chip-list.js";
|
|
4
3
|
import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
|
|
4
|
+
import { FilterChipList } from "./filter-chip-list.js";
|
|
5
5
|
import { useId, useState } from "react";
|
|
6
6
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
//#region src/app/searches/search-name-dialog.tsx
|
|
@@ -2,10 +2,10 @@ import { Skeleton } from "../../primitives/skeleton.js";
|
|
|
2
2
|
import { Button } from "../../primitives/button.js";
|
|
3
3
|
import { useClient } from "../../data/api-provider.js";
|
|
4
4
|
import { humanize } from "../../live/naming.js";
|
|
5
|
+
import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
|
|
5
6
|
import { defaultOrderBy } from "../list-order.js";
|
|
6
7
|
import { countQuery } from "../pages/use-layout-data.js";
|
|
7
8
|
import { useFilterChips } from "./use-filter-chips.js";
|
|
8
|
-
import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
|
|
9
9
|
import { EmbeddedList } from "../embedded-list/embedded-list.js";
|
|
10
10
|
import { addedFilter, filterChanges, netChange, removedFilter } from "./search-diff.js";
|
|
11
11
|
import { useMemo } from "react";
|
|
@@ -3,8 +3,8 @@ import { recordKey } from "../../live/model-helpers.js";
|
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
4
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
5
5
|
import { useAdminMeta } from "../meta-gate.js";
|
|
6
|
-
import { can } from "../permissions.js";
|
|
7
6
|
import { useAppAddress } from "../use-app-address.js";
|
|
7
|
+
import { can } from "../permissions.js";
|
|
8
8
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
10
10
|
import { useOpenedSearch } from "./use-opened-search.js";
|
|
@@ -2,9 +2,9 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { Input } from "../primitives/input.js";
|
|
3
3
|
import { Button } from "../primitives/button.js";
|
|
4
4
|
import { humanize } from "../live/naming.js";
|
|
5
|
-
import { displayValue } from "../live/display-value.js";
|
|
6
|
-
import { ErrorBanner } from "./error-banner.js";
|
|
7
5
|
import { toDraft } from "../live/field-values.js";
|
|
6
|
+
import { ErrorBanner } from "./error-banner.js";
|
|
7
|
+
import { displayValue } from "../live/display-value.js";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { Eye, EyeOff } from "lucide-react";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -50,8 +50,7 @@ const useNavRecent = (meta, basePath, active) => {
|
|
|
50
50
|
useFollowRecords(groups);
|
|
51
51
|
const results = useQueries({ queries: groups.map((group) => ({
|
|
52
52
|
queryKey: groupKey(group),
|
|
53
|
-
queryFn: () => client.list(group.model.name, group.request)
|
|
54
|
-
refetchOnWindowFocus: true
|
|
53
|
+
queryFn: () => client.list(group.model.name, group.request)
|
|
55
54
|
})) });
|
|
56
55
|
return Object.fromEntries(groups.map(({ model, view, recent }, index) => {
|
|
57
56
|
const result = results[index];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../data/api-provider.js";
|
|
3
|
-
import { displayValue } from "../live/display-value.js";
|
|
4
3
|
import { differs, toDraft, toPatchValue } from "../live/field-values.js";
|
|
4
|
+
import { displayValue } from "../live/display-value.js";
|
|
5
5
|
import { useLiveSave } from "./use-live-save.js";
|
|
6
6
|
import { readDraft, writeDraft } from "./draft-storage.js";
|
|
7
7
|
import { isConflict, useUpdateRecord } from "../data/use-update-record.js";
|
|
@@ -2,8 +2,8 @@ import { RouteLink } from "../primitives/route-link.js";
|
|
|
2
2
|
import { useAuth } from "./auth-provider.js";
|
|
3
3
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
4
4
|
import { useSignInAddress } from "./use-sign-in-address.js";
|
|
5
|
-
import { namedIcon } from "../app/named-icon.js";
|
|
6
5
|
import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
|
|
6
|
+
import { namedIcon } from "../app/named-icon.js";
|
|
7
7
|
import { useId } from "react";
|
|
8
8
|
import { Check, LogIn } from "lucide-react";
|
|
9
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -97,7 +97,8 @@ export type ApiProviderProps = {
|
|
|
97
97
|
/**
|
|
98
98
|
* Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
|
|
99
99
|
* compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
|
|
100
|
-
* per-caller body, so the two never match.
|
|
100
|
+
* per-caller body, so the two never match. The data on screen refetches whenever the window regains focus, unless a
|
|
101
|
+
* form holds unsaved changes (`usePauseFocusRefresh`).
|
|
101
102
|
*/
|
|
102
103
|
export declare const ApiProvider: ({ baseUrl, client: given, sources, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }: ApiProviderProps) => import("react").JSX.Element;
|
|
103
104
|
//#endregion
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { FocusRefreshContext, createFocusRefresh, listenToWindowFocus } from "./focus-refresh.js";
|
|
1
2
|
import { keys } from "./query-keys.js";
|
|
2
|
-
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { createClient } from "@protobase/client";
|
|
4
5
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
import { QueryClient, QueryClientProvider, focusManager } from "@tanstack/react-query";
|
|
6
7
|
//#region src/data/api-provider.tsx
|
|
7
8
|
const ApiContext = createContext(null);
|
|
8
9
|
const SourceClientsContext = createContext(null);
|
|
@@ -44,14 +45,17 @@ const ClientProvider = ({ client, resourceClient, children }) => /* @__PURE__ */
|
|
|
44
45
|
/**
|
|
45
46
|
* Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
|
|
46
47
|
* compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
|
|
47
|
-
* per-caller body, so the two never match.
|
|
48
|
+
* per-caller body, so the two never match. The data on screen refetches whenever the window regains focus, unless a
|
|
49
|
+
* form holds unsaved changes (`usePauseFocusRefresh`).
|
|
48
50
|
*/
|
|
49
51
|
const ApiProvider = ({ baseUrl, client: given, sources, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }) => {
|
|
52
|
+
const [focusRefresh] = useState(createFocusRefresh);
|
|
50
53
|
const [queryClient] = useState(() => givenQueryClient ?? new QueryClient({ defaultOptions: { queries: {
|
|
51
54
|
staleTime: 3e4,
|
|
52
55
|
retry: false,
|
|
53
|
-
refetchOnWindowFocus:
|
|
56
|
+
refetchOnWindowFocus: focusRefresh.refetchOnWindowFocus
|
|
54
57
|
} } }));
|
|
58
|
+
useEffect(() => focusManager.setEventListener(listenToWindowFocus), []);
|
|
55
59
|
const metaVersion = useRef(void 0);
|
|
56
60
|
const client = useMemo(() => given ?? createClient({
|
|
57
61
|
baseUrl,
|
|
@@ -104,7 +108,10 @@ const ApiProvider = ({ baseUrl, client: given, sources, fetch, queryClient: give
|
|
|
104
108
|
value: client,
|
|
105
109
|
children: /* @__PURE__ */ jsx(SourceClientsContext.Provider, {
|
|
106
110
|
value: sourceClient,
|
|
107
|
-
children
|
|
111
|
+
children: /* @__PURE__ */ jsx(FocusRefreshContext.Provider, {
|
|
112
|
+
value: focusRefresh,
|
|
113
|
+
children
|
|
114
|
+
})
|
|
108
115
|
})
|
|
109
116
|
})
|
|
110
117
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect } from "react";
|
|
2
|
+
//#region src/data/focus-refresh.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether the data on screen refetches when the window regains focus: always, unless a form on screen holds unsaved
|
|
5
|
+
* changes. A refetch under such a form would hand it a newer version of the record without the changes it was edited
|
|
6
|
+
* from, so its Save would no longer find a conflict.
|
|
7
|
+
*/
|
|
8
|
+
const createFocusRefresh = () => {
|
|
9
|
+
let paused = 0;
|
|
10
|
+
return {
|
|
11
|
+
refetchOnWindowFocus: () => paused > 0 ? false : "always",
|
|
12
|
+
/** Holds the refetches until the returned function is called. */
|
|
13
|
+
pause: () => {
|
|
14
|
+
paused += 1;
|
|
15
|
+
let released = false;
|
|
16
|
+
return () => {
|
|
17
|
+
if (released) return;
|
|
18
|
+
released = true;
|
|
19
|
+
paused -= 1;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const FocusRefreshContext = createContext(null);
|
|
25
|
+
/** Holds the app's refetches on window focus while `paused`, such as while a form has unsaved changes. */
|
|
26
|
+
const usePauseFocusRefresh = (paused) => {
|
|
27
|
+
const refresh = useContext(FocusRefreshContext);
|
|
28
|
+
useEffect(() => paused && refresh ? refresh.pause() : void 0, [paused, refresh]);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Tells TanStack Query the window regained focus on a return to the browser tab, as by default, and on a return to the
|
|
32
|
+
* browser window from another program, which leaves the tab visible.
|
|
33
|
+
*/
|
|
34
|
+
const listenToWindowFocus = (onFocus) => {
|
|
35
|
+
const listener = () => onFocus();
|
|
36
|
+
window.addEventListener("visibilitychange", listener, false);
|
|
37
|
+
window.addEventListener("focus", listener, false);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener("visibilitychange", listener);
|
|
40
|
+
window.removeEventListener("focus", listener);
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { FocusRefreshContext, createFocusRefresh, listenToWindowFocus, usePauseFocusRefresh };
|
package/dist/data/query-keys.js
CHANGED
|
@@ -25,6 +25,13 @@ const keys = {
|
|
|
25
25
|
pageSize,
|
|
26
26
|
page
|
|
27
27
|
],
|
|
28
|
+
/** Every `page` of one list. */
|
|
29
|
+
pages: (resource, filter, orderBy) => [
|
|
30
|
+
"page",
|
|
31
|
+
resource,
|
|
32
|
+
filter,
|
|
33
|
+
orderBy
|
|
34
|
+
],
|
|
28
35
|
/** A sidebar group's records; starts with `list`, so invalidating a resource's lists refreshes it too. */
|
|
29
36
|
navRecent: (resource, filter, orderBy, pageSize) => [
|
|
30
37
|
"list",
|
|
@@ -45,6 +52,12 @@ const keys = {
|
|
|
45
52
|
resource,
|
|
46
53
|
key
|
|
47
54
|
],
|
|
55
|
+
/** What a list row knows of a record, which its page shows until the record has loaded. */
|
|
56
|
+
recordPreview: (resource, key) => [
|
|
57
|
+
"record-preview",
|
|
58
|
+
resource,
|
|
59
|
+
key
|
|
60
|
+
],
|
|
48
61
|
facets: (resource, field, filter) => [
|
|
49
62
|
"facets",
|
|
50
63
|
resource,
|
package/dist/data/use-record.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { encodeKey } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/data/use-record.ts
|
|
6
6
|
const useRecord = (resource, key, enabled = true) => {
|
|
@@ -12,5 +12,20 @@ const useRecord = (resource, key, enabled = true) => {
|
|
|
12
12
|
queryFn: () => client.get(resource, key)
|
|
13
13
|
});
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* `useRecord` for the record's page: until the record has loaded the first time, `data` is what the list row it was
|
|
17
|
+
* opened from knows of it, when it was opened from one, with `isPlaceholderData` set: only the fields the list loaded.
|
|
18
|
+
*/
|
|
19
|
+
const useOpenedRecord = (resource, key, enabled = true) => {
|
|
20
|
+
const client = useClient();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const encoded = encodeKey(key);
|
|
23
|
+
return useQuery({
|
|
24
|
+
queryKey: keys.record(resource, encoded),
|
|
25
|
+
enabled,
|
|
26
|
+
queryFn: () => client.get(resource, key),
|
|
27
|
+
placeholderData: () => queryClient.getQueryData(keys.recordPreview(resource, encoded))
|
|
28
|
+
});
|
|
29
|
+
};
|
|
15
30
|
//#endregion
|
|
16
|
-
export { useRecord };
|
|
31
|
+
export { useOpenedRecord, useRecord };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/data-grid/cell-moves.ts
|
|
2
|
+
const isMoveKey = (key) => key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight";
|
|
3
|
+
/** The cell an arrow key moves to from `at` among `total` rows of `columns`, or undefined at the grid's edge. */
|
|
4
|
+
const movedCell = (at, key, columns, total) => {
|
|
5
|
+
const column = columns.indexOf(at.column);
|
|
6
|
+
if (key === "ArrowUp") return at.index > 0 ? {
|
|
7
|
+
...at,
|
|
8
|
+
index: at.index - 1
|
|
9
|
+
} : void 0;
|
|
10
|
+
if (key === "ArrowDown") return at.index < total - 1 ? {
|
|
11
|
+
...at,
|
|
12
|
+
index: at.index + 1
|
|
13
|
+
} : void 0;
|
|
14
|
+
const next = columns[key === "ArrowLeft" ? column - 1 : column + 1];
|
|
15
|
+
return column >= 0 && next !== void 0 ? {
|
|
16
|
+
...at,
|
|
17
|
+
column: next
|
|
18
|
+
} : void 0;
|
|
19
|
+
};
|
|
20
|
+
/** The editable column after (`1`) or before (`-1`) `column` in its row, or undefined when there is none. */
|
|
21
|
+
const nextEditable = (column, columns, editable, direction) => {
|
|
22
|
+
const from = columns.indexOf(column);
|
|
23
|
+
return (direction === 1 ? columns.slice(from + 1) : columns.slice(0, Math.max(0, from)).reverse()).find(editable);
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { isMoveKey, movedCell, nextEditable };
|