@protobase/ui 0.63.2 → 0.63.4
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/activity-chart/activity-chart.js +1 -1
- package/dist/app/lines-plan.js +116 -0
- package/dist/app/list-order.js +7 -1
- package/dist/app/record-lines-cell.js +47 -0
- package/dist/app/record-lines.js +102 -25
- package/dist/app/record-messages.js +8 -2
- package/dist/app/record-page.js +13 -15
- package/dist/app/use-lines-draft.js +94 -35
- package/dist/app/use-lines.js +21 -7
- package/dist/app/use-record-editor.js +15 -10
- package/dist/app/use-record-lines.js +47 -0
- package/dist/auth/auth-gate.js +4 -1
- package/dist/auth/auth-provider.js +4 -1
- package/dist/auth/first-run-page.js +49 -40
- package/dist/auth/messages.js +4 -0
- package/dist/data/use-list.d.ts +27 -1
- package/dist/data/use-list.js +10 -3
- package/dist/data-grid/cell-text.js +1 -1
- package/dist/data-grid/cells.js +1 -1
- package/dist/inline-child-table/columns.js +7 -2
- package/dist/inline-child-table/inline-child-table.js +29 -48
- package/dist/inline-child-table/line-row.js +16 -44
- package/dist/inline-child-table/messages.js +2 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-CBYlZ19-.js +9 -0
- package/dist/shell/assets/{calls-BjMBacTA.js → calls-BIXaKTs0.js} +2 -2
- package/dist/shell/assets/{index-DYpysp6_.js → index-BshKxX1Z.js} +2 -2
- package/dist/shell/assets/index-BvT0X9o1.css +2 -0
- package/dist/shell/assets/{records-map-BZPeBvM4.js → records-map-D1SJKyvj.js} +2 -2
- package/dist/shell/assets/{shared-protobase-ui-ByA8um6R.js → shared-protobase-ui-DibYt-TM.js} +2 -2
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +2 -2
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-CBYlZ19-.js.map +1 -0
- package/dist/shell-source-maps/assets/{calls-BjMBacTA.js.map → calls-BIXaKTs0.js.map} +1 -1
- package/dist/shell-source-maps/assets/{index-DYpysp6_.js.map → index-BshKxX1Z.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-BZPeBvM4.js.map → records-map-D1SJKyvj.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-ByA8um6R.js.map → shared-protobase-ui-DibYt-TM.js.map} +1 -1
- package/package.json +4 -4
- package/dist/inline-child-table/number-cell.js +0 -25
- package/dist/inline-child-table/totals-footer.js +0 -39
- package/dist/mocks/invoices.js +0 -13
- package/dist/mocks/products.js +0 -167
- package/dist/shell/assets/app-DqOuQJpX.js +0 -9
- package/dist/shell/assets/index-Be8nCyRW.css +0 -2
- package/dist/shell-source-maps/assets/app-DqOuQJpX.js.map +0 -1
|
@@ -15,7 +15,7 @@ const without = (source, name) => Object.fromEntries(Object.entries(source).filt
|
|
|
15
15
|
/**
|
|
16
16
|
* Draft state for one record. Saving sends only the changed fields with the ETag that was read;
|
|
17
17
|
* a 412 refetches the record and marks the fields both sides changed so the user can pick a side.
|
|
18
|
-
* When the lines
|
|
18
|
+
* When the lines changed too, Save sends the record and the lines' writes as one batch: all or nothing.
|
|
19
19
|
*/
|
|
20
20
|
const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, lines }) => {
|
|
21
21
|
const client = useClient();
|
|
@@ -38,7 +38,7 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
if (!draftKey) return;
|
|
40
40
|
const kept = Object.fromEntries(Object.entries(draft).filter(([name]) => !model.fields[name]?.sensitive));
|
|
41
|
-
const keep = Object.keys(kept).length > 0 || Boolean(lines?.
|
|
41
|
+
const keep = Object.keys(kept).length > 0 || Boolean(lines?.order);
|
|
42
42
|
writeDraft(localStorage, draftKey, keep ? {
|
|
43
43
|
etag: stored.etag,
|
|
44
44
|
fields: kept,
|
|
@@ -48,7 +48,6 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
48
48
|
draftKey,
|
|
49
49
|
model,
|
|
50
50
|
draft,
|
|
51
|
-
lines?.dirty,
|
|
52
51
|
lines?.order,
|
|
53
52
|
stored.etag
|
|
54
53
|
]);
|
|
@@ -93,14 +92,15 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
93
92
|
throw error;
|
|
94
93
|
}
|
|
95
94
|
};
|
|
96
|
-
const saveBatch = async (
|
|
97
|
-
const
|
|
95
|
+
const saveBatch = async (linesPlan) => {
|
|
96
|
+
const recordOps = fieldsDirty ? [{
|
|
98
97
|
op: "update",
|
|
99
98
|
resource: model.name,
|
|
100
99
|
key: recordKey,
|
|
101
100
|
data: patchOf(),
|
|
102
101
|
etag: stored.etag
|
|
103
|
-
}] : []
|
|
102
|
+
}] : [];
|
|
103
|
+
const ops = [...recordOps, ...linesPlan.ops];
|
|
104
104
|
try {
|
|
105
105
|
await client.batchWrite(ops);
|
|
106
106
|
} catch (error) {
|
|
@@ -108,7 +108,12 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
108
108
|
await adoptLatest();
|
|
109
109
|
await queryClient.invalidateQueries({ queryKey: ["list"] });
|
|
110
110
|
setLinesConflict(true);
|
|
111
|
-
} else if (isApiError(error))
|
|
111
|
+
} else if (isApiError(error)) {
|
|
112
|
+
const lineOp = error.operation === void 0 ? -1 : error.operation - recordOps.length;
|
|
113
|
+
const row = linesPlan.rows[lineOp];
|
|
114
|
+
if (lineOp < 0) reject(error);
|
|
115
|
+
else if (row) lines?.reject(row, error);
|
|
116
|
+
}
|
|
112
117
|
throw isApiError(error) ? nameFailingOperation(error, ops) : error;
|
|
113
118
|
}
|
|
114
119
|
await Promise.all([
|
|
@@ -120,8 +125,8 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
120
125
|
lines?.reset();
|
|
121
126
|
};
|
|
122
127
|
const save = () => feedback.run(async () => {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
128
|
+
const linesPlan = lines?.plan();
|
|
129
|
+
if (linesPlan && linesPlan.ops.length > 0) await saveBatch(linesPlan);
|
|
125
130
|
else await saveRecordOnly();
|
|
126
131
|
setDraft({});
|
|
127
132
|
setConflicts({});
|
|
@@ -146,7 +151,7 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
146
151
|
/** The changes on screen were brought back from this browser. */
|
|
147
152
|
restored: showRestored && dirty,
|
|
148
153
|
dismissRestored: () => setShowRestored(false),
|
|
149
|
-
/** Someone changed the lines while
|
|
154
|
+
/** Someone changed the lines while they were being edited here. */
|
|
150
155
|
linesConflict,
|
|
151
156
|
saving: feedback.phase === "saving",
|
|
152
157
|
phase: feedback.phase,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useAdminMeta } from "./meta-gate.js";
|
|
2
|
+
import { listColumnNames } from "../live/columns-from-model.js";
|
|
3
|
+
import { keyValue } from "./create/create-values.js";
|
|
4
|
+
import { listLabels } from "../live/relation-labels.js";
|
|
5
|
+
import { useLines } from "./use-lines.js";
|
|
6
|
+
import { useLinesDraft } from "./use-lines-draft.js";
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
//#region src/app/use-record-lines.ts
|
|
9
|
+
/** A resource that holds numbered lines of this one: it points at the parent and has `position` and `quantity`. */
|
|
10
|
+
const findLinesResource = (resources, parent) => Object.values(resources).find((candidate) => candidate.name !== parent.name && candidate.fields.position && candidate.fields.quantity && Object.values(candidate.fields).some((field) => field.type === "relation" && field.relation?.resource === parent.name));
|
|
11
|
+
/**
|
|
12
|
+
* The lines' columns: the list columns of their view, or its first fields, without what the editor fills in itself:
|
|
13
|
+
* the key, the tenant, the relation to the record and the position, which is the order of the rows.
|
|
14
|
+
*/
|
|
15
|
+
const lineColumnNames = (lines, view, link) => listColumnNames(lines, view).filter((name) => {
|
|
16
|
+
const field = lines.fields[name];
|
|
17
|
+
return field && !field.writeOnly && name !== link && name !== "position" && name !== lines.tenant && !lines.primaryKey.includes(name);
|
|
18
|
+
});
|
|
19
|
+
/** The record's lines when it has a lines resource: the resource, its columns, the lines as stored and the changes made to them. */
|
|
20
|
+
const useRecordLines = (parent, parentKey) => {
|
|
21
|
+
const { resources, views } = useAdminMeta();
|
|
22
|
+
const model = useMemo(() => findLinesResource(resources, parent), [resources, parent]);
|
|
23
|
+
const link = model && Object.values(model.fields).find((field) => field.type === "relation" && field.relation?.resource === parent.name);
|
|
24
|
+
const columns = model && link ? lineColumnNames(model, views[model.name], link.name) : [];
|
|
25
|
+
const data = useLines(model, link, parentKey, model ? listLabels(resources, views, model, columns) : void 0);
|
|
26
|
+
const target = useMemo(() => model && link ? {
|
|
27
|
+
model,
|
|
28
|
+
position: "position",
|
|
29
|
+
link: {
|
|
30
|
+
field: link.name,
|
|
31
|
+
value: keyValue(parent, parentKey)
|
|
32
|
+
}
|
|
33
|
+
} : void 0, [
|
|
34
|
+
model,
|
|
35
|
+
link,
|
|
36
|
+
parent,
|
|
37
|
+
parentKey
|
|
38
|
+
]);
|
|
39
|
+
return {
|
|
40
|
+
model,
|
|
41
|
+
columns,
|
|
42
|
+
data,
|
|
43
|
+
draft: useLinesDraft(target, data.items)
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { findLinesResource, lineColumnNames, useRecordLines };
|
package/dist/auth/auth-gate.js
CHANGED
|
@@ -148,7 +148,10 @@ const useAuthPage = (workspace, children) => {
|
|
|
148
148
|
children: text("tryAgain")
|
|
149
149
|
})]
|
|
150
150
|
});
|
|
151
|
-
case "needs-admin": return /* @__PURE__ */ jsx(FirstRunPage, {
|
|
151
|
+
case "needs-admin": return /* @__PURE__ */ jsx(FirstRunPage, {
|
|
152
|
+
firstAdminUrl: state.firstAdminUrl,
|
|
153
|
+
onCheckAgain: recheck
|
|
154
|
+
});
|
|
152
155
|
}
|
|
153
156
|
if (state.kind === "signed-in" && address) return /* @__PURE__ */ jsx(Redirect, { to: next ?? "/" });
|
|
154
157
|
if (page.kind === "reset-password") return /* @__PURE__ */ jsx(ResetPasswordPage, {
|
|
@@ -125,7 +125,10 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
|
|
|
125
125
|
setAnonymous(Boolean(status.anonymous));
|
|
126
126
|
setBeforeSignIn(status.plugins ?? []);
|
|
127
127
|
setLocale(status.locale);
|
|
128
|
-
if (status.needsAdmin) return setState({
|
|
128
|
+
if (status.needsAdmin) return setState({
|
|
129
|
+
kind: "needs-admin",
|
|
130
|
+
...status.firstAdminUrl && { firstAdminUrl: status.firstAdminUrl }
|
|
131
|
+
});
|
|
129
132
|
const user = await session.session();
|
|
130
133
|
return user ? enter(user) : setState({ kind: status.anonymous ? "visitor" : "signed-out" });
|
|
131
134
|
}).catch((error) => setState({
|
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
import { text } from "./messages.js";
|
|
2
|
-
import {
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { Button, buttonClass } from "../primitives/button.js";
|
|
3
4
|
import { AuthLayout } from "./auth-layout.js";
|
|
4
5
|
import { useState } from "react";
|
|
5
|
-
import { Check, Copy } from "lucide-react";
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Check, Copy, ExternalLink } from "lucide-react";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/auth/first-run-page.tsx
|
|
8
9
|
const createAdminCommand = "protobase users create you@example.com";
|
|
9
10
|
/**
|
|
10
11
|
* Shown while the admin store has no user. Accounts are not created over the web (a fresh URL must not be claimable
|
|
11
|
-
* by whoever reaches it first), so this says how to create the first admin on the host
|
|
12
|
+
* by whoever reaches it first), so this says how to create the first admin on the host, or, with `firstAdminUrl`,
|
|
13
|
+
* links to the platform's page where it is added, since nobody hosted there can run the command.
|
|
12
14
|
*/
|
|
13
|
-
const FirstRunPage = ({ command = createAdminCommand, onCheckAgain, busy }) => {
|
|
15
|
+
const FirstRunPage = ({ command = createAdminCommand, firstAdminUrl, onCheckAgain, busy }) => /* @__PURE__ */ jsxs(AuthLayout, {
|
|
16
|
+
title: text("noAdminTitle"),
|
|
17
|
+
description: text("noAdminDescription"),
|
|
18
|
+
children: [firstAdminUrl ? /* @__PURE__ */ jsx(ConsoleLink, { url: firstAdminUrl }) : /* @__PURE__ */ jsx(CommandBox, { command }), /* @__PURE__ */ jsx(Button, {
|
|
19
|
+
variant: firstAdminUrl ? "secondary" : "primary",
|
|
20
|
+
className: "mt-4 min-h-10 w-full",
|
|
21
|
+
loading: busy,
|
|
22
|
+
onClick: onCheckAgain,
|
|
23
|
+
children: text("reload")
|
|
24
|
+
})]
|
|
25
|
+
});
|
|
26
|
+
const ConsoleLink = ({ url }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
|
|
27
|
+
className: "text-muted-foreground",
|
|
28
|
+
children: text("firstAdminExplanation")
|
|
29
|
+
}), /* @__PURE__ */ jsxs("a", {
|
|
30
|
+
href: url,
|
|
31
|
+
target: "_blank",
|
|
32
|
+
rel: "noopener noreferrer",
|
|
33
|
+
className: cn(buttonClass({ variant: "primary" }), "mt-4 min-h-10 w-full"),
|
|
34
|
+
children: [text("firstAdminLink"), /* @__PURE__ */ jsx(ExternalLink, { className: "size-4" })]
|
|
35
|
+
})] });
|
|
36
|
+
const CommandBox = ({ command }) => {
|
|
14
37
|
const [copied, setCopied] = useState(false);
|
|
15
|
-
return /* @__PURE__ */ jsxs(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
className: "inline-flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted pointer-coarse:size-11",
|
|
38
|
-
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
|
|
39
|
-
})]
|
|
40
|
-
}),
|
|
41
|
-
/* @__PURE__ */ jsx(Button, {
|
|
42
|
-
variant: "primary",
|
|
43
|
-
className: "mt-4 min-h-10 w-full",
|
|
44
|
-
loading: busy,
|
|
45
|
-
onClick: onCheckAgain,
|
|
46
|
-
children: text("reload")
|
|
47
|
-
})
|
|
48
|
-
]
|
|
49
|
-
});
|
|
38
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
|
|
39
|
+
className: "text-muted-foreground",
|
|
40
|
+
children: text("noAdminExplanation")
|
|
41
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: "mt-4 flex items-center gap-2 rounded-md border bg-surface py-1.5 pl-3 pr-1.5",
|
|
43
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
44
|
+
className: "min-w-0 flex-1 break-all font-mono text-xs",
|
|
45
|
+
children: command
|
|
46
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
47
|
+
type: "button",
|
|
48
|
+
"aria-label": text("copyCommand"),
|
|
49
|
+
onClick: () => {
|
|
50
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
51
|
+
setCopied(true);
|
|
52
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
className: "inline-flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted pointer-coarse:size-11",
|
|
56
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
|
|
57
|
+
})]
|
|
58
|
+
})] });
|
|
50
59
|
};
|
|
51
60
|
//#endregion
|
|
52
61
|
export { FirstRunPage, createAdminCommand };
|
package/dist/auth/messages.js
CHANGED
|
@@ -29,6 +29,8 @@ const text = defineMessages({
|
|
|
29
29
|
noAdminDescription: "This Protobase has no users to sign in with.",
|
|
30
30
|
noAdminExplanation: "Run this in the terminal where Protobase runs, then reload. The first admin is created on the server, not in the browser, so nobody else can claim a fresh URL.",
|
|
31
31
|
copyCommand: "Copy command",
|
|
32
|
+
firstAdminExplanation: "The first user is added in the Protobase Cloud console, not in the browser, so nobody else can claim a fresh URL. Add yourself there, then reload.",
|
|
33
|
+
firstAdminLink: "Add your first user in the Protobase Cloud console",
|
|
32
34
|
reload: "Reload",
|
|
33
35
|
tooManyRequests: "Too many requests. Wait a minute and try again.",
|
|
34
36
|
tooManyAttempts: "Too many attempts. Wait a minute and try again.",
|
|
@@ -127,6 +129,8 @@ const text = defineMessages({
|
|
|
127
129
|
noAdminDescription: "Deze Protobase heeft geen gebruikers om mee in te loggen.",
|
|
128
130
|
noAdminExplanation: "Voer dit uit in de terminal waar Protobase draait en laad de pagina daarna opnieuw. De eerste beheerder wordt op de server gemaakt, niet in de browser, zodat niemand anders een nieuwe URL kan opeisen.",
|
|
129
131
|
copyCommand: "Opdracht kopiëren",
|
|
132
|
+
firstAdminExplanation: "De eerste gebruiker wordt in de Protobase Cloud-console toegevoegd, niet in de browser, zodat niemand anders een nieuwe URL kan opeisen. Voeg jezelf daar toe en laad de pagina daarna opnieuw.",
|
|
133
|
+
firstAdminLink: "Voeg je eerste gebruiker toe in de Protobase Cloud-console",
|
|
130
134
|
reload: "Opnieuw laden",
|
|
131
135
|
tooManyRequests: "Te veel verzoeken. Wacht een minuut en probeer het opnieuw.",
|
|
132
136
|
tooManyAttempts: "Te veel pogingen. Wacht een minuut en probeer het opnieuw.",
|
package/dist/data/use-list.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type UseListParams = {
|
|
|
6
6
|
orderBy?: string;
|
|
7
7
|
pageSize?: number;
|
|
8
8
|
fields?: string[];
|
|
9
|
+
/** Names the records relations point at, as `client.list`'s `labels`: `{ product_id: 'name' }`. */
|
|
10
|
+
labels?: Record<string, string>;
|
|
9
11
|
/** Start at this page token instead of the first page, for example one from `useAnchors`. */
|
|
10
12
|
startToken?: string;
|
|
11
13
|
enabled?: boolean;
|
|
@@ -14,9 +16,13 @@ export type UseListParams = {
|
|
|
14
16
|
};
|
|
15
17
|
type Row = Record<string, unknown>;
|
|
16
18
|
/** Infinite pages of a resource, following `next_page_token`. */
|
|
17
|
-
export declare const useList: (resource: string, { filter, orderBy, pageSize, fields, startToken, enabled, refetchInterval }?: UseListParams) => {
|
|
19
|
+
export declare const useList: (resource: string, { filter, orderBy, pageSize, fields, labels, startToken, enabled, refetchInterval }?: UseListParams) => {
|
|
18
20
|
items: Row[];
|
|
19
21
|
totalEstimate: number | undefined;
|
|
22
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
23
|
+
labels: {
|
|
24
|
+
[k: string]: Record<string, string>;
|
|
25
|
+
};
|
|
20
26
|
data: InfiniteData<ListPage<Row>, unknown>;
|
|
21
27
|
error: Error;
|
|
22
28
|
isError: true;
|
|
@@ -53,6 +59,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
|
|
|
53
59
|
} | {
|
|
54
60
|
items: Row[];
|
|
55
61
|
totalEstimate: number | undefined;
|
|
62
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
63
|
+
labels: {
|
|
64
|
+
[k: string]: Record<string, string>;
|
|
65
|
+
};
|
|
56
66
|
data: InfiniteData<ListPage<Row>, unknown>;
|
|
57
67
|
error: null;
|
|
58
68
|
isError: false;
|
|
@@ -89,6 +99,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
|
|
|
89
99
|
} | {
|
|
90
100
|
items: Row[];
|
|
91
101
|
totalEstimate: number | undefined;
|
|
102
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
103
|
+
labels: {
|
|
104
|
+
[k: string]: Record<string, string>;
|
|
105
|
+
};
|
|
92
106
|
data: undefined;
|
|
93
107
|
error: Error;
|
|
94
108
|
isError: true;
|
|
@@ -125,6 +139,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
|
|
|
125
139
|
} | {
|
|
126
140
|
items: Row[];
|
|
127
141
|
totalEstimate: number | undefined;
|
|
142
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
143
|
+
labels: {
|
|
144
|
+
[k: string]: Record<string, string>;
|
|
145
|
+
};
|
|
128
146
|
data: undefined;
|
|
129
147
|
error: null;
|
|
130
148
|
isError: false;
|
|
@@ -161,6 +179,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
|
|
|
161
179
|
} | {
|
|
162
180
|
items: Row[];
|
|
163
181
|
totalEstimate: number | undefined;
|
|
182
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
183
|
+
labels: {
|
|
184
|
+
[k: string]: Record<string, string>;
|
|
185
|
+
};
|
|
164
186
|
data: undefined;
|
|
165
187
|
error: null;
|
|
166
188
|
isError: false;
|
|
@@ -197,6 +219,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
|
|
|
197
219
|
} | {
|
|
198
220
|
items: Row[];
|
|
199
221
|
totalEstimate: number | undefined;
|
|
222
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
223
|
+
labels: {
|
|
224
|
+
[k: string]: Record<string, string>;
|
|
225
|
+
};
|
|
200
226
|
data: InfiniteData<ListPage<Row>, unknown>;
|
|
201
227
|
isError: false;
|
|
202
228
|
error: null;
|
package/dist/data/use-list.js
CHANGED
|
@@ -4,11 +4,15 @@ import { filterText } from "@protobase/client";
|
|
|
4
4
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
5
5
|
//#region src/data/use-list.ts
|
|
6
6
|
/** Infinite pages of a resource, following `next_page_token`. */
|
|
7
|
-
const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken = "", enabled = true, refetchInterval } = {}) => {
|
|
7
|
+
const useList = (resource, { filter, orderBy, pageSize = 50, fields, labels, startToken = "", enabled = true, refetchInterval } = {}) => {
|
|
8
8
|
const client = useClient();
|
|
9
9
|
const text = filterText(filter) ?? "";
|
|
10
10
|
const query = useInfiniteQuery({
|
|
11
|
-
queryKey: [
|
|
11
|
+
queryKey: [
|
|
12
|
+
...keys.list(resource, text, orderBy ?? "", pageSize, startToken),
|
|
13
|
+
fields?.join(",") ?? "",
|
|
14
|
+
...labels ? [labels] : []
|
|
15
|
+
],
|
|
12
16
|
enabled,
|
|
13
17
|
refetchInterval,
|
|
14
18
|
initialPageParam: startToken,
|
|
@@ -17,6 +21,7 @@ const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken
|
|
|
17
21
|
orderBy,
|
|
18
22
|
pageSize,
|
|
19
23
|
fields,
|
|
24
|
+
labels,
|
|
20
25
|
pageToken: pageParam
|
|
21
26
|
}),
|
|
22
27
|
getNextPageParam: (last) => last.nextPageToken || void 0
|
|
@@ -25,7 +30,9 @@ const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken
|
|
|
25
30
|
return {
|
|
26
31
|
...query,
|
|
27
32
|
items: pages.flatMap((page) => page.items),
|
|
28
|
-
totalEstimate: pages[0]?.totalSizeEstimate
|
|
33
|
+
totalEstimate: pages[0]?.totalSizeEstimate,
|
|
34
|
+
/** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
|
|
35
|
+
labels: Object.fromEntries(Object.keys(labels ?? {}).map((name) => [name, Object.assign({}, ...pages.map((page) => page.labels?.[name] ?? {}))]))
|
|
29
36
|
};
|
|
30
37
|
};
|
|
31
38
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { userById } from "../mocks/users.js";
|
|
2
1
|
import { formatMoney } from "../format/money.js";
|
|
3
2
|
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
4
3
|
import { formatDate, formatDateTime, formatRelativeTime } from "../format/date.js";
|
|
4
|
+
import { userById } from "../mocks/users.js";
|
|
5
5
|
import { fileShown } from "../live/file-values.js";
|
|
6
6
|
import { text } from "./messages.js";
|
|
7
7
|
import { enumLabel } from "@protobase/schema";
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { userById } from "../mocks/users.js";
|
|
3
2
|
import { viewerZone, zoneLabel } from "../format/zone.js";
|
|
4
3
|
import { formatDateTime } from "../format/date.js";
|
|
4
|
+
import { userById } from "../mocks/users.js";
|
|
5
5
|
import { Avatar } from "../primitives/avatar.js";
|
|
6
6
|
import { Badge } from "../primitives/badge.js";
|
|
7
7
|
import { fileShown, shownImage } from "../live/file-values.js";
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//#region src/inline-child-table/columns.ts
|
|
2
|
-
const
|
|
2
|
+
const track = (column) => column.width ?? "minmax(8rem,1fr)";
|
|
3
|
+
/** The columns' tracks, between the drag handle and the menu that `lineGrid` places around them. */
|
|
4
|
+
const lineGridStyle = (columns) => ({ "--line-columns": columns.map(track).join(" ") });
|
|
5
|
+
/** The narrowest the table gets before it scrolls sideways: each column's least width, and the handle and menu. */
|
|
6
|
+
const lineTableMinWidth = (columns) => `${columns.reduce((sum, column) => sum + (column.width?.endsWith("px") ? parseInt(column.width) / 16 : 8), 6)}rem`;
|
|
7
|
+
const lineGrid = "grid grid-cols-[28px_var(--line-columns)_32px] items-center gap-2 pointer-coarse:grid-cols-[44px_var(--line-columns)_44px]";
|
|
3
8
|
//#endregion
|
|
4
|
-
export { lineGrid };
|
|
9
|
+
export { lineGrid, lineGridStyle, lineTableMinWidth };
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
1
2
|
import { Button } from "../primitives/button.js";
|
|
2
|
-
import {
|
|
3
|
-
import { lineGrid } from "./columns.js";
|
|
3
|
+
import { lineGrid, lineGridStyle } from "./columns.js";
|
|
4
4
|
import { text } from "./messages.js";
|
|
5
5
|
import { LineRow } from "./line-row.js";
|
|
6
6
|
import { reorder } from "./reorder.js";
|
|
7
|
-
import { TotalsFooter } from "./totals-footer.js";
|
|
8
7
|
import { Plus } from "lucide-react";
|
|
9
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
9
|
import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
|
|
11
10
|
import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
12
11
|
//#region src/inline-child-table/inline-child-table.tsx
|
|
13
12
|
const headerCell = "px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground";
|
|
14
|
-
const announcementsFor = (lines) => {
|
|
15
|
-
const name = (id) =>
|
|
13
|
+
const announcementsFor = (lines, lineName) => {
|
|
14
|
+
const name = (id) => {
|
|
15
|
+
const line = lines.find((each) => each.id === id);
|
|
16
|
+
return line ? lineName(line) : text("line");
|
|
17
|
+
};
|
|
16
18
|
const place = (id) => lines.findIndex((line) => line.id === id) + 1;
|
|
17
19
|
return {
|
|
18
20
|
onDragStart: ({ active }) => text("pickedUp", name(active.id), place(active.id), lines.length),
|
|
@@ -21,15 +23,16 @@ const announcementsFor = (lines) => {
|
|
|
21
23
|
onDragCancel: ({ active }) => text("cancelled", name(active.id), place(active.id))
|
|
22
24
|
};
|
|
23
25
|
};
|
|
24
|
-
|
|
26
|
+
/** The lines of a record as rows of the given columns, put in order by dragging or from each row's menu. */
|
|
27
|
+
const InlineChildTable = ({ lines, columns, lineName, onReorder, reorderable = Boolean(onReorder), onAdd, onRemove, removable = () => true, lineError, footer }) => {
|
|
25
28
|
const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, { activationConstraint: {
|
|
26
29
|
delay: 250,
|
|
27
30
|
tolerance: 5
|
|
28
31
|
} }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
29
32
|
const move = (from, to) => {
|
|
30
33
|
const line = lines[from];
|
|
31
|
-
if (!line || to < 0 || to >= lines.length || from === to) return;
|
|
32
|
-
|
|
34
|
+
if (!line || !onReorder || to < 0 || to >= lines.length || from === to) return;
|
|
35
|
+
onReorder(reorder(lines, from, to), {
|
|
33
36
|
id: line.id,
|
|
34
37
|
from,
|
|
35
38
|
to
|
|
@@ -39,48 +42,31 @@ const InlineChildTable = ({ lines, onChange, onReorder, vatRate = .21, readOnly,
|
|
|
39
42
|
if (!over || active.id === over.id) return;
|
|
40
43
|
move(lines.findIndex((line) => line.id === active.id), lines.findIndex((line) => line.id === over.id));
|
|
41
44
|
};
|
|
42
|
-
const addLine = () => {
|
|
43
|
-
const product = productAt(lines.length * 3 + 1);
|
|
44
|
-
onChange([...lines, {
|
|
45
|
-
id: `new-${lines.length}-${product.id}`,
|
|
46
|
-
productId: product.id,
|
|
47
|
-
description: product.name,
|
|
48
|
-
quantity: 1,
|
|
49
|
-
unitPriceCents: product.priceCents
|
|
50
|
-
}]);
|
|
51
|
-
};
|
|
52
45
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
53
46
|
className: "overflow-hidden rounded-lg border border-border bg-background",
|
|
47
|
+
style: lineGridStyle(columns),
|
|
54
48
|
children: [
|
|
55
49
|
/* @__PURE__ */ jsxs("div", {
|
|
56
50
|
className: `${lineGrid} border-b border-border bg-surface px-2 py-2`,
|
|
57
51
|
children: [
|
|
58
52
|
/* @__PURE__ */ jsx("span", {}),
|
|
59
|
-
/* @__PURE__ */ jsx("span", {
|
|
60
|
-
className: headerCell,
|
|
61
|
-
children:
|
|
62
|
-
}),
|
|
63
|
-
/* @__PURE__ */ jsx("span", {
|
|
64
|
-
className: `${headerCell} text-right`,
|
|
65
|
-
children: text("qty")
|
|
66
|
-
}),
|
|
67
|
-
/* @__PURE__ */ jsx("span", {
|
|
68
|
-
className: `${headerCell} text-right`,
|
|
69
|
-
children: text("unitPrice")
|
|
70
|
-
}),
|
|
71
|
-
/* @__PURE__ */ jsx("span", {
|
|
72
|
-
className: `${headerCell} text-right`,
|
|
73
|
-
children: text("lineTotal")
|
|
74
|
-
}),
|
|
53
|
+
columns.map((column) => /* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: cn(headerCell, column.align === "right" && "text-right"),
|
|
55
|
+
children: column.label
|
|
56
|
+
}, column.key)),
|
|
75
57
|
/* @__PURE__ */ jsx("span", {})
|
|
76
58
|
]
|
|
77
59
|
}),
|
|
60
|
+
lines.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
61
|
+
className: "px-4 py-3 text-muted-foreground",
|
|
62
|
+
children: text("noLines")
|
|
63
|
+
}),
|
|
78
64
|
/* @__PURE__ */ jsx(DndContext, {
|
|
79
65
|
sensors,
|
|
80
66
|
collisionDetection: closestCenter,
|
|
81
67
|
onDragEnd,
|
|
82
68
|
accessibility: {
|
|
83
|
-
announcements: announcementsFor(lines),
|
|
69
|
+
announcements: announcementsFor(lines, lineName),
|
|
84
70
|
screenReaderInstructions: { draggable: text("instructions") }
|
|
85
71
|
},
|
|
86
72
|
children: /* @__PURE__ */ jsx(SortableContext, {
|
|
@@ -88,33 +74,28 @@ const InlineChildTable = ({ lines, onChange, onReorder, vatRate = .21, readOnly,
|
|
|
88
74
|
strategy: verticalListSortingStrategy,
|
|
89
75
|
children: lines.map((line, index) => /* @__PURE__ */ jsx(LineRow, {
|
|
90
76
|
line,
|
|
77
|
+
name: lineName(line),
|
|
78
|
+
columns,
|
|
91
79
|
position: index,
|
|
92
80
|
count: lines.length,
|
|
93
81
|
reorderable,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...patch
|
|
98
|
-
} : item)),
|
|
99
|
-
onRemove: () => onChange(lines.filter((item) => item.id !== line.id)),
|
|
100
|
-
onMove: (to) => move(index, to)
|
|
82
|
+
onRemove: onRemove && removable(line) ? () => onRemove(line) : void 0,
|
|
83
|
+
onMove: (to) => move(index, to),
|
|
84
|
+
error: lineError?.(line)
|
|
101
85
|
}, line.id))
|
|
102
86
|
})
|
|
103
87
|
}),
|
|
104
|
-
|
|
88
|
+
onAdd && /* @__PURE__ */ jsx("div", {
|
|
105
89
|
className: "border-t border-border bg-surface px-2 py-1.5",
|
|
106
90
|
children: /* @__PURE__ */ jsxs(Button, {
|
|
107
91
|
variant: "ghost",
|
|
108
92
|
size: "sm",
|
|
109
|
-
onClick:
|
|
93
|
+
onClick: onAdd,
|
|
110
94
|
children: [/* @__PURE__ */ jsx(Plus, { className: "size-3.5" }), text("addLine")]
|
|
111
95
|
})
|
|
112
96
|
})
|
|
113
97
|
]
|
|
114
|
-
}),
|
|
115
|
-
lines,
|
|
116
|
-
vatRate
|
|
117
|
-
})] });
|
|
98
|
+
}), footer] });
|
|
118
99
|
};
|
|
119
100
|
//#endregion
|
|
120
101
|
export { InlineChildTable };
|