@protobase/ui 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/create/create-values.js +2 -1
- package/dist/app/field-editor.js +29 -11
- package/dist/app/history/describe-change.js +44 -0
- package/dist/app/history/history-section.js +92 -0
- package/dist/app/history/use-record-history.js +34 -0
- package/dist/app/record-page.js +43 -7
- package/dist/data/api-provider.d.ts +8 -0
- package/dist/data/query-keys.js +8 -0
- package/dist/format/date.js +15 -2
- package/dist/live/field-values.js +1 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BBx2Bkop.js +69 -0
- package/dist/shell/assets/index-Dc6xgHsz.css +2 -0
- package/dist/shell/assets/{index-B0veahji.js → index-iXIDNHEm.js} +1 -1
- package/dist/shell/assets/{maplibre-CbjT6RKG.js → maplibre-DxWag0Wp.js} +1 -1
- package/dist/shell/assets/{markdown-content-BjRGtSOA.js → markdown-content-Bco0XDqw.js} +1 -1
- package/dist/shell/assets/{record-map-rmkTnAs8.js → record-map-Ca1fz02Q.js} +1 -1
- package/dist/shell/assets/{records-map-D1vLGY06.js → records-map-JooppeKw.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-PnkJqBqB.js → shared-protobase-ui-BZs06oKf.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-B5h_q_JL.js +0 -69
- package/dist/shell/assets/index-BZmb28OT.css +0 -2
|
@@ -29,6 +29,7 @@ const createSections = (model, view) => {
|
|
|
29
29
|
fields: rest
|
|
30
30
|
}];
|
|
31
31
|
};
|
|
32
|
+
/** A chosen record's key as the API takes it: a number when the target's key is an integer. */
|
|
32
33
|
const keyValue = (target, key) => {
|
|
33
34
|
return (target?.primaryKey.length === 1 ? target.fields[target.primaryKey[0]]?.type : void 0) === "integer" ? Number(key) : key;
|
|
34
35
|
};
|
|
@@ -68,4 +69,4 @@ const initialDraft = (model, sections, given = {}) => Object.fromEntries(section
|
|
|
68
69
|
})));
|
|
69
70
|
const hasDatabaseDefault = (field) => Boolean(field.default && "db" in field.default);
|
|
70
71
|
//#endregion
|
|
71
|
-
export { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isCreatable, isRequired };
|
|
72
|
+
export { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isCreatable, isRequired, keyValue };
|
package/dist/app/field-editor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Input } from "../primitives/input.js";
|
|
3
|
+
import { buttonClass } from "../primitives/button.js";
|
|
3
4
|
import { humanize } from "../live/naming.js";
|
|
4
5
|
import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
|
|
5
6
|
import { Switch } from "../primitives/switch.js";
|
|
@@ -14,6 +15,7 @@ import { WidgetPart } from "./field-types/widget-part.js";
|
|
|
14
15
|
import { Markdown } from "../markdown/markdown.js";
|
|
15
16
|
import { MarkdownEditor } from "../markdown/markdown-editor.js";
|
|
16
17
|
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
18
|
+
import { ArrowUpRight } from "lucide-react";
|
|
17
19
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
20
|
import { enumLabel } from "@protobase/schema";
|
|
19
21
|
//#region src/app/field-editor.tsx
|
|
@@ -42,17 +44,33 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
42
44
|
placeholder,
|
|
43
45
|
onChange
|
|
44
46
|
});
|
|
45
|
-
if (field.type === "relation" && relationPicker && !field.readOnly
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
if (field.type === "relation" && relationPicker && !field.readOnly && !locked) {
|
|
48
|
+
const picker = /* @__PURE__ */ jsx(RelationPicker, {
|
|
49
|
+
target: relationPicker.target,
|
|
50
|
+
view: relationPicker.view,
|
|
51
|
+
value: String(value),
|
|
52
|
+
label: relationLabel,
|
|
53
|
+
invalid,
|
|
54
|
+
onChange: (next, label) => onChange({
|
|
55
|
+
key: next,
|
|
56
|
+
label
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
if (!relationTo) return picker;
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
61
|
+
className: "flex items-center gap-1",
|
|
62
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
63
|
+
className: "min-w-0 flex-1",
|
|
64
|
+
children: picker
|
|
65
|
+
}), /* @__PURE__ */ jsx(AppLink, {
|
|
66
|
+
to: relationTo,
|
|
67
|
+
"aria-label": `Open ${relationLabel ?? String(value)}`,
|
|
68
|
+
title: "Open",
|
|
69
|
+
className: cn(buttonClass({ variant: "ghost" }), "w-8 px-0"),
|
|
70
|
+
children: /* @__PURE__ */ jsx(ArrowUpRight, { className: "size-4" })
|
|
71
|
+
})]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
56
74
|
if (field.type === "boolean" && locked) return /* @__PURE__ */ jsx("div", {
|
|
57
75
|
className: "py-1.5 text-muted-foreground",
|
|
58
76
|
children: displayValue(field, stored, hints)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { humanize } from "../../live/naming.js";
|
|
2
|
+
import { displayValue } from "../../live/display-value.js";
|
|
3
|
+
//#region src/app/history/describe-change.ts
|
|
4
|
+
const verbs = {
|
|
5
|
+
create: "created this",
|
|
6
|
+
update: "changed",
|
|
7
|
+
delete: "deleted this",
|
|
8
|
+
undelete: "restored this"
|
|
9
|
+
};
|
|
10
|
+
/** Who made a change: their name, else their id; a write from outside the app names the database role. */
|
|
11
|
+
const changedBy = (change) => {
|
|
12
|
+
if (change.actor) return change.actor.name ?? change.actor.id;
|
|
13
|
+
return change.databaseRole ? `Database (${change.databaseRole})` : "Database";
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A change as the record page tells it: who, what they did, and for an update each changed field the model still has,
|
|
17
|
+
* before and after, as the view labels and formats it.
|
|
18
|
+
*/
|
|
19
|
+
const describeChange = (change, model, view) => {
|
|
20
|
+
const fields = (change.operation === "create" ? [] : change.changed).flatMap((name) => {
|
|
21
|
+
const field = model.fields[name];
|
|
22
|
+
if (!field || name === model.softDelete) return [];
|
|
23
|
+
const hints = view?.fields[name];
|
|
24
|
+
const label = hints?.label ?? humanize(name);
|
|
25
|
+
if (field.sensitive || !change.before || !change.after) return [{
|
|
26
|
+
name,
|
|
27
|
+
label
|
|
28
|
+
}];
|
|
29
|
+
return [{
|
|
30
|
+
name,
|
|
31
|
+
label,
|
|
32
|
+
before: displayValue(field, change.before[name], hints),
|
|
33
|
+
after: displayValue(field, change.after[name], hints)
|
|
34
|
+
}];
|
|
35
|
+
});
|
|
36
|
+
const verb = change.operation === "update" && fields.length === 0 ? "changed this" : verbs[change.operation];
|
|
37
|
+
return {
|
|
38
|
+
who: changedBy(change),
|
|
39
|
+
verb,
|
|
40
|
+
fields
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { changedBy, describeChange };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
|
+
import { formatDateTime, formatRelativeTime } from "../../format/date.js";
|
|
3
|
+
import { Button } from "../../primitives/button.js";
|
|
4
|
+
import { Section } from "../../record-view/section.js";
|
|
5
|
+
import { ErrorBanner } from "../error-banner.js";
|
|
6
|
+
import { describeChange } from "./describe-change.js";
|
|
7
|
+
import { useRecordHistory } from "./use-record-history.js";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/app/history/history-section.tsx
|
|
10
|
+
const Loading = () => /* @__PURE__ */ jsx("div", {
|
|
11
|
+
className: "flex flex-col gap-2",
|
|
12
|
+
children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
|
|
13
|
+
});
|
|
14
|
+
const Change = ({ change, model, view }) => {
|
|
15
|
+
const { who, verb, fields } = describeChange(change, model, view);
|
|
16
|
+
const at = Date.parse(change.at);
|
|
17
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
18
|
+
className: "flex flex-col gap-1.5 py-3 first:pt-0 last:pb-0",
|
|
19
|
+
children: [/* @__PURE__ */ jsxs("p", { children: [
|
|
20
|
+
/* @__PURE__ */ jsx("span", {
|
|
21
|
+
className: "font-medium text-foreground",
|
|
22
|
+
children: who
|
|
23
|
+
}),
|
|
24
|
+
" ",
|
|
25
|
+
/* @__PURE__ */ jsx("span", {
|
|
26
|
+
className: "text-muted-foreground",
|
|
27
|
+
children: verb
|
|
28
|
+
}),
|
|
29
|
+
" ",
|
|
30
|
+
/* @__PURE__ */ jsxs("time", {
|
|
31
|
+
dateTime: change.at,
|
|
32
|
+
title: formatDateTime(at),
|
|
33
|
+
className: "text-muted-foreground",
|
|
34
|
+
children: ["· ", formatRelativeTime(Math.max(0, Math.round((Date.now() - at) / 1e3)))]
|
|
35
|
+
})
|
|
36
|
+
] }), fields.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
37
|
+
className: "grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-4 gap-y-1",
|
|
38
|
+
children: fields.map((field) => /* @__PURE__ */ jsxs("div", {
|
|
39
|
+
className: "contents",
|
|
40
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
41
|
+
className: "text-muted-foreground",
|
|
42
|
+
children: field.label
|
|
43
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
44
|
+
className: "min-w-0 break-words",
|
|
45
|
+
children: field.before === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
46
|
+
className: "text-muted-foreground",
|
|
47
|
+
children: "changed"
|
|
48
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
49
|
+
/* @__PURE__ */ jsx("del", {
|
|
50
|
+
className: "text-muted-foreground",
|
|
51
|
+
children: field.before
|
|
52
|
+
}),
|
|
53
|
+
" → ",
|
|
54
|
+
/* @__PURE__ */ jsx("ins", {
|
|
55
|
+
className: "no-underline",
|
|
56
|
+
children: field.after
|
|
57
|
+
})
|
|
58
|
+
] })
|
|
59
|
+
})]
|
|
60
|
+
}, field.name))
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
/** The record's changes, newest first, with who made each and the fields before and after; Show more pages further back. */
|
|
65
|
+
const HistorySection = ({ item, model, view, recordKey, etag }) => {
|
|
66
|
+
const history = useRecordHistory(model.name, recordKey, item.limit ?? 10, etag);
|
|
67
|
+
return /* @__PURE__ */ jsx(Section, {
|
|
68
|
+
title: item.title,
|
|
69
|
+
children: history.isPending ? /* @__PURE__ */ jsx(Loading, {}) : history.error ? /* @__PURE__ */ jsx(ErrorBanner, {
|
|
70
|
+
title: "Could not load the history",
|
|
71
|
+
error: history.error
|
|
72
|
+
}) : history.items.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
73
|
+
className: "text-muted-foreground",
|
|
74
|
+
children: "No changes kept yet."
|
|
75
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ol", {
|
|
76
|
+
className: "divide-y divide-border",
|
|
77
|
+
children: history.items.map((change) => /* @__PURE__ */ jsx(Change, {
|
|
78
|
+
change,
|
|
79
|
+
model,
|
|
80
|
+
view
|
|
81
|
+
}, change.id))
|
|
82
|
+
}), history.hasNextPage && /* @__PURE__ */ jsx(Button, {
|
|
83
|
+
size: "sm",
|
|
84
|
+
className: "mt-4",
|
|
85
|
+
disabled: history.isFetchingNextPage,
|
|
86
|
+
onClick: () => void history.fetchNextPage(),
|
|
87
|
+
children: "Show more"
|
|
88
|
+
})] })
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { HistorySection };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { keys } from "../../data/query-keys.js";
|
|
2
|
+
import { useClient } from "../../data/api-provider.js";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
5
|
+
//#region src/app/history/use-record-history.ts
|
|
6
|
+
/** How long after a save the history looks again: the server keeps a change just after its write commits. */
|
|
7
|
+
const afterSave = 1500;
|
|
8
|
+
/**
|
|
9
|
+
* A record's changes, newest first, `pageSize` at a time. Its key starts with the record's, so refreshing the record
|
|
10
|
+
* refreshes it too; it looks again shortly after `etag` changes, once the server has kept the save.
|
|
11
|
+
*/
|
|
12
|
+
const useRecordHistory = (resource, recordKey, pageSize, etag) => {
|
|
13
|
+
const client = useClient();
|
|
14
|
+
const query = useInfiniteQuery({
|
|
15
|
+
queryKey: keys.history(resource, recordKey, pageSize),
|
|
16
|
+
initialPageParam: "",
|
|
17
|
+
queryFn: ({ pageParam }) => client.history(resource, recordKey, {
|
|
18
|
+
pageSize,
|
|
19
|
+
pageToken: pageParam
|
|
20
|
+
}),
|
|
21
|
+
getNextPageParam: (last) => last.nextPageToken || void 0
|
|
22
|
+
});
|
|
23
|
+
const { refetch } = query;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const timer = setTimeout(() => void refetch(), afterSave);
|
|
26
|
+
return () => clearTimeout(timer);
|
|
27
|
+
}, [etag, refetch]);
|
|
28
|
+
return {
|
|
29
|
+
...query,
|
|
30
|
+
items: query.data?.pages.flatMap((page) => page.items) ?? []
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { useRecordHistory };
|
package/dist/app/record-page.js
CHANGED
|
@@ -18,8 +18,9 @@ import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record
|
|
|
18
18
|
import { RecordHeader } from "../record-view/record-header.js";
|
|
19
19
|
import { SpecTable } from "../record-view/spec-table.js";
|
|
20
20
|
import { KeyFacts } from "../record-view/key-facts.js";
|
|
21
|
-
import { isRequiredOnEdit } from "../live/field-values.js";
|
|
21
|
+
import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
|
|
22
22
|
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
23
|
+
import { isChosen, keyValue } from "./create/create-values.js";
|
|
23
24
|
import { ErrorBanner } from "./error-banner.js";
|
|
24
25
|
import { displayValue } from "../live/display-value.js";
|
|
25
26
|
import { filledFields } from "./filled-fields.js";
|
|
@@ -45,6 +46,7 @@ import { draftStorageKey } from "./draft-storage.js";
|
|
|
45
46
|
import { useLines } from "./use-lines.js";
|
|
46
47
|
import { useLinesDraft } from "./use-lines-draft.js";
|
|
47
48
|
import { GallerySection } from "./gallery/gallery-section.js";
|
|
49
|
+
import { HistorySection } from "./history/history-section.js";
|
|
48
50
|
import { useRecordEditor } from "./use-record-editor.js";
|
|
49
51
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
50
52
|
import { RelatedSection } from "./related-section.js";
|
|
@@ -57,8 +59,9 @@ import { enumLabel } from "@protobase/schema";
|
|
|
57
59
|
//#region src/app/record-page.tsx
|
|
58
60
|
const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
|
|
59
61
|
/**
|
|
60
|
-
* The view's layout, or one section with the editable fields when the view has none
|
|
61
|
-
* user who may only read the resource gets, shows every field: the record rather
|
|
62
|
+
* The view's layout, or one section with the editable fields when the view has none, then History when the resource
|
|
63
|
+
* keeps it. The read view, which is all a user who may only read the resource gets, shows every field: the record rather
|
|
64
|
+
* than what can be changed.
|
|
62
65
|
*/
|
|
63
66
|
const layoutFor = (model, view, read) => {
|
|
64
67
|
if (view && view.layout.length > 0) return view.layout;
|
|
@@ -68,7 +71,10 @@ const layoutFor = (model, view, read) => {
|
|
|
68
71
|
kind: "section",
|
|
69
72
|
title: "Details",
|
|
70
73
|
fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
|
|
71
|
-
}
|
|
74
|
+
}, ...model.history ? [{
|
|
75
|
+
kind: "history",
|
|
76
|
+
title: "History"
|
|
77
|
+
}] : []];
|
|
72
78
|
};
|
|
73
79
|
const RecordPage = ({ resource, recordKey }) => {
|
|
74
80
|
const { resources } = useAdminMeta();
|
|
@@ -152,6 +158,24 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
152
158
|
onReview: () => void queryClient.invalidateQueries({ queryKey: keys.record(model.name, recordKey) })
|
|
153
159
|
});
|
|
154
160
|
const related = useRelatedRecords(model, view, meta.resources, meta.views, stored.record, basePath);
|
|
161
|
+
const pickTarget = (name) => {
|
|
162
|
+
const field = model.fields[name];
|
|
163
|
+
const target = field?.relation ? meta.resources[field.relation.resource] : void 0;
|
|
164
|
+
if (!target || target.primaryKey.length !== 1 || name === model.tenant || model.primaryKey.includes(name)) return void 0;
|
|
165
|
+
return target;
|
|
166
|
+
};
|
|
167
|
+
const picked = Object.values(model.fields).filter((field) => pickTarget(field.name));
|
|
168
|
+
const drafted = {
|
|
169
|
+
...stored.record,
|
|
170
|
+
...Object.fromEntries(picked.map((field) => [field.name, fromDraft(field, editor.value(field.name))]))
|
|
171
|
+
};
|
|
172
|
+
const chosen = useRelatedRecords(model, view, meta.resources, meta.views, drafted, basePath);
|
|
173
|
+
const pick = (name, next) => {
|
|
174
|
+
const target = pickTarget(name);
|
|
175
|
+
if (!target || !isChosen(next)) return editor.change(name, next);
|
|
176
|
+
queryClient.setQueryData(keys.relatedLabel(target.name, next.key), next.label);
|
|
177
|
+
editor.change(name, keyValue(target, next.key));
|
|
178
|
+
};
|
|
155
179
|
const { fields: fieldTypes } = useProjectUi();
|
|
156
180
|
const layout = layoutFor(model, view, shown === "read").filter((item) => whenShown(item.when, editing));
|
|
157
181
|
const sidebarFields = layout.flatMap((item) => item.kind === "sidebar" ? item.fields : []);
|
|
@@ -167,6 +191,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
167
191
|
const access = fieldAccess(stored.permissions, name);
|
|
168
192
|
if (!field || access === "hidden") return null;
|
|
169
193
|
const conflict = editor.conflicts[name];
|
|
194
|
+
const target = pickTarget(name);
|
|
170
195
|
return /* @__PURE__ */ jsx(Field, {
|
|
171
196
|
label: hint(name)?.label ?? humanize(name),
|
|
172
197
|
required: shown === "form" && access !== "read" && isRequiredOnEdit(field),
|
|
@@ -189,13 +214,17 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
189
214
|
hints: hint(name),
|
|
190
215
|
value: editor.value(name),
|
|
191
216
|
stored: stored.record[name],
|
|
192
|
-
relationLabel:
|
|
193
|
-
relationTo:
|
|
217
|
+
relationLabel: chosen.labels[name],
|
|
218
|
+
relationTo: chosen.addresses[name],
|
|
219
|
+
relationPicker: target && {
|
|
220
|
+
target,
|
|
221
|
+
view: meta.views[target.name]
|
|
222
|
+
},
|
|
194
223
|
locked: shown !== "form" || access === "read",
|
|
195
224
|
invalid: Boolean(editor.errors[name]),
|
|
196
225
|
reveal: field.sensitive ? () => client.reveal(model.name, recordKey, name) : void 0,
|
|
197
226
|
resource: model.name,
|
|
198
|
-
onChange: (next) =>
|
|
227
|
+
onChange: (next) => pick(name, next)
|
|
199
228
|
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-9" })
|
|
200
229
|
}, field.sensitive ? `${name}:${stored.etag}` : name);
|
|
201
230
|
};
|
|
@@ -381,6 +410,13 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
381
410
|
record: stored.record,
|
|
382
411
|
recordKey
|
|
383
412
|
}, `gallery:${index}`)];
|
|
413
|
+
if (item.kind === "history") return [/* @__PURE__ */ jsx(HistorySection, {
|
|
414
|
+
item,
|
|
415
|
+
model,
|
|
416
|
+
view,
|
|
417
|
+
recordKey,
|
|
418
|
+
etag: stored.etag
|
|
419
|
+
}, `history:${index}`)];
|
|
384
420
|
if (item.kind !== "section") return [];
|
|
385
421
|
const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
|
|
386
422
|
const facts = shown === "read" && item.presentation === "facts";
|
|
@@ -16,6 +16,10 @@ export declare const useClient: () => {
|
|
|
16
16
|
}) => Promise<void>;
|
|
17
17
|
undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
18
18
|
reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
|
|
19
|
+
history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
20
|
+
pageSize?: number;
|
|
21
|
+
pageToken?: string;
|
|
22
|
+
}) => Promise<import("@protobase/client").HistoryPage>;
|
|
19
23
|
batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
|
|
20
24
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
21
25
|
filter?: import("@protobase/client").FilterInput;
|
|
@@ -57,6 +61,10 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
57
61
|
}) => Promise<void>;
|
|
58
62
|
undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
59
63
|
reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
|
|
64
|
+
history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
65
|
+
pageSize?: number;
|
|
66
|
+
pageToken?: string;
|
|
67
|
+
}) => Promise<import("@protobase/client").HistoryPage>;
|
|
60
68
|
batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
|
|
61
69
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
62
70
|
filter?: import("@protobase/client").FilterInput;
|
package/dist/data/query-keys.js
CHANGED
|
@@ -58,6 +58,14 @@ const keys = {
|
|
|
58
58
|
resource,
|
|
59
59
|
key
|
|
60
60
|
],
|
|
61
|
+
/** A record's changes; starts with `record`, so refreshing the record refreshes them too. */
|
|
62
|
+
history: (resource, key, pageSize) => [
|
|
63
|
+
"record",
|
|
64
|
+
resource,
|
|
65
|
+
key,
|
|
66
|
+
"history",
|
|
67
|
+
pageSize
|
|
68
|
+
],
|
|
61
69
|
facets: (resource, field, filter) => [
|
|
62
70
|
"facets",
|
|
63
71
|
resource,
|
package/dist/format/date.js
CHANGED
|
@@ -25,6 +25,19 @@ const formatAgo = (seconds) => {
|
|
|
25
25
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)} h ago`;
|
|
26
26
|
return `${Math.floor(seconds / 86400)} d ago`;
|
|
27
27
|
};
|
|
28
|
-
new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
28
|
+
const relative = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
29
|
+
const units = [
|
|
30
|
+
["year", 31536e3],
|
|
31
|
+
["month", 2592e3],
|
|
32
|
+
["day", 86400],
|
|
33
|
+
["hour", 3600],
|
|
34
|
+
["minute", 60]
|
|
35
|
+
];
|
|
36
|
+
/** "5 minutes ago", "yesterday", "3 years ago"; `seconds` is how long ago it was. */
|
|
37
|
+
const formatRelativeTime = (seconds) => {
|
|
38
|
+
const [unit, size] = units.find(([, size]) => seconds >= size) ?? ["second", 1];
|
|
39
|
+
if (unit === "second" && seconds < 45) return "just now";
|
|
40
|
+
return relative.format(-Math.floor(seconds / size), unit);
|
|
41
|
+
};
|
|
29
42
|
//#endregion
|
|
30
|
-
export { formatAgo, formatDate, formatDateTime, formatDayShort };
|
|
43
|
+
export { formatAgo, formatDate, formatDateTime, formatDayShort, formatRelativeTime };
|
|
@@ -21,6 +21,7 @@ const toPatchValue = (field, draft) => {
|
|
|
21
21
|
const text = String(draft).trim();
|
|
22
22
|
if (text === "") return field.nullable ? null : "";
|
|
23
23
|
if (field.type === "integer") return Number(text);
|
|
24
|
+
if (field.type === "relation") return typeof draft === "number" ? draft : text;
|
|
24
25
|
if (textEdited.has(field.type) || field.type === "enum") return text;
|
|
25
26
|
throw new Error(`Not implemented: editing ${field.type} fields`);
|
|
26
27
|
};
|
package/dist/package.js
CHANGED