@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
|
@@ -2,10 +2,10 @@ import { cn } from "../../lib/cn.js";
|
|
|
2
2
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
4
|
import { useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
5
|
+
import { Section } from "../../record-view/section.js";
|
|
5
6
|
import { shownImage } from "../../live/file-values.js";
|
|
6
7
|
import { ErrorBanner } from "../error-banner.js";
|
|
7
8
|
import { FileImage } from "../../live/file-image.js";
|
|
8
|
-
import { Section } from "../../record-view/section.js";
|
|
9
9
|
import { galleryTiles } from "./gallery-layout.js";
|
|
10
10
|
import { Lightbox } from "./lightbox.js";
|
|
11
11
|
import { detailFields } from "./detail-fields.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { useIsDesktop } from "../../lib/use-media-query.js";
|
|
3
3
|
import { useFocusTrap } from "../../lib/use-focus-trap.js";
|
|
4
|
+
import { Sheet } from "../../primitives/sheet.js";
|
|
4
5
|
import { shownImage } from "../../live/file-values.js";
|
|
5
6
|
import { FileImage } from "../../live/file-image.js";
|
|
6
|
-
import { Sheet } from "../../primitives/sheet.js";
|
|
7
7
|
import { keyStep, steppedPhoto, swipeStep, togglesDetails } from "./gallery-layout.js";
|
|
8
8
|
import { readDetailsOpen, writeDetailsOpen } from "./details-open-storage.js";
|
|
9
9
|
import { useEffect, useId, useRef, useState } from "react";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
2
|
import { useRecord } from "../../data/use-record.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
|
-
import {
|
|
5
|
-
import { AppLink } from "../use-app-address.js";
|
|
4
|
+
import { SpecTable } from "../../record-view/spec-table.js";
|
|
6
5
|
import { ErrorBanner } from "../error-banner.js";
|
|
6
|
+
import { AppLink } from "../use-app-address.js";
|
|
7
|
+
import { fieldAccess } from "../permissions.js";
|
|
7
8
|
import { FieldValue } from "../pages/field-value.js";
|
|
8
|
-
import { SpecTable } from "../../record-view/spec-table.js";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/app/gallery/photo-details.tsx
|
|
11
11
|
/**
|
|
@@ -5,7 +5,7 @@ import { galleryItems } from "./gallery-items.js";
|
|
|
5
5
|
import { useQuery } from "@tanstack/react-query";
|
|
6
6
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
7
7
|
//#region src/app/gallery/use-gallery-photos.ts
|
|
8
|
-
const where = createWhere();
|
|
8
|
+
const where$1 = createWhere();
|
|
9
9
|
/**
|
|
10
10
|
* A gallery's photos: the record's own image, then the photo records that point at it, read with the list API under
|
|
11
11
|
* the user's own access, so its row filters and readable fields apply. Without the photos' resource in `/meta`, the
|
|
@@ -15,7 +15,7 @@ const useGalleryPhotos = (item, record, recordKey) => {
|
|
|
15
15
|
const client = useClient();
|
|
16
16
|
const { resources } = useAdminMeta();
|
|
17
17
|
const holder = item.photos ? resources[item.photos.resource] : void 0;
|
|
18
|
-
const filter = item.photos ? printFilter(where.eq(item.photos.field, recordKey)) : "";
|
|
18
|
+
const filter = item.photos ? printFilter(where$1.eq(item.photos.field, recordKey)) : "";
|
|
19
19
|
const sort = item.photos?.sort ?? "";
|
|
20
20
|
const rows = useQuery({
|
|
21
21
|
queryKey: keys.list(holder?.name ?? "", filter, sort, 500, "gallery"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Button } from "../../primitives/button.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
|
-
import { ErrorBanner } from "../error-banner.js";
|
|
4
3
|
import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
|
|
4
|
+
import { ErrorBanner } from "../error-banner.js";
|
|
5
5
|
import { unmappedRequired } from "./mapping.js";
|
|
6
6
|
import { ImportCheckStep, ImportChecking } from "./import-check-step.js";
|
|
7
7
|
import { ImportFileStep } from "./import-file-step.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
2
2
|
//#region src/app/import/lookups.ts
|
|
3
|
-
const where = createWhere();
|
|
3
|
+
const where$1 = createWhere();
|
|
4
4
|
const valuesPerRequest = 100;
|
|
5
5
|
/** A value as the lookups compare it: the API answers numbers as numbers and the rest as text. */
|
|
6
6
|
const lookupKey = (value) => String(value);
|
|
@@ -23,7 +23,7 @@ const createLookups = (clientFor) => {
|
|
|
23
23
|
for (let at = 0; at < missing.length; at += valuesPerRequest) {
|
|
24
24
|
const asked = missing.slice(at, at + valuesPerRequest);
|
|
25
25
|
for (const value of asked) found.set(lookupKey(value), []);
|
|
26
|
-
const filter = printFilter(where.in(field, asked));
|
|
26
|
+
const filter = printFilter(where$1.in(field, asked));
|
|
27
27
|
let pageToken;
|
|
28
28
|
do {
|
|
29
29
|
const page = await clientFor(model.name).list(model.name, {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { differs, toFieldDraft, toPatchValue } from "../../live/field-values.js";
|
|
2
|
+
import { FieldEditor } from "../field-editor.js";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/app/list-edit/cell-field-editor.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A field's editor in a list cell, the one the record page uses: Enter, Tab and leaving the cell save a change, and Tab
|
|
8
|
+
* and Shift+Tab go on to the next or previous editable cell; Escape leaves it as it was. A built-in switch or choice
|
|
9
|
+
* saves as soon as it changes.
|
|
10
|
+
*/
|
|
11
|
+
const CellFieldEditor = ({ field, hints, resource, stored, onSave, close }) => {
|
|
12
|
+
const [draft, setDraft] = useState(() => toFieldDraft(field, stored));
|
|
13
|
+
const ref = useRef(null);
|
|
14
|
+
const finished = useRef(false);
|
|
15
|
+
const instant = !hints?.widget && (field.type === "boolean" || field.type === "enum");
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
ref.current?.querySelector("input, select, textarea, button, [tabindex]")?.focus();
|
|
18
|
+
}, []);
|
|
19
|
+
const finish = (save, move, value = draft) => {
|
|
20
|
+
if (finished.current) return;
|
|
21
|
+
finished.current = true;
|
|
22
|
+
if (save && differs(field, value, stored)) onSave(toPatchValue(field, value));
|
|
23
|
+
close(move);
|
|
24
|
+
};
|
|
25
|
+
const onKeyDown = (event) => {
|
|
26
|
+
if (event.key === "Enter") {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
finish(true);
|
|
29
|
+
} else if (event.key === "Tab") {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
finish(true, event.shiftKey ? -1 : 1);
|
|
32
|
+
} else if (event.key === "Escape") {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
finish(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const onBlur = (event) => {
|
|
39
|
+
if (!ref.current?.contains(event.relatedTarget)) finish(true);
|
|
40
|
+
};
|
|
41
|
+
const onChange = (next) => {
|
|
42
|
+
setDraft(next);
|
|
43
|
+
if (instant) finish(true, void 0, next);
|
|
44
|
+
};
|
|
45
|
+
return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
ref,
|
|
47
|
+
className: "w-full min-w-0",
|
|
48
|
+
onKeyDown,
|
|
49
|
+
onBlur,
|
|
50
|
+
children: /* @__PURE__ */ jsx(FieldEditor, {
|
|
51
|
+
field,
|
|
52
|
+
hints,
|
|
53
|
+
value: draft,
|
|
54
|
+
stored,
|
|
55
|
+
resource,
|
|
56
|
+
onChange
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { CellFieldEditor };
|
|
@@ -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
|
@@ -7,12 +7,13 @@ import { recordKey, recordTitleField } from "../live/model-helpers.js";
|
|
|
7
7
|
import { humanize } from "../live/naming.js";
|
|
8
8
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
9
9
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { Sheet } from "../primitives/sheet.js";
|
|
11
|
+
import { ErrorBanner } from "./error-banner.js";
|
|
12
|
+
import { displayRow, loadedFields, shownDisplay } from "./displays/list-displays.js";
|
|
12
13
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
13
14
|
import { listedResource } from "./current-address.js";
|
|
14
15
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
15
|
-
import {
|
|
16
|
+
import { can, rowAllows } from "./permissions.js";
|
|
16
17
|
import { columnsFromModel } from "../live/columns-from-model.js";
|
|
17
18
|
import { pinnedStorageKey } from "../data-grid/pinned-storage.js";
|
|
18
19
|
import { DataGrid } from "../data-grid/data-grid.js";
|
|
@@ -25,8 +26,11 @@ import { FilterPanel } from "../filter-panel/filter-panel.js";
|
|
|
25
26
|
import { stateToText, textToState } from "../live/filter-string.js";
|
|
26
27
|
import { widgetFields } from "../live/view-fields.js";
|
|
27
28
|
import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
28
|
-
import {
|
|
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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ActionInputModel, JsonValue } from "@protobase/schema";
|
|
2
|
+
//#region src/app/pages/action-inputs/action-inputs-dialog.d.ts
|
|
3
|
+
/** What a custom input's component gets, as a field type's editor does: the answer, a way to set it, whether it is refused, and `props`. */
|
|
4
|
+
export type ActionInputProps<V = unknown, P = Record<string, JsonValue>> = {
|
|
5
|
+
/** The input, as the action declares it. */
|
|
6
|
+
input: ActionInputModel;
|
|
7
|
+
value: V | null;
|
|
8
|
+
onChange: (value: V | null) => void;
|
|
9
|
+
invalid: boolean;
|
|
10
|
+
/** What the input gives in `props`; `{}` without. */
|
|
11
|
+
props: P;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Button } from "../../../primitives/button.js";
|
|
2
|
+
import { useProjectUi } from "../project-ui.js";
|
|
3
|
+
import { useAdminMeta } from "../../meta-gate.js";
|
|
4
|
+
import { ResponsiveDialog } from "../../../primitives/responsive-dialog.js";
|
|
5
|
+
import { Field } from "../../../record-view/field.js";
|
|
6
|
+
import { isChosen } from "../../create/create-values.js";
|
|
7
|
+
import { ErrorBanner } from "../../error-banner.js";
|
|
8
|
+
import { FieldEditor } from "../../field-editor.js";
|
|
9
|
+
import { answeredValues, inputField, inputHints, refusedAnswers, startingAnswers } from "./input-answers.js";
|
|
10
|
+
import { inputErrors } from "./input-errors.js";
|
|
11
|
+
import { useId, useState } from "react";
|
|
12
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
//#region src/app/pages/action-inputs/action-inputs-dialog.tsx
|
|
14
|
+
const InputEditor = ({ input, held, invalid, onChange }) => {
|
|
15
|
+
const { resources, views } = useAdminMeta();
|
|
16
|
+
const { components } = useProjectUi();
|
|
17
|
+
if (input.kind === "custom") {
|
|
18
|
+
const Custom = components?.[input.component ?? ""];
|
|
19
|
+
if (!Custom) throw new Error(`Input "${input.name}": the app has no component "${input.component}"`);
|
|
20
|
+
return /* @__PURE__ */ jsx(Custom, {
|
|
21
|
+
input,
|
|
22
|
+
value: held ?? null,
|
|
23
|
+
onChange,
|
|
24
|
+
invalid,
|
|
25
|
+
props: input.props ?? {}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const target = input.resource === void 0 ? void 0 : resources[input.resource];
|
|
29
|
+
return /* @__PURE__ */ jsx(FieldEditor, {
|
|
30
|
+
field: inputField(input),
|
|
31
|
+
hints: inputHints(input),
|
|
32
|
+
value: isChosen(held) ? held.key : held,
|
|
33
|
+
stored: void 0,
|
|
34
|
+
relationLabel: isChosen(held) ? held.label : void 0,
|
|
35
|
+
relationPicker: target ? {
|
|
36
|
+
target,
|
|
37
|
+
view: views[target.name]
|
|
38
|
+
} : void 0,
|
|
39
|
+
invalid,
|
|
40
|
+
onChange
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
/** The dialog an action with inputs opens: the inputs that show, edited as on the create form, and the action's button. */
|
|
44
|
+
const ActionInputsDialog = ({ action, label, danger, onCancel, onSubmit }) => {
|
|
45
|
+
const [draft, setDraft] = useState(() => startingAnswers(action.inputs));
|
|
46
|
+
const [errors, setErrors] = useState({});
|
|
47
|
+
const [failure, setFailure] = useState();
|
|
48
|
+
const [running, setRunning] = useState(false);
|
|
49
|
+
const formId = useId();
|
|
50
|
+
const { shown, values } = answeredValues(action.inputs, draft);
|
|
51
|
+
const submit = async () => {
|
|
52
|
+
const refused = refusedAnswers(action.inputs, values);
|
|
53
|
+
setErrors(refused);
|
|
54
|
+
setFailure(void 0);
|
|
55
|
+
if (Object.keys(refused).length > 0) return;
|
|
56
|
+
setRunning(true);
|
|
57
|
+
await onSubmit(values).catch((error) => {
|
|
58
|
+
setErrors(inputErrors(error, action.inputs));
|
|
59
|
+
setFailure(error);
|
|
60
|
+
});
|
|
61
|
+
setRunning(false);
|
|
62
|
+
};
|
|
63
|
+
const change = (name, value) => {
|
|
64
|
+
setErrors(({ [name]: _, ...rest }) => rest);
|
|
65
|
+
setDraft((current) => ({
|
|
66
|
+
...current,
|
|
67
|
+
[name]: value
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
return /* @__PURE__ */ jsx(ResponsiveDialog, {
|
|
71
|
+
title: label,
|
|
72
|
+
description: action.confirm,
|
|
73
|
+
open: true,
|
|
74
|
+
onClose: onCancel,
|
|
75
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
76
|
+
onClick: onCancel,
|
|
77
|
+
children: "Cancel"
|
|
78
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
79
|
+
variant: danger ? "danger" : "primary",
|
|
80
|
+
type: "submit",
|
|
81
|
+
form: formId,
|
|
82
|
+
loading: running,
|
|
83
|
+
children: label
|
|
84
|
+
})] }),
|
|
85
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
86
|
+
id: formId,
|
|
87
|
+
className: "flex flex-col gap-4",
|
|
88
|
+
onSubmit: (event) => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
submit();
|
|
91
|
+
},
|
|
92
|
+
children: [failure !== void 0 && Object.keys(errors).length === 0 && /* @__PURE__ */ jsx(ErrorBanner, {
|
|
93
|
+
title: `${label} failed`,
|
|
94
|
+
error: failure
|
|
95
|
+
}), action.inputs.filter((input) => shown.has(input.name)).map((input) => /* @__PURE__ */ jsx(Field, {
|
|
96
|
+
label: input.label,
|
|
97
|
+
required: input.required,
|
|
98
|
+
help: input.help,
|
|
99
|
+
error: errors[input.name],
|
|
100
|
+
children: /* @__PURE__ */ jsx(InputEditor, {
|
|
101
|
+
input,
|
|
102
|
+
held: draft[input.name],
|
|
103
|
+
invalid: Boolean(errors[input.name]),
|
|
104
|
+
onChange: (next) => change(input.name, next)
|
|
105
|
+
})
|
|
106
|
+
}, input.name))]
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ActionInputsDialog };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { toDraft, toPatchValue } from "../../../live/field-values.js";
|
|
2
|
+
import { isChosen } from "../../create/create-values.js";
|
|
3
|
+
import { shownInputs } from "@protobase/schema/internal";
|
|
4
|
+
//#region src/app/pages/action-inputs/input-answers.ts
|
|
5
|
+
const fieldTypes = {
|
|
6
|
+
text: "text",
|
|
7
|
+
integer: "integer",
|
|
8
|
+
decimal: "decimal",
|
|
9
|
+
boolean: "boolean",
|
|
10
|
+
date: "date"
|
|
11
|
+
};
|
|
12
|
+
/** The field an input is edited as, so the create form's editors edit it: a choice of options is an enum, of records a relation. */
|
|
13
|
+
const inputField = (input) => {
|
|
14
|
+
const base = {
|
|
15
|
+
name: input.name,
|
|
16
|
+
column: input.name,
|
|
17
|
+
nullable: true,
|
|
18
|
+
readOnly: false,
|
|
19
|
+
filterable: false,
|
|
20
|
+
sortable: false,
|
|
21
|
+
aliases: []
|
|
22
|
+
};
|
|
23
|
+
if (input.kind === "custom") return {
|
|
24
|
+
...base,
|
|
25
|
+
type: "json"
|
|
26
|
+
};
|
|
27
|
+
if (input.kind !== "choice") return {
|
|
28
|
+
...base,
|
|
29
|
+
type: fieldTypes[input.kind]
|
|
30
|
+
};
|
|
31
|
+
if (input.resource !== void 0) return {
|
|
32
|
+
...base,
|
|
33
|
+
type: "relation",
|
|
34
|
+
relation: {
|
|
35
|
+
resource: input.resource,
|
|
36
|
+
columns: [input.name]
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
type: "enum",
|
|
42
|
+
enumValues: (input.options ?? []).map((option) => option.value)
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** The view hints an input's editor reads: its label, a box that grows for a multiline text, a choice's labels. */
|
|
46
|
+
const inputHints = (input) => ({
|
|
47
|
+
label: input.label,
|
|
48
|
+
...input.multiline && { multiline: true },
|
|
49
|
+
...input.options && { valueLabels: Object.fromEntries(input.options.map((option) => [option.value, option.label])) }
|
|
50
|
+
});
|
|
51
|
+
/** What each input holds when the dialog opens: its default, as its editor holds it. */
|
|
52
|
+
const startingAnswers = (inputs) => Object.fromEntries(inputs.map((input) => {
|
|
53
|
+
if (input.kind === "custom") return [input.name, input.default ?? null];
|
|
54
|
+
if (input.kind === "boolean") return [input.name, Boolean(input.default)];
|
|
55
|
+
return [input.name, toDraft(input.default)];
|
|
56
|
+
}));
|
|
57
|
+
const answerOf = (input, held) => {
|
|
58
|
+
if (input.kind === "custom") return held ?? null;
|
|
59
|
+
if (isChosen(held)) return held.key;
|
|
60
|
+
if (input.resource !== void 0) return String(held ?? "") === "" ? null : String(held);
|
|
61
|
+
return toPatchValue(inputField(input), held);
|
|
62
|
+
};
|
|
63
|
+
/** The answers of the inputs that show, as the handler gets them in `values`, and the names of those that show. */
|
|
64
|
+
const answeredValues = (inputs, draft) => {
|
|
65
|
+
const all = Object.fromEntries(inputs.map((input) => [input.name, answerOf(input, draft[input.name])]));
|
|
66
|
+
const shown = shownInputs(inputs, all);
|
|
67
|
+
return {
|
|
68
|
+
shown,
|
|
69
|
+
values: Object.fromEntries(Object.entries(all).filter(([name]) => shown.has(name)))
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const decimal = /^-?\d+(\.\d+)?$/;
|
|
73
|
+
const date = /^\d{4}-\d{2}-\d{2}$/;
|
|
74
|
+
const refusal = (input, value) => {
|
|
75
|
+
if (value === null || value === "") return input.required ? "Required" : void 0;
|
|
76
|
+
if (input.kind === "integer" && !Number.isInteger(value)) return "A whole number";
|
|
77
|
+
if (input.kind === "decimal" && !decimal.test(String(value))) return "A number";
|
|
78
|
+
if (input.kind === "date" && !date.test(String(value))) return "A date as YYYY-MM-DD";
|
|
79
|
+
if (input.max !== void 0 && String(value).length > input.max) return `At most ${input.max} characters`;
|
|
80
|
+
};
|
|
81
|
+
/** What is wrong with the answers that show, by input name: one left empty that is required, a number that is none, a text too long. */
|
|
82
|
+
const refusedAnswers = (inputs, values) => Object.fromEntries(inputs.flatMap((input) => {
|
|
83
|
+
if (!Object.hasOwn(values, input.name)) return [];
|
|
84
|
+
const reason = refusal(input, values[input.name]);
|
|
85
|
+
return reason ? [[input.name, reason]] : [];
|
|
86
|
+
}));
|
|
87
|
+
//#endregion
|
|
88
|
+
export { answeredValues, inputField, inputHints, refusedAnswers, startingAnswers };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ActionInputModel } from "@protobase/schema";
|
|
2
|
+
//#region src/app/pages/action-inputs/input-errors.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a handler throws to refuse answers, each message under the input it is about, such as
|
|
5
|
+
* `throw new ActionInputError({ email: 'No user has this address' })`: the dialog stays open and shows each under its input.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ActionInputError extends Error {
|
|
8
|
+
readonly errors: Readonly<Record<string, string>>;
|
|
9
|
+
constructor(errors: Readonly<Record<string, string>>);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|