@protobase/ui 0.19.15 → 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/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-displays.js +13 -1
- package/dist/app/displays/record-card.js +16 -5
- 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 +9 -0
- package/dist/app/export/export-options.js +2 -2
- package/dist/app/gallery/use-gallery-photos.js +2 -2
- 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 +28 -10
- package/dist/app/map/map-area.js +6 -6
- package/dist/app/pages/action-href.js +4 -2
- package/dist/app/pages/use-run-action.js +4 -3
- package/dist/app/record-page.js +30 -16
- package/dist/app/record-preview.js +25 -0
- package/dist/app/record-row/record-row-body.js +2 -2
- package/dist/app/record-row/record-row.js +1 -0
- 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 +4 -4
- 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.js +3 -3
- package/dist/app/searches/search-diff.js +8 -8
- package/dist/app/use-nav-recent.js +1 -2
- 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/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/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/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-CzV9U_NJ.js → index-CmmYBR7Y.js} +1 -1
- package/dist/shell/assets/{maplibre-G35v6TYU.js → maplibre-Siom2Zwj.js} +1 -1
- package/dist/shell/assets/{markdown-content-DGFnggSX.js → markdown-content-ChyGEubR.js} +1 -1
- package/dist/shell/assets/{record-map-ZAC_U52Y.js → record-map-B_QnHF99.js} +1 -1
- package/dist/shell/assets/{records-map-DLXOQVpM.js → records-map-CcHhEDBC.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-xByMDSPg.js → shared-protobase-ui-zRmreKZS.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-WromtADN.js +0 -69
- package/dist/shell/assets/index-DEEIUcYb.css +0 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isEditable } from "../../live/field-values.js";
|
|
2
|
+
import { can, rowAllows } from "../permissions.js";
|
|
3
|
+
import { CellFieldEditor } from "./cell-field-editor.js";
|
|
4
|
+
import { useListEdits } from "./use-list-edits.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/app/list-edit/use-list-cell-editor.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The list's cells edited in place: the view's `editable` columns, which `/meta` keeps to the fields the caller may
|
|
9
|
+
* write, of the rows the caller may change. `patches` are the values saved since the rows loaded, for the grid to show.
|
|
10
|
+
*/
|
|
11
|
+
const useListCellEditor = (model, view, permissions) => {
|
|
12
|
+
const edits = useListEdits(model);
|
|
13
|
+
const columns = view?.list?.editable ?? [];
|
|
14
|
+
return {
|
|
15
|
+
editor: columns.length > 0 && can(permissions, "update") ? {
|
|
16
|
+
editable: (row, column) => columns.includes(column) && model.fields[column] !== void 0 && isEditable(model.fields[column]) && rowAllows(row, "update"),
|
|
17
|
+
render: (row, column, close) => /* @__PURE__ */ jsx(CellFieldEditor, {
|
|
18
|
+
field: model.fields[column],
|
|
19
|
+
hints: view?.fields[column],
|
|
20
|
+
resource: model.name,
|
|
21
|
+
stored: row[column],
|
|
22
|
+
onSave: (value) => void edits.save(row, column, value),
|
|
23
|
+
close
|
|
24
|
+
})
|
|
25
|
+
} : void 0,
|
|
26
|
+
patches: edits.patches
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { useListCellEditor };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { keys } from "../../data/query-keys.js";
|
|
2
|
+
import { useClient } from "../../data/api-provider.js";
|
|
3
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
4
|
+
import { useToast } from "../../toasts/toast-provider.js";
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
|
+
import { isApiError } from "@protobase/client";
|
|
7
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
9
|
+
//#region src/app/list-edit/use-list-edits.ts
|
|
10
|
+
/**
|
|
11
|
+
* The list's edits in place: `patches` holds, by row id, the values saving or saved since the rows loaded, with the
|
|
12
|
+
* ETag each saved row has now. `save` sends one field as a PATCH with the row's ETag, after any save of the same row
|
|
13
|
+
* still under way. A refusal puts the old value back, or the record's current one when someone else changed it, and
|
|
14
|
+
* says why in a toast.
|
|
15
|
+
*/
|
|
16
|
+
const useListEdits = (model) => {
|
|
17
|
+
const client = useClient();
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const toast = useToast();
|
|
20
|
+
const [patches, setPatches] = useState(/* @__PURE__ */ new Map());
|
|
21
|
+
const current = useRef(patches);
|
|
22
|
+
const queues = useRef(/* @__PURE__ */ new Map());
|
|
23
|
+
const patch = (id, values) => {
|
|
24
|
+
current.current = new Map(current.current).set(id, {
|
|
25
|
+
...current.current.get(id),
|
|
26
|
+
...values
|
|
27
|
+
});
|
|
28
|
+
setPatches(current.current);
|
|
29
|
+
};
|
|
30
|
+
const send = async (row, name, value) => {
|
|
31
|
+
const key = recordKey(model, row);
|
|
32
|
+
const etag = String(current.current.get(row.id)?.etag ?? row.etag);
|
|
33
|
+
try {
|
|
34
|
+
const stored = await client.update(model.name, key, { [name]: value }, etag);
|
|
35
|
+
patch(row.id, {
|
|
36
|
+
[name]: stored.record[name],
|
|
37
|
+
etag: stored.etag
|
|
38
|
+
});
|
|
39
|
+
queryClient.setQueryData(keys.record(model.name, encodeKey(key)), stored);
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ["page", model.name] });
|
|
41
|
+
queryClient.invalidateQueries({ queryKey: ["list", model.name] });
|
|
42
|
+
queryClient.invalidateQueries({ queryKey: ["map", model.name] });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!isApiError(error)) {
|
|
45
|
+
patch(row.id, { [name]: row[name] });
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const changed = error.status === 412;
|
|
49
|
+
const latest = changed ? await client.get(model.name, key) : void 0;
|
|
50
|
+
patch(row.id, latest ? {
|
|
51
|
+
[name]: latest.record[name],
|
|
52
|
+
etag: latest.etag
|
|
53
|
+
} : { [name]: row[name] });
|
|
54
|
+
const reason = error.errors.find((entry) => entry.field === name)?.message ?? error.message;
|
|
55
|
+
toast.show({
|
|
56
|
+
state: "error",
|
|
57
|
+
title: changed ? "Changed by someone else" : "Could not save",
|
|
58
|
+
description: reason
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** Saves `value` as the field `name` of `row`, as the grid shows it before the edit. */
|
|
63
|
+
const save = (row, name, value) => {
|
|
64
|
+
patch(row.id, { [name]: value });
|
|
65
|
+
const queued = (queues.current.get(row.id) ?? Promise.resolve()).then(() => send(row, name, value));
|
|
66
|
+
queues.current.set(row.id, queued.catch(() => void 0));
|
|
67
|
+
return queued;
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
patches,
|
|
71
|
+
save
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { useListEdits };
|
package/dist/app/list-page.js
CHANGED
|
@@ -9,7 +9,7 @@ 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 { loadedFields, shownDisplay } from "./displays/list-displays.js";
|
|
12
|
+
import { displayRow, loadedFields, shownDisplay } from "./displays/list-displays.js";
|
|
13
13
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
14
14
|
import { listedResource } from "./current-address.js";
|
|
15
15
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
@@ -26,7 +26,11 @@ import { FilterPanel } from "../filter-panel/filter-panel.js";
|
|
|
26
26
|
import { stateToText, textToState } from "../live/filter-string.js";
|
|
27
27
|
import { widgetFields } from "../live/view-fields.js";
|
|
28
28
|
import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
29
|
+
import { useSeedRecord } from "./record-preview.js";
|
|
29
30
|
import { SignInPrompt } from "../auth/sign-in-prompt.js";
|
|
31
|
+
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
32
|
+
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
33
|
+
import { BoardDisplay } from "./displays/board-display.js";
|
|
30
34
|
import { CustomDisplay } from "./displays/custom-display.js";
|
|
31
35
|
import { MapDisplay } from "./displays/map-display.js";
|
|
32
36
|
import { DisplaySwitch, SortMenu } from "./displays/display-controls.js";
|
|
@@ -39,22 +43,21 @@ import { ImportButton } from "./import/import-button.js";
|
|
|
39
43
|
import { exportOffered, exportsSelection } from "./export/export-options.js";
|
|
40
44
|
import { useExportFlow } from "./export/use-export-flow.js";
|
|
41
45
|
import { ListCards } from "./displays/list-cards.js";
|
|
42
|
-
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
43
46
|
import { useListScroll } from "./list-scroll.js";
|
|
44
47
|
import { ListChart } from "./list-chart.js";
|
|
45
48
|
import { ListSearch } from "./list-search.js";
|
|
46
|
-
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
47
49
|
import { withRelatedDotsColumns } from "./related-dots/related-dots-columns.js";
|
|
48
50
|
import { SavedList } from "./saved/saved-list.js";
|
|
49
51
|
import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
50
52
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
53
|
+
import { useListCellEditor } from "./list-edit/use-list-cell-editor.js";
|
|
51
54
|
import { useMemo, useState } from "react";
|
|
52
55
|
import { Download, Plus, SlidersHorizontal } from "lucide-react";
|
|
53
56
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
54
57
|
import { createWhere } from "@protobase/schema";
|
|
55
58
|
import { tableDisplay } from "@protobase/schema/internal";
|
|
56
59
|
//#region src/app/list-page.tsx
|
|
57
|
-
const where = createWhere();
|
|
60
|
+
const where$1 = createWhere();
|
|
58
61
|
const searchText = (extras) => extras.find((clause) => clause.kind === "search");
|
|
59
62
|
/**
|
|
60
63
|
* A resource's list: its rows, with `listOf` the records its relation points at (`SavedList`), or with `searchesOf` the
|
|
@@ -169,6 +172,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
169
172
|
});
|
|
170
173
|
const singular = view?.names?.singular ?? humanize(resource);
|
|
171
174
|
const permissions = usePermissions(resource);
|
|
175
|
+
const cellEditing = useListCellEditor(model, view, permissions);
|
|
172
176
|
const { state: auth } = useAuth();
|
|
173
177
|
const restore = useRestore(model);
|
|
174
178
|
const remove = useDeleteFlow({
|
|
@@ -185,7 +189,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
185
189
|
const search = searchText(extras);
|
|
186
190
|
const commitSearch = (text) => {
|
|
187
191
|
const others = extras.filter((clause) => clause.kind !== "search");
|
|
188
|
-
setFilter(state, text ? [...others, where.search(text)] : others);
|
|
192
|
+
setFilter(state, text ? [...others, where$1.search(text)] : others);
|
|
189
193
|
};
|
|
190
194
|
const plural = view?.names?.plural ?? humanize(resource);
|
|
191
195
|
const sortMenu = display && /* @__PURE__ */ jsx(SortMenu, {
|
|
@@ -309,17 +313,23 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
309
313
|
onChange: (name) => set({ display: name }),
|
|
310
314
|
menu
|
|
311
315
|
});
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
const seedRecord = useSeedRecord(model);
|
|
317
|
+
const openRecord = (row) => {
|
|
318
|
+
seedRecord(displayRow(model, row).record);
|
|
319
|
+
go({
|
|
320
|
+
kind: "record",
|
|
321
|
+
resource,
|
|
322
|
+
key: recordKey(model, row)
|
|
323
|
+
});
|
|
324
|
+
};
|
|
317
325
|
const table = rows.source ? /* @__PURE__ */ jsx(DataGrid, {
|
|
318
326
|
columns,
|
|
319
327
|
source: rows.source,
|
|
320
328
|
sort: parseSort(orderBy),
|
|
321
329
|
onSortChange: (sort) => set({ orderBy: sortToOrderBy(sort) }),
|
|
322
330
|
onOpenRow: openRecord,
|
|
331
|
+
cellEditor: cellEditing.editor,
|
|
332
|
+
rowPatches: cellEditing.patches,
|
|
323
333
|
initialScrollIndex: scroll.initialIndex,
|
|
324
334
|
onScrollIndex: scroll.remember,
|
|
325
335
|
rowActions: [{
|
|
@@ -368,6 +378,14 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
368
378
|
onMove: (map) => set({ map }),
|
|
369
379
|
flush: true
|
|
370
380
|
});
|
|
381
|
+
if (display.kind === "board") return /* @__PURE__ */ jsx(BoardDisplay, {
|
|
382
|
+
model,
|
|
383
|
+
view,
|
|
384
|
+
display,
|
|
385
|
+
fields: loaded,
|
|
386
|
+
filter: loadFilter,
|
|
387
|
+
orderBy
|
|
388
|
+
});
|
|
371
389
|
return /* @__PURE__ */ jsx(CustomDisplay, {
|
|
372
390
|
display,
|
|
373
391
|
list: displayRows
|
package/dist/app/map/map-area.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
2
2
|
//#region src/app/map/map-area.ts
|
|
3
|
-
const where = createWhere();
|
|
3
|
+
const where$1 = createWhere();
|
|
4
4
|
/** The whole world from as far out as a map goes: what a map asks for before it knows where its records are. */
|
|
5
5
|
const worldArea = {
|
|
6
6
|
bounds: {
|
|
@@ -20,11 +20,11 @@ const wrapped = (longitude) => ((longitude + 180) % 360 + 360) % 360 - 180;
|
|
|
20
20
|
* A view across the antimeridian takes the longitudes on both sides of it, and one around the whole world takes them all.
|
|
21
21
|
*/
|
|
22
22
|
const boundsFilter = (fields, bounds) => {
|
|
23
|
-
const latitude = [where.gte(fields.latitude, down(Math.max(bounds.south, -90))), where.lte(fields.latitude, up(Math.min(bounds.north, 90)))];
|
|
24
|
-
if (bounds.east - bounds.west >= 360) return where.and(...latitude);
|
|
25
|
-
const west = where.gte(fields.longitude, down(wrapped(bounds.west)));
|
|
26
|
-
const east = where.lte(fields.longitude, up(wrapped(bounds.east)));
|
|
27
|
-
return wrapped(bounds.west) <= wrapped(bounds.east) ? where.and(...latitude, west, east) : where.and(...latitude, where.or(west, east));
|
|
23
|
+
const latitude = [where$1.gte(fields.latitude, down(Math.max(bounds.south, -90))), where$1.lte(fields.latitude, up(Math.min(bounds.north, 90)))];
|
|
24
|
+
if (bounds.east - bounds.west >= 360) return where$1.and(...latitude);
|
|
25
|
+
const west = where$1.gte(fields.longitude, down(wrapped(bounds.west)));
|
|
26
|
+
const east = where$1.lte(fields.longitude, up(wrapped(bounds.east)));
|
|
27
|
+
return wrapped(bounds.west) <= wrapped(bounds.east) ? where$1.and(...latitude, west, east) : where$1.and(...latitude, where$1.or(west, east));
|
|
28
28
|
};
|
|
29
29
|
/** The list's filter, its search included, narrowed to the records inside the map's bounds. */
|
|
30
30
|
const areaFilter = (listFilter, fields, bounds) => {
|
|
@@ -25,12 +25,14 @@ const openLink = (target) => {
|
|
|
25
25
|
else window.location.assign(target);
|
|
26
26
|
};
|
|
27
27
|
const hostOf = (target) => URL.canParse(target) ? new URL(target).hostname.replace(/^www\./, "") : "";
|
|
28
|
+
const holdsAll = (record, values) => Object.entries(values).every(([name, value]) => Object.is(record[name], value));
|
|
28
29
|
/**
|
|
29
|
-
* `action` as it shows for `record`:
|
|
30
|
-
* becomes the host its target opens, without `www.`.
|
|
30
|
+
* `action` as it shows for `record`: an update that would change nothing there, and a link whose `href` has no target
|
|
31
|
+
* there, are left out, and `{host}` in a link's label becomes the host its target opens, without `www.`.
|
|
31
32
|
*/
|
|
32
33
|
const actionFor = (action, record) => {
|
|
33
34
|
const run = action.run;
|
|
35
|
+
if (run?.kind === "update") return record && holdsAll(record, run.values) ? void 0 : action;
|
|
34
36
|
if (run?.kind !== "link" || !("href" in run)) return action;
|
|
35
37
|
const target = linkTarget(run.href, record);
|
|
36
38
|
if (target === void 0) return void 0;
|
|
@@ -12,9 +12,10 @@ import { fillAddress } from "@protobase/schema/internal";
|
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
14
14
|
* The named actions of `resource`'s view, ready to run on `scope` (the record they act on): built-in behaviour first
|
|
15
|
-
* (`update`, `delete`, `link`), else the handler the project registered under the action's name.
|
|
16
|
-
* record holds is left out while it holds none. `blocked` says
|
|
17
|
-
* that succeeded. `submit` runs one with the answers to its
|
|
15
|
+
* (`update`, `delete`, `link`), else the handler the project registered under the action's name. An update whose values
|
|
16
|
+
* the record holds already is left out, and so is a link to a URL the record holds while it holds none. `blocked` says
|
|
17
|
+
* why one cannot run; `afterRun` hears about every action that succeeded. `submit` runs one with the answers to its
|
|
18
|
+
* inputs and throws its failure, for the dialog to show.
|
|
18
19
|
*/
|
|
19
20
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
20
21
|
const { views } = useAdminMeta();
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Skeleton } from "../primitives/skeleton.js";
|
|
2
3
|
import { inApp } from "../app-shell/breadcrumb.js";
|
|
3
4
|
import { useAuth } from "../auth/auth-provider.js";
|
|
4
5
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
6
|
+
import { usePauseFocusRefresh } from "../data/focus-refresh.js";
|
|
5
7
|
import { keys } from "../data/query-keys.js";
|
|
6
8
|
import { useClient } from "../data/api-provider.js";
|
|
7
9
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
8
|
-
import {
|
|
10
|
+
import { useOpenedRecord } from "../data/use-record.js";
|
|
9
11
|
import { recordTitle } from "../live/model-helpers.js";
|
|
10
12
|
import { humanize } from "../live/naming.js";
|
|
11
13
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
@@ -16,6 +18,7 @@ import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record
|
|
|
16
18
|
import { RecordHeader } from "../record-view/record-header.js";
|
|
17
19
|
import { SpecTable } from "../record-view/spec-table.js";
|
|
18
20
|
import { KeyFacts } from "../record-view/key-facts.js";
|
|
21
|
+
import { isRequiredOnEdit } from "../live/field-values.js";
|
|
19
22
|
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
20
23
|
import { ErrorBanner } from "./error-banner.js";
|
|
21
24
|
import { displayValue } from "../live/display-value.js";
|
|
@@ -70,7 +73,7 @@ const layoutFor = (model, view, read) => {
|
|
|
70
73
|
const RecordPage = ({ resource, recordKey }) => {
|
|
71
74
|
const { resources } = useAdminMeta();
|
|
72
75
|
const model = resources[resource];
|
|
73
|
-
const record =
|
|
76
|
+
const record = useOpenedRecord(resource, recordKey);
|
|
74
77
|
if (!model) return /* @__PURE__ */ jsxs(Notice, {
|
|
75
78
|
title: "Not found",
|
|
76
79
|
children: [
|
|
@@ -96,10 +99,11 @@ const RecordPage = ({ resource, recordKey }) => {
|
|
|
96
99
|
return /* @__PURE__ */ jsx(LoadedRecord, {
|
|
97
100
|
model,
|
|
98
101
|
recordKey,
|
|
99
|
-
stored: record.data
|
|
100
|
-
|
|
102
|
+
stored: record.data,
|
|
103
|
+
preview: record.isPlaceholderData
|
|
104
|
+
}, record.isPlaceholderData ? "preview" : "loaded");
|
|
101
105
|
};
|
|
102
|
-
const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
106
|
+
const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
103
107
|
const meta = useAdminMeta();
|
|
104
108
|
const { basePath } = useRouter();
|
|
105
109
|
const { address, go, open } = useAppAddress();
|
|
@@ -115,7 +119,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
115
119
|
recordKey,
|
|
116
120
|
mode: view?.saveFeedback ?? "toast",
|
|
117
121
|
subject: title,
|
|
118
|
-
draftKey: auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
122
|
+
draftKey: !preview && auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
119
123
|
lines: linesDraft
|
|
120
124
|
});
|
|
121
125
|
const queryClient = useQueryClient();
|
|
@@ -125,6 +129,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
125
129
|
const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
|
|
126
130
|
const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
|
|
127
131
|
useNavigationGuard(editor.dirty && shown === "form");
|
|
132
|
+
usePauseFocusRefresh(editor.dirty);
|
|
128
133
|
const readAddress = {
|
|
129
134
|
kind: "record",
|
|
130
135
|
resource: model.name,
|
|
@@ -156,6 +161,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
156
161
|
const updatedAt = auditField(model, "updated");
|
|
157
162
|
const hint = (name) => view?.fields[name];
|
|
158
163
|
const visible = (name) => fieldAccess(stored.permissions, name) !== "hidden" && whenShown(hint(name)?.when, editing);
|
|
164
|
+
const known = (name) => !preview || Object.hasOwn(stored.record, name);
|
|
159
165
|
const fieldView = (name) => {
|
|
160
166
|
const field = model.fields[name];
|
|
161
167
|
const access = fieldAccess(stored.permissions, name);
|
|
@@ -163,6 +169,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
163
169
|
const conflict = editor.conflicts[name];
|
|
164
170
|
return /* @__PURE__ */ jsx(Field, {
|
|
165
171
|
label: hint(name)?.label ?? humanize(name),
|
|
172
|
+
required: shown === "form" && access !== "read" && isRequiredOnEdit(field),
|
|
166
173
|
help: fieldHelp(hint(name), view, editing),
|
|
167
174
|
error: editor.errors[name],
|
|
168
175
|
wide: Boolean(hint(name)?.multiline || hint(name)?.markdown),
|
|
@@ -177,7 +184,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
177
184
|
onKeepMine: () => editor.keepMine(name),
|
|
178
185
|
onUseTheirs: () => editor.useTheirs(name)
|
|
179
186
|
},
|
|
180
|
-
children: /* @__PURE__ */ jsx(FieldEditor, {
|
|
187
|
+
children: known(name) ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
181
188
|
field,
|
|
182
189
|
hints: hint(name),
|
|
183
190
|
value: editor.value(name),
|
|
@@ -189,12 +196,12 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
189
196
|
reveal: field.sensitive ? () => client.reveal(model.name, recordKey, name) : void 0,
|
|
190
197
|
resource: model.name,
|
|
191
198
|
onChange: (next) => editor.change(name, next)
|
|
192
|
-
})
|
|
199
|
+
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-9" })
|
|
193
200
|
}, field.sensitive ? `${name}:${stored.etag}` : name);
|
|
194
201
|
};
|
|
195
202
|
const readRow = (name) => {
|
|
196
203
|
const field = model.fields[name];
|
|
197
|
-
const value = field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
204
|
+
const value = !known(name) ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }) : field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
198
205
|
field,
|
|
199
206
|
hints: hint(name),
|
|
200
207
|
value: "",
|
|
@@ -222,6 +229,10 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
222
229
|
rows: sidebarFields.flatMap((name) => {
|
|
223
230
|
const field = model.fields[name];
|
|
224
231
|
if (!field || !visible(name)) return [];
|
|
232
|
+
if (!known(name)) return [{
|
|
233
|
+
label: hint(name)?.label ?? humanize(name),
|
|
234
|
+
value: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
235
|
+
}];
|
|
225
236
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
226
237
|
const value = stored.record[name];
|
|
227
238
|
const markdown = isMarkdown(field, hint(name)) && typeof value === "string" && value !== "";
|
|
@@ -284,6 +295,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
284
295
|
] });
|
|
285
296
|
return /* @__PURE__ */ jsxs("div", {
|
|
286
297
|
className: "flex flex-1 flex-col",
|
|
298
|
+
inert: preview,
|
|
287
299
|
children: [
|
|
288
300
|
remove.dialogs,
|
|
289
301
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -355,6 +367,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
355
367
|
recordKey,
|
|
356
368
|
owner: model.name
|
|
357
369
|
}, `related:${item.title}`)];
|
|
370
|
+
if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
|
|
358
371
|
if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
|
|
359
372
|
item,
|
|
360
373
|
scope
|
|
@@ -385,7 +398,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
385
398
|
children: fields.map(fieldView)
|
|
386
399
|
})
|
|
387
400
|
}, item.title)];
|
|
388
|
-
}), linesModel && /* @__PURE__ */ jsx(RecordLines, {
|
|
401
|
+
}), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
|
|
389
402
|
lines: linesModel,
|
|
390
403
|
draft: linesDraft,
|
|
391
404
|
vatRate: Number(stored.record.vatRate ?? 21) / 100,
|
|
@@ -399,7 +412,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
399
412
|
className: "hidden flex-col items-center gap-2 md:flex",
|
|
400
413
|
children: formControls
|
|
401
414
|
}),
|
|
402
|
-
/* @__PURE__ */ jsx(RecordActions, {
|
|
415
|
+
!preview && /* @__PURE__ */ jsx(RecordActions, {
|
|
403
416
|
model,
|
|
404
417
|
stored,
|
|
405
418
|
recordKey,
|
|
@@ -408,11 +421,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
408
421
|
}),
|
|
409
422
|
firstSidebar === -1 && summary,
|
|
410
423
|
layout.map((item, index) => {
|
|
411
|
-
if (item.kind === "custom" && item.sidebar) return /* @__PURE__ */ jsx(CustomSection, {
|
|
424
|
+
if (item.kind === "custom" && item.sidebar && !preview) return /* @__PURE__ */ jsx(CustomSection, {
|
|
412
425
|
item,
|
|
413
426
|
scope
|
|
414
427
|
}, `custom:${index}`);
|
|
415
|
-
if (item.kind === "map" && item.sidebar) return /* @__PURE__ */ jsx(MapSection, {
|
|
428
|
+
if (item.kind === "map" && item.sidebar && !preview) return /* @__PURE__ */ jsx(MapSection, {
|
|
416
429
|
item,
|
|
417
430
|
record: stored.record
|
|
418
431
|
}, `map:${index}`);
|
|
@@ -422,10 +435,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
422
435
|
title: "Details",
|
|
423
436
|
rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
|
|
424
437
|
const field = name ? model.fields[name] : void 0;
|
|
425
|
-
|
|
438
|
+
if (!name || !field || !visible(name)) return [];
|
|
439
|
+
return [{
|
|
426
440
|
label: hint(name)?.label ?? humanize(name),
|
|
427
|
-
value: displayValue(field, stored.record[name], hint(name))
|
|
428
|
-
}]
|
|
441
|
+
value: known(name) ? displayValue(field, stored.record[name], hint(name)) : /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
442
|
+
}];
|
|
429
443
|
})
|
|
430
444
|
}),
|
|
431
445
|
related.records.length > 0 && /* @__PURE__ */ jsx(SidebarCard, {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { keys } from "../data/query-keys.js";
|
|
2
|
+
import { recordKey } from "../live/model-helpers.js";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
6
|
+
//#region src/app/record-preview.ts
|
|
7
|
+
/**
|
|
8
|
+
* A record as far as a list row knows it: the fields the row loaded, its ETag and permissions as the list gives them.
|
|
9
|
+
* `row` holds relations as their keys, as `displayRow` splits a loaded row.
|
|
10
|
+
*/
|
|
11
|
+
const recordPreview = (model, row) => ({
|
|
12
|
+
record: Object.fromEntries(Object.entries(row).filter(([name]) => Object.hasOwn(model.fields, name))),
|
|
13
|
+
etag: typeof row.etag === "string" ? row.etag : "",
|
|
14
|
+
...typeof row.permissions === "object" && row.permissions !== null && { permissions: row.permissions }
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Keeps what a list row knows of its record for the record's page, which shows it until the record has loaded
|
|
18
|
+
* (`useRecord`), so a record opened from a list shows its fields at once.
|
|
19
|
+
*/
|
|
20
|
+
const useSeedRecord = (model) => {
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
return useCallback((row) => queryClient.setQueryData(keys.recordPreview(model.name, encodeKey(recordKey(model, row))), recordPreview(model, row)), [queryClient, model]);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { recordPreview, useSeedRecord };
|
|
@@ -16,12 +16,12 @@ import { ArrowRight } from "lucide-react";
|
|
|
16
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { createWhere, parseFilter, printFilter } from "@protobase/schema";
|
|
18
18
|
//#region src/app/record-row/record-row-body.tsx
|
|
19
|
-
const where = createWhere();
|
|
19
|
+
const where$1 = createWhere();
|
|
20
20
|
const scoped = (filter, scope) => {
|
|
21
21
|
if (!scope) return filter;
|
|
22
22
|
const parsed = parseFilter(filter);
|
|
23
23
|
if (!parsed.ok) throw new Error(`The filter cannot be read: ${parsed.errors.map((error) => error.message).join("; ")}`);
|
|
24
|
-
return printFilter(parsed.ast ? where.and(parsed.ast, scope) : scope);
|
|
24
|
+
return printFilter(parsed.ast ? where$1.and(parsed.ast, scope) : scope);
|
|
25
25
|
};
|
|
26
26
|
/** The display a row draws its cards with: the one named, else the view's first cards display, else a card of the title. */
|
|
27
27
|
const rowDisplay = (view, name) => {
|
|
@@ -26,6 +26,7 @@ const RecordRow = ({ model, view, display, source, label, isNew }) => {
|
|
|
26
26
|
total,
|
|
27
27
|
rowAt: source.rowAt,
|
|
28
28
|
loadRows: source.loadRows,
|
|
29
|
+
revision: source.revision,
|
|
29
30
|
latencyMs: 0
|
|
30
31
|
}, scroll.start, Math.max(scroll.start, scroll.end - 1));
|
|
31
32
|
const indexes = Array.from({ length: scroll.end - scroll.start }, (_, offset) => scroll.start + offset);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/app/related-dots/card-dots-lines.ts
|
|
2
|
+
/**
|
|
3
|
+
* The lines of dots a card of `display` shows for a row, in the order it names their columns: those of the list's
|
|
4
|
+
* related columns that were loaded with the row and hold a record. A row loaded without them, as a map's or a saved
|
|
5
|
+
* search's, shows none.
|
|
6
|
+
*/
|
|
7
|
+
const cardDotsLines = (view, display, row) => (display.dots ?? []).flatMap((name) => {
|
|
8
|
+
const column = view?.list?.relatedColumns?.find((candidate) => candidate.name === name);
|
|
9
|
+
const records = row.record[name];
|
|
10
|
+
return column && Array.isArray(records) && records.length > 0 ? [{
|
|
11
|
+
column,
|
|
12
|
+
records
|
|
13
|
+
}] : [];
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { cardDotsLines };
|
|
@@ -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
|
});
|