@protobase/ui 0.18.0 → 0.19.1
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.d.ts +2 -2
- package/dist/app/app.js +12 -9
- package/dist/app/current-address.d.ts +1 -0
- package/dist/app/current-address.js +2 -1
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/custom-display.js +1 -1
- package/dist/app/displays/map-display.js +1 -1
- package/dist/app/displays/record-card.js +1 -1
- package/dist/app/gallery/gallery-items.js +37 -0
- package/dist/app/gallery/gallery-layout.js +17 -0
- package/dist/app/gallery/gallery-section.js +124 -0
- package/dist/app/gallery/lightbox.js +121 -0
- package/dist/app/gallery/photo-details.js +49 -0
- package/dist/app/gallery/use-gallery-photos.js +38 -0
- package/dist/app/pages/merge-ui.d.ts +1 -1
- package/dist/app/pages/merge-ui.js +12 -2
- package/dist/app/pages/project-ui.d.ts +24 -4
- package/dist/app/pages/project-ui.js +3 -3
- package/dist/app/pages/render-node.js +1 -1
- package/dist/app/record-lines.js +2 -2
- package/dist/app/record-mode/record-mode.js +31 -0
- package/dist/app/record-page.js +109 -28
- package/dist/app/use-live-save.js +4 -1
- package/dist/assistant-dock/widget-view.js +1 -1
- package/dist/auth/auth-gate.js +7 -1
- package/dist/auth/auth-provider.js +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/package.js +1 -1
- package/dist/plugins/before-sign-in.js +49 -0
- package/dist/plugins/plugin-gate.js +8 -8
- package/dist/plugins/plugin-source.js +32 -0
- package/dist/plugins/plugin-ui.js +5 -3
- package/dist/plugins/use-plugin-loads.js +3 -4
- package/dist/record-view/spec-table.js +25 -0
- package/dist/shell/assets/app-DYl-j-Nf.js +66 -0
- package/dist/shell/assets/index-BLnOMDD8.css +2 -0
- package/dist/shell/assets/{index-BXEkf4-O.js → index-Bes91xE3.js} +1 -1
- package/dist/shell/assets/{maplibre-DCGyNbM2.js → maplibre-BDe9GhVw.js} +2 -2
- package/dist/shell/assets/{record-map-DEn8Ulev.js → record-map-CY2LFq1z.js} +1 -1
- package/dist/shell/assets/{records-map-DdLiVxEp.js → records-map-B_DHVvmx.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-Bb2W4Il8.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-j50iAbLC.js +0 -66
- package/dist/shell/assets/index-D0AzKE47.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-zMOavpbt.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useProjectUi } from "./project-ui.js";
|
|
2
1
|
import { Problem } from "./problem.js";
|
|
3
2
|
import { CustomBoundary } from "./custom-boundary.js";
|
|
3
|
+
import { useProjectUi } from "./project-ui.js";
|
|
4
4
|
import { ActionBlock } from "./blocks/action.js";
|
|
5
5
|
import { CardBlock } from "./blocks/card.js";
|
|
6
6
|
import { CardRowBlock } from "./blocks/card-row.js";
|
package/dist/app/record-lines.js
CHANGED
|
@@ -14,7 +14,7 @@ const toLine = (record) => ({
|
|
|
14
14
|
unitPriceCents: Math.round(Number(record.unitPrice) * 100)
|
|
15
15
|
});
|
|
16
16
|
/** The lines of a record. Reordering only changes `draft`; the record's Save writes it, in one batch. */
|
|
17
|
-
const RecordLines = ({ lines, draft, vatRate, saving }) => {
|
|
17
|
+
const RecordLines = ({ lines, draft, vatRate, saving, locked = false }) => {
|
|
18
18
|
const canReorder = can(usePermissions(lines.name), "update");
|
|
19
19
|
return /* @__PURE__ */ jsx(Section, {
|
|
20
20
|
title: "Lines",
|
|
@@ -29,7 +29,7 @@ const RecordLines = ({ lines, draft, vatRate, saving }) => {
|
|
|
29
29
|
onReorder: (next) => draft.setOrder(next.map((line) => line.id)),
|
|
30
30
|
vatRate,
|
|
31
31
|
readOnly: true,
|
|
32
|
-
reorderable: canReorder && !saving
|
|
32
|
+
reorderable: canReorder && !saving && !locked
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
35
|
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/app/record-mode/record-mode.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the record page shows a user, by the view's `mode`, whether they may change the record and whether its address
|
|
4
|
+
* asks for the form (`edit`). Always editable (`edit`, the default) shows the form, locked to a user who may not change
|
|
5
|
+
* it, and ignores `edit`; `read` and `locked` show the read view or the locked form until the address asks for the
|
|
6
|
+
* form, which only a user who may change the record gets. `offersEdit` is whether the page links to the form;
|
|
7
|
+
* `editing` whether the record is being edited, which decides the help shown only while editing.
|
|
8
|
+
*/
|
|
9
|
+
const recordShown = (mode, canUpdate, edit) => {
|
|
10
|
+
if (mode === void 0 || mode === "edit") return {
|
|
11
|
+
shown: canUpdate ? "form" : "locked",
|
|
12
|
+
offersEdit: false,
|
|
13
|
+
editing: canUpdate
|
|
14
|
+
};
|
|
15
|
+
if (canUpdate && edit) return {
|
|
16
|
+
shown: "form",
|
|
17
|
+
offersEdit: false,
|
|
18
|
+
editing: true
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
shown: mode,
|
|
22
|
+
offersEdit: canUpdate,
|
|
23
|
+
editing: false
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Whether a help line shows: always, unless it, or else the view, says only while editing and the record is not. */
|
|
27
|
+
const helpShown = (own, view, editing) => (own ?? view?.helpWhen ?? "always") === "always" || editing;
|
|
28
|
+
/** A field's help line on the record page, when it shows. */
|
|
29
|
+
const fieldHelp = (hints, view, editing) => hints?.help !== void 0 && helpShown(hints.helpWhen, view, editing) ? hints.help : void 0;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { fieldHelp, helpShown, recordShown };
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { inApp } from "../app-shell/breadcrumb.js";
|
|
1
3
|
import { useAuth } from "../auth/auth-provider.js";
|
|
2
4
|
import { formatRelativeTime } from "../format/date.js";
|
|
3
|
-
import { Button } from "../primitives/button.js";
|
|
5
|
+
import { Button, buttonClass } from "../primitives/button.js";
|
|
4
6
|
import { keys } from "../data/query-keys.js";
|
|
5
7
|
import { useClient } from "../data/api-provider.js";
|
|
6
8
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
@@ -10,7 +12,7 @@ import { humanize } from "../live/naming.js";
|
|
|
10
12
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
11
13
|
import { displayValue } from "../live/display-value.js";
|
|
12
14
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
13
|
-
import { useAppAddress } from "./use-app-address.js";
|
|
15
|
+
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
14
16
|
import { ErrorBanner } from "./error-banner.js";
|
|
15
17
|
import { shownWidget } from "./field-types/shown-widget.js";
|
|
16
18
|
import { WidgetPart } from "./field-types/widget-part.js";
|
|
@@ -20,10 +22,12 @@ import { Field } from "../record-view/field.js";
|
|
|
20
22
|
import { Section } from "../record-view/section.js";
|
|
21
23
|
import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record-sidebar.js";
|
|
22
24
|
import { RecordHeader } from "../record-view/record-header.js";
|
|
25
|
+
import { SpecTable } from "../record-view/spec-table.js";
|
|
23
26
|
import { toneFor } from "../live/columns-from-model.js";
|
|
24
27
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
25
28
|
import { useRestore } from "./delete/use-restore.js";
|
|
26
29
|
import { ValueBadge } from "./value-badge.js";
|
|
30
|
+
import { FieldValue } from "./pages/field-value.js";
|
|
27
31
|
import { auditField } from "../live/audit-fields.js";
|
|
28
32
|
import { CustomSection } from "./custom-section.js";
|
|
29
33
|
import { MapSection } from "./map/map-section.js";
|
|
@@ -32,11 +36,13 @@ import { RecordLines, findLinesResource } from "./record-lines.js";
|
|
|
32
36
|
import { draftStorageKey } from "./draft-storage.js";
|
|
33
37
|
import { useLines } from "./use-lines.js";
|
|
34
38
|
import { useLinesDraft } from "./use-lines-draft.js";
|
|
39
|
+
import { GallerySection } from "./gallery/gallery-section.js";
|
|
40
|
+
import { fieldHelp, helpShown, recordShown } from "./record-mode/record-mode.js";
|
|
35
41
|
import { useRecordEditor } from "./use-record-editor.js";
|
|
36
42
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
37
43
|
import { RelatedSection } from "./related-section.js";
|
|
38
44
|
import { Fragment, useMemo } from "react";
|
|
39
|
-
import { Check } from "lucide-react";
|
|
45
|
+
import { Check, Pencil } from "lucide-react";
|
|
40
46
|
import { isApiError } from "@protobase/client";
|
|
41
47
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
42
48
|
import { useQueryClient } from "@tanstack/react-query";
|
|
@@ -45,16 +51,17 @@ import { enumLabel } from "@protobase/schema";
|
|
|
45
51
|
const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
|
|
46
52
|
/**
|
|
47
53
|
* The view's layout, or one section with the editable fields when the view has none. A user who may only read the
|
|
48
|
-
* resource has every field read-only, so they get every field, locked, rather than an empty section
|
|
54
|
+
* resource has every field read-only, so they get every field, locked, rather than an empty section; so does the read
|
|
55
|
+
* view, which shows the record rather than what can be changed.
|
|
49
56
|
*/
|
|
50
|
-
const layoutFor = (model, view) => {
|
|
57
|
+
const layoutFor = (model, view, read) => {
|
|
51
58
|
if (view && view.layout.length > 0) return view.layout;
|
|
52
59
|
const shown = Object.values(model.fields).filter((field) => !hidden(model, field.name));
|
|
53
60
|
const editable = shown.filter((field) => !field.readOnly);
|
|
54
61
|
return [{
|
|
55
62
|
kind: "section",
|
|
56
63
|
title: "Details",
|
|
57
|
-
fields: (editable.length > 0 ? editable : shown).map((field) => field.name)
|
|
64
|
+
fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
|
|
58
65
|
}];
|
|
59
66
|
};
|
|
60
67
|
const RecordPage = ({ resource, recordKey }) => {
|
|
@@ -92,7 +99,7 @@ const RecordPage = ({ resource, recordKey }) => {
|
|
|
92
99
|
const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
93
100
|
const meta = useAdminMeta();
|
|
94
101
|
const { basePath } = useRouter();
|
|
95
|
-
const { go, open } = useAppAddress();
|
|
102
|
+
const { address, go, open } = useAppAddress();
|
|
96
103
|
const view = meta.views[model.name];
|
|
97
104
|
const singular = view?.names?.singular ?? humanize(model.name);
|
|
98
105
|
const title = recordTitle(model, view, stored.record, recordKey);
|
|
@@ -109,12 +116,23 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
109
116
|
draftKey: auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
110
117
|
lines: linesDraft
|
|
111
118
|
});
|
|
112
|
-
useNavigationGuard(editor.dirty);
|
|
113
119
|
const queryClient = useQueryClient();
|
|
114
120
|
const client = useClient();
|
|
115
121
|
const permissions = usePermissions(model.name);
|
|
116
122
|
const restore = useRestore(model);
|
|
117
123
|
const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
|
|
124
|
+
const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
|
|
125
|
+
useNavigationGuard(editor.dirty && shown === "form");
|
|
126
|
+
const readAddress = {
|
|
127
|
+
kind: "record",
|
|
128
|
+
resource: model.name,
|
|
129
|
+
key: recordKey
|
|
130
|
+
};
|
|
131
|
+
const editAddress = {
|
|
132
|
+
...readAddress,
|
|
133
|
+
edit: true
|
|
134
|
+
};
|
|
135
|
+
const leavesForm = shown === "form" && view?.recordMode !== void 0 && view.recordMode !== "edit";
|
|
118
136
|
const canDelete = can(permissions, "delete") && stored.permissions?.delete !== false;
|
|
119
137
|
const remove = useDeleteFlow({
|
|
120
138
|
model,
|
|
@@ -128,7 +146,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
128
146
|
});
|
|
129
147
|
const related = useRelatedRecords(model, view, meta.resources, meta.views, stored.record, basePath);
|
|
130
148
|
const { fields: fieldTypes } = useProjectUi();
|
|
131
|
-
const layout = layoutFor(model, view);
|
|
149
|
+
const layout = layoutFor(model, view, shown === "read");
|
|
132
150
|
const sidebarFields = layout.flatMap((item) => item.kind === "sidebar" ? item.fields : []);
|
|
133
151
|
const status = model.fields.status?.type === "enum" ? String(stored.record.status) : void 0;
|
|
134
152
|
const updatedAt = auditField(model, "updated");
|
|
@@ -142,7 +160,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
142
160
|
const conflict = editor.conflicts[name];
|
|
143
161
|
return /* @__PURE__ */ jsx(Field, {
|
|
144
162
|
label: hint(name)?.label ?? humanize(name),
|
|
145
|
-
help: hint(name)
|
|
163
|
+
help: fieldHelp(hint(name), view, editing),
|
|
146
164
|
error: editor.errors[name],
|
|
147
165
|
wide: Boolean(hint(name)?.multiline),
|
|
148
166
|
conflict: conflict === void 0 ? void 0 : {
|
|
@@ -163,7 +181,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
163
181
|
stored: stored.record[name],
|
|
164
182
|
relationLabel: related.labels[name],
|
|
165
183
|
relationTo: related.addresses[name],
|
|
166
|
-
locked:
|
|
184
|
+
locked: shown !== "form" || access === "read",
|
|
167
185
|
invalid: Boolean(editor.errors[name]),
|
|
168
186
|
reveal: field.sensitive ? () => client.reveal(model.name, recordKey, name) : void 0,
|
|
169
187
|
resource: model.name,
|
|
@@ -171,6 +189,30 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
171
189
|
})
|
|
172
190
|
}, field.sensitive ? `${name}:${stored.etag}` : name);
|
|
173
191
|
};
|
|
192
|
+
const readRow = (name) => {
|
|
193
|
+
const field = model.fields[name];
|
|
194
|
+
const value = field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
195
|
+
field,
|
|
196
|
+
hints: hint(name),
|
|
197
|
+
value: "",
|
|
198
|
+
stored: stored.record[name],
|
|
199
|
+
locked: true,
|
|
200
|
+
reveal: () => client.reveal(model.name, recordKey, name),
|
|
201
|
+
resource: model.name,
|
|
202
|
+
onChange: (next) => editor.change(name, next)
|
|
203
|
+
}) : /* @__PURE__ */ jsx(FieldValue, {
|
|
204
|
+
model,
|
|
205
|
+
field,
|
|
206
|
+
value: stored.record[name],
|
|
207
|
+
relationLabel: related.labels[name]
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
label: hint(name)?.label ?? humanize(name),
|
|
211
|
+
help: fieldHelp(hint(name), view, editing),
|
|
212
|
+
wide: Boolean(hint(name)?.multiline),
|
|
213
|
+
value
|
|
214
|
+
};
|
|
215
|
+
};
|
|
174
216
|
const firstSidebar = layout.findIndex((item) => item.kind === "sidebar");
|
|
175
217
|
const summary = /* @__PURE__ */ jsx(MetadataCard, {
|
|
176
218
|
title: "Summary",
|
|
@@ -206,34 +248,51 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
206
248
|
etag: stored.etag,
|
|
207
249
|
permissions: stored.permissions
|
|
208
250
|
};
|
|
209
|
-
const
|
|
251
|
+
const save = async () => {
|
|
252
|
+
if (await editor.save() && leavesForm) go(readAddress, { replace: true });
|
|
253
|
+
};
|
|
254
|
+
const deleteButton = canDelete && /* @__PURE__ */ jsx(Button, {
|
|
255
|
+
variant: "danger",
|
|
256
|
+
className: "max-md:min-h-11",
|
|
257
|
+
onClick: () => void remove.start({
|
|
258
|
+
key: recordKey,
|
|
259
|
+
title,
|
|
260
|
+
etag: stored.etag,
|
|
261
|
+
snapshot: stored.record
|
|
262
|
+
}),
|
|
263
|
+
children: "Delete"
|
|
264
|
+
});
|
|
265
|
+
const saveControls = offersEdit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [deleteButton, /* @__PURE__ */ jsxs(AppLink, {
|
|
266
|
+
to: editAddress,
|
|
267
|
+
className: cn(buttonClass({ variant: "primary" }), "max-md:min-h-11 max-md:flex-1"),
|
|
268
|
+
children: [/* @__PURE__ */ jsx(Pencil, {
|
|
269
|
+
className: "size-3.5",
|
|
270
|
+
"aria-hidden": true
|
|
271
|
+
}), "Edit"]
|
|
272
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
210
273
|
editor.dirty && /* @__PURE__ */ jsx("span", {
|
|
211
274
|
className: "text-xs text-muted-foreground",
|
|
212
275
|
children: "Unsaved changes"
|
|
213
276
|
}),
|
|
214
|
-
editor.dirty && canUpdate && /* @__PURE__ */ jsx(Button, {
|
|
277
|
+
editor.dirty && canUpdate && !leavesForm && /* @__PURE__ */ jsx(Button, {
|
|
215
278
|
className: "max-md:min-h-11",
|
|
216
279
|
disabled: editor.saving,
|
|
217
280
|
onClick: editor.discard,
|
|
218
281
|
children: "Discard"
|
|
219
282
|
}),
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
className: "max-md:min-h-11",
|
|
223
|
-
onClick: () =>
|
|
224
|
-
|
|
225
|
-
title,
|
|
226
|
-
etag: stored.etag,
|
|
227
|
-
snapshot: stored.record
|
|
228
|
-
}),
|
|
229
|
-
children: "Delete"
|
|
283
|
+
leavesForm && /* @__PURE__ */ jsx(AppLink, {
|
|
284
|
+
to: readAddress,
|
|
285
|
+
className: cn(buttonClass(), "max-md:min-h-11"),
|
|
286
|
+
onClick: (event) => inApp(event) && editor.discard(),
|
|
287
|
+
children: "Cancel"
|
|
230
288
|
}),
|
|
289
|
+
deleteButton,
|
|
231
290
|
canUpdate && /* @__PURE__ */ jsxs(Button, {
|
|
232
291
|
variant: "primary",
|
|
233
292
|
className: "max-md:min-h-11 max-md:flex-1",
|
|
234
293
|
loading: editor.saving,
|
|
235
294
|
disabled: !editor.dirty && editor.phase !== "saved",
|
|
236
|
-
onClick: () => void
|
|
295
|
+
onClick: () => void save(),
|
|
237
296
|
children: [editor.phase === "saved" && /* @__PURE__ */ jsx(Check, { className: "size-4" }), editor.phase === "saved" ? "Saved" : "Save"]
|
|
238
297
|
})
|
|
239
298
|
] });
|
|
@@ -273,7 +332,23 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
273
332
|
error: /* @__PURE__ */ new Error("The lines were reloaded. Your new order is kept: Save again to apply it, or Discard to drop it.")
|
|
274
333
|
})
|
|
275
334
|
}),
|
|
276
|
-
editor.
|
|
335
|
+
shown !== "form" && offersEdit && editor.dirty && /* @__PURE__ */ jsxs("div", {
|
|
336
|
+
className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
|
|
337
|
+
role: "status",
|
|
338
|
+
children: [/* @__PURE__ */ jsx("span", { children: "You have unsaved changes to this record." }), /* @__PURE__ */ jsxs("span", {
|
|
339
|
+
className: "flex gap-2",
|
|
340
|
+
children: [/* @__PURE__ */ jsx(AppLink, {
|
|
341
|
+
to: editAddress,
|
|
342
|
+
className: buttonClass({ size: "sm" }),
|
|
343
|
+
children: "Continue editing"
|
|
344
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
345
|
+
size: "sm",
|
|
346
|
+
onClick: editor.discard,
|
|
347
|
+
children: "Discard"
|
|
348
|
+
})]
|
|
349
|
+
})]
|
|
350
|
+
}),
|
|
351
|
+
shown === "form" && editor.restored && /* @__PURE__ */ jsxs("div", {
|
|
277
352
|
className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
|
|
278
353
|
role: "status",
|
|
279
354
|
children: [/* @__PURE__ */ jsx("span", { children: "Your unsaved changes were restored." }), /* @__PURE__ */ jsxs("span", {
|
|
@@ -307,13 +382,18 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
307
382
|
item,
|
|
308
383
|
record: stored.record
|
|
309
384
|
}, `map:${index}`)];
|
|
385
|
+
if (item.kind === "gallery") return [/* @__PURE__ */ jsx(GallerySection, {
|
|
386
|
+
item,
|
|
387
|
+
record: stored.record,
|
|
388
|
+
recordKey
|
|
389
|
+
}, `gallery:${index}`)];
|
|
310
390
|
if (item.kind !== "section") return [];
|
|
311
391
|
const fields = item.fields.filter((name) => model.fields[name] && visible(name));
|
|
312
392
|
if (fields.length === 0) return [];
|
|
313
393
|
return [/* @__PURE__ */ jsx(Section, {
|
|
314
394
|
title: item.title,
|
|
315
|
-
help: item.help,
|
|
316
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
395
|
+
help: helpShown(item.helpWhen, view, editing) ? item.help : void 0,
|
|
396
|
+
children: shown === "read" ? /* @__PURE__ */ jsx(SpecTable, { rows: fields.map(readRow) }) : /* @__PURE__ */ jsx("div", {
|
|
317
397
|
className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
|
|
318
398
|
children: fields.map(fieldView)
|
|
319
399
|
})
|
|
@@ -322,7 +402,8 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
322
402
|
lines: linesModel,
|
|
323
403
|
draft: linesDraft,
|
|
324
404
|
vatRate: Number(stored.record.vatRate ?? 21) / 100,
|
|
325
|
-
saving: editor.saving
|
|
405
|
+
saving: editor.saving,
|
|
406
|
+
locked: shown !== "form"
|
|
326
407
|
})]
|
|
327
408
|
}), /* @__PURE__ */ jsxs("aside", {
|
|
328
409
|
className: "space-y-4 md:pt-8",
|
|
@@ -5,7 +5,8 @@ import { isApiError } from "@protobase/client";
|
|
|
5
5
|
const savedForMs = 2e3;
|
|
6
6
|
/**
|
|
7
7
|
* Shows the outcome of a save the way the view asks: a toast (spinner, then check) or the state of
|
|
8
|
-
* the Save button. API errors are reported and not rethrown; anything else propagates.
|
|
8
|
+
* the Save button. API errors are reported and not rethrown; anything else propagates. `run` resolves to whether the
|
|
9
|
+
* save went through.
|
|
9
10
|
*/
|
|
10
11
|
const useLiveSave = (mode, subject) => {
|
|
11
12
|
const toast = useToast();
|
|
@@ -30,6 +31,7 @@ const useLiveSave = (mode, subject) => {
|
|
|
30
31
|
setPhase("saved");
|
|
31
32
|
timer.current = setTimeout(() => setPhase("idle"), savedForMs);
|
|
32
33
|
}
|
|
34
|
+
return true;
|
|
33
35
|
} catch (error) {
|
|
34
36
|
if (!isApiError(error)) throw error;
|
|
35
37
|
const title = error.status === 412 ? "Changed by someone else" : "Could not save";
|
|
@@ -42,6 +44,7 @@ const useLiveSave = (mode, subject) => {
|
|
|
42
44
|
setPhase("idle");
|
|
43
45
|
setMessage(`${title}: ${error.message}`);
|
|
44
46
|
}
|
|
47
|
+
return false;
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
50
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ActionCard } from "../action-card/action-card.js";
|
|
2
1
|
import { reportRenderError } from "../app/error-reporting.js";
|
|
2
|
+
import { ActionCard } from "../action-card/action-card.js";
|
|
3
3
|
import { useProjectUi } from "../app/pages/project-ui.js";
|
|
4
4
|
import { Component } from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
package/dist/auth/auth-gate.js
CHANGED
|
@@ -19,6 +19,7 @@ import { TwoFactorPage } from "./two-factor-page.js";
|
|
|
19
19
|
import { useSignInAddress } from "./use-sign-in-address.js";
|
|
20
20
|
import { readInvitationLink, withoutInvitationLink } from "../organizations/invitation-link.js";
|
|
21
21
|
import { InvitationPage } from "../organizations/invitation-page.js";
|
|
22
|
+
import { BeforeSignIn } from "../plugins/before-sign-in.js";
|
|
22
23
|
import { useCallback, useEffect, useState } from "react";
|
|
23
24
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
24
25
|
import { signInPath } from "@protobase/schema";
|
|
@@ -62,9 +63,14 @@ const Redirect = ({ to }) => {
|
|
|
62
63
|
* browse, and the right page otherwise: the step of the sign-in route the address names, or the page an emailed reset
|
|
63
64
|
* link, an invitation link or a staff link opened. Someone signed out without a visitor's browsing goes to the sign-in
|
|
64
65
|
* route, and signing in comes back to the page the route was opened from. An invited person with an account signs in
|
|
65
|
-
* first and comes back to the invitation.
|
|
66
|
+
* first and comes back to the invitation. The plugins the app loads before sign-in draw around what someone signed
|
|
67
|
+
* out sees (see `BeforeSignIn`), while the gate keeps its state.
|
|
66
68
|
*/
|
|
67
69
|
const AuthGate = ({ workspace, children }) => {
|
|
70
|
+
const page = useAuthPage(workspace, children);
|
|
71
|
+
return /* @__PURE__ */ jsx(BeforeSignIn, { children: page });
|
|
72
|
+
};
|
|
73
|
+
const useAuthPage = (workspace, children) => {
|
|
68
74
|
const { state, recheck, passwordReset, signOut, signInProviders, canSignUp, browse } = useAuth();
|
|
69
75
|
const { path, params, basePath, navigate } = useRouter();
|
|
70
76
|
const { address, next, open, link, leave } = useSignInAddress();
|
|
@@ -21,6 +21,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
21
21
|
const [staffSignIn, setStaffSignIn] = useState();
|
|
22
22
|
const [organizations, setOrganizations] = useState();
|
|
23
23
|
const [anonymous, setAnonymous] = useState(false);
|
|
24
|
+
const [beforeSignIn, setBeforeSignIn] = useState([]);
|
|
24
25
|
const signedOutState = useCallback(() => anonymous ? { kind: "visitor" } : { kind: "signed-out" }, [anonymous]);
|
|
25
26
|
const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
|
|
26
27
|
const signedOut = useRef(markSignedOut);
|
|
@@ -54,6 +55,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
54
55
|
setStaffSignIn(status.staffSignIn);
|
|
55
56
|
setOrganizations(status.organizations);
|
|
56
57
|
setAnonymous(Boolean(status.anonymous));
|
|
58
|
+
setBeforeSignIn(status.plugins ?? []);
|
|
57
59
|
if (status.needsAdmin) return setState({ kind: "needs-admin" });
|
|
58
60
|
const user = await session.session();
|
|
59
61
|
return user ? enter(user) : setState({ kind: status.anonymous ? "visitor" : "signed-out" });
|
|
@@ -79,6 +81,13 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
79
81
|
...organizations && { organizations },
|
|
80
82
|
markSignedOut,
|
|
81
83
|
recheck,
|
|
84
|
+
beforeSignIn,
|
|
85
|
+
adoptSession: async () => {
|
|
86
|
+
const user = await session.session();
|
|
87
|
+
if (!user) return false;
|
|
88
|
+
await enter(user);
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
82
91
|
...anonymous && { browse: () => {
|
|
83
92
|
setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current);
|
|
84
93
|
} },
|
|
@@ -116,6 +125,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
116
125
|
staffSignIn,
|
|
117
126
|
organizations,
|
|
118
127
|
anonymous,
|
|
128
|
+
beforeSignIn,
|
|
119
129
|
signedOutState,
|
|
120
130
|
markSignedOut,
|
|
121
131
|
recheck,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FieldWidget, FieldWidgetEditorProps, FieldWidgetProps } from "./app/field-types/field-widget.js";
|
|
2
|
-
import { ActionContext, ActionHandler, ProjectUi, definePlugin, defineUi } from "./app/pages/project-ui.js";
|
|
2
|
+
import { ActionContext, ActionHandler, BeforeSignInProps, ProjectUi, definePlugin, defineUi } from "./app/pages/project-ui.js";
|
|
3
3
|
import { PluginInfo, usePlugin } from "./plugins/plugin-context.js";
|
|
4
4
|
import { App, AppProps } from "./app/app.js";
|
|
5
5
|
import "./app/index.js";
|
|
@@ -42,4 +42,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
|
|
|
42
42
|
import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
|
|
43
43
|
import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
|
|
44
44
|
import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
|
|
45
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
|
45
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type BeforeSignInProps, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { OtpInput } from "./primitives/otp-input.js";
|
|
2
2
|
import { ApiProvider, useClient, useResourceClient } from "./data/api-provider.js";
|
|
3
|
+
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
4
|
+
import { mergeUi } from "./app/pages/merge-ui.js";
|
|
5
|
+
import { usePlugin } from "./plugins/plugin-context.js";
|
|
6
|
+
import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
|
|
3
7
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
4
8
|
import { useRecord } from "./data/use-record.js";
|
|
5
9
|
import { ChatMessage } from "./chat/chat-message.js";
|
|
@@ -25,10 +29,6 @@ import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
|
25
29
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
26
30
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
27
31
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
28
|
-
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
29
|
-
import { mergeUi } from "./app/pages/merge-ui.js";
|
|
30
|
-
import { usePlugin } from "./plugins/plugin-context.js";
|
|
31
|
-
import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
|
|
32
32
|
import { useList } from "./data/use-list.js";
|
|
33
33
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
34
34
|
import { App } from "./app/app.js";
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Spinner } from "../primitives/spinner.js";
|
|
2
|
+
import { useAuth } from "../auth/auth-provider.js";
|
|
3
|
+
import { useRouter } from "../app/router.js";
|
|
4
|
+
import { CustomBoundary } from "../app/pages/custom-boundary.js";
|
|
5
|
+
import { mergePlugins } from "./merge-plugins.js";
|
|
6
|
+
import { usePluginSource } from "./plugin-source.js";
|
|
7
|
+
import { usePluginLoads } from "./use-plugin-loads.js";
|
|
8
|
+
import { useCallback, useMemo } from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/plugins/before-sign-in.tsx
|
|
11
|
+
/**
|
|
12
|
+
* Loads the plugins the setup status names to everyone, from the app's plugin source, and draws their `beforeSignIn`
|
|
13
|
+
* components around `children` for someone signed out or a visitor, once they have loaded, or failed, or after a wait
|
|
14
|
+
* (see `usePluginLoads`). Signed in, it draws `children` alone, and the plugins stay loaded for the app to merge with
|
|
15
|
+
* the others `/meta` names. A `beforeSignIn` that throws leaves `children` as they are without it.
|
|
16
|
+
*/
|
|
17
|
+
const BeforeSignIn = ({ children }) => {
|
|
18
|
+
const { state, beforeSignIn, adoptSession } = useAuth();
|
|
19
|
+
const { path, params, navigate } = useRouter();
|
|
20
|
+
const { loads, settled } = usePluginLoads(beforeSignIn, usePluginSource());
|
|
21
|
+
const Around = useMemo(() => mergePlugins(void 0, loads).ui.beforeSignIn, [loads]);
|
|
22
|
+
const removeParams = useCallback((names) => {
|
|
23
|
+
const kept = new URLSearchParams(params);
|
|
24
|
+
for (const name of names) kept.delete(name);
|
|
25
|
+
navigate(kept.size > 0 ? `${path}?${kept}` : path, { replace: true });
|
|
26
|
+
}, [
|
|
27
|
+
path,
|
|
28
|
+
params,
|
|
29
|
+
navigate
|
|
30
|
+
]);
|
|
31
|
+
if (state.kind !== "signed-out" && state.kind !== "visitor") return children;
|
|
32
|
+
if (!settled) return /* @__PURE__ */ jsxs("div", {
|
|
33
|
+
className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
|
|
34
|
+
children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
|
|
35
|
+
});
|
|
36
|
+
if (!Around) return children;
|
|
37
|
+
return /* @__PURE__ */ jsx(CustomBoundary, {
|
|
38
|
+
name: "beforeSignIn",
|
|
39
|
+
fallback: children,
|
|
40
|
+
children: /* @__PURE__ */ jsx(Around, {
|
|
41
|
+
signedIn: adoptSession,
|
|
42
|
+
params,
|
|
43
|
+
removeParams,
|
|
44
|
+
children
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { BeforeSignIn };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Spinner } from "../primitives/spinner.js";
|
|
2
|
-
import { ProjectUiContext } from "../app/pages/project-ui.js";
|
|
3
|
-
import { useAdminMeta } from "../app/meta-gate.js";
|
|
4
|
-
import { browserLoader } from "./load-plugin.js";
|
|
5
2
|
import { mergePlugins } from "./merge-plugins.js";
|
|
3
|
+
import { usePluginSource } from "./plugin-source.js";
|
|
6
4
|
import { usePluginLoads } from "./use-plugin-loads.js";
|
|
5
|
+
import { ProjectUiContext } from "../app/pages/project-ui.js";
|
|
6
|
+
import { useAdminMeta } from "../app/meta-gate.js";
|
|
7
7
|
import { createContext, useContext, useMemo } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/plugins/plugin-gate.tsx
|
|
@@ -11,13 +11,13 @@ const PluginsOffContext = createContext([]);
|
|
|
11
11
|
/** The plugins `/meta` names that the app did not load, and why. */
|
|
12
12
|
const usePluginsOff = () => useContext(PluginsOffContext);
|
|
13
13
|
/**
|
|
14
|
-
* Loads the plugins `/meta` names and provides the project's UI config with theirs
|
|
15
|
-
* once they have loaded, or failed, or after a wait (see `usePluginLoads`); a plugin
|
|
16
|
-
* components draw what any missing component draws.
|
|
14
|
+
* Loads the plugins `/meta` names, from the app's plugin source, and provides the project's UI config with theirs
|
|
15
|
+
* merged in. It draws its children once they have loaded, or failed, or after a wait (see `usePluginLoads`); a plugin
|
|
16
|
+
* that fails is left out, and its components draw what any missing component draws.
|
|
17
17
|
*/
|
|
18
|
-
const PluginGate = ({ ui,
|
|
18
|
+
const PluginGate = ({ ui, children }) => {
|
|
19
19
|
const { plugins = [] } = useAdminMeta();
|
|
20
|
-
const { loads, settled } = usePluginLoads(plugins,
|
|
20
|
+
const { loads, settled } = usePluginLoads(plugins, usePluginSource());
|
|
21
21
|
const merged = useMemo(() => mergePlugins(ui, loads), [ui, loads]);
|
|
22
22
|
if (!settled) return /* @__PURE__ */ jsxs("div", {
|
|
23
23
|
className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { browserLoader, loadPlugin } from "./load-plugin.js";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/plugins/plugin-source.tsx
|
|
5
|
+
/** A source that loads with `loader` and keeps each plugin's load, so one named before and after sign-in loads once. */
|
|
6
|
+
const createPluginSource = (loader) => {
|
|
7
|
+
const loads = /* @__PURE__ */ new Map();
|
|
8
|
+
return { load: (ref) => {
|
|
9
|
+
const key = JSON.stringify(ref);
|
|
10
|
+
const known = loads.get(key);
|
|
11
|
+
if (known) return known;
|
|
12
|
+
const load = loadPlugin(ref, loader, window.location.href);
|
|
13
|
+
loads.set(key, load);
|
|
14
|
+
return load;
|
|
15
|
+
} };
|
|
16
|
+
};
|
|
17
|
+
const PluginSourceContext = createContext(null);
|
|
18
|
+
/** The app's plugin source, loading with `loader`: the page's `fetch` and `import()` by default. */
|
|
19
|
+
const PluginSourceProvider = ({ loader = browserLoader, children }) => {
|
|
20
|
+
const source = useMemo(() => createPluginSource(loader), [loader]);
|
|
21
|
+
return /* @__PURE__ */ jsx(PluginSourceContext.Provider, {
|
|
22
|
+
value: source,
|
|
23
|
+
children
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const usePluginSource = () => {
|
|
27
|
+
const source = useContext(PluginSourceContext);
|
|
28
|
+
if (!source) throw new Error("usePluginSource must be used inside PluginSourceProvider");
|
|
29
|
+
return source;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PluginSourceProvider, createPluginSource, usePluginSource };
|
|
@@ -19,8 +19,8 @@ const fieldWithin = (plugin, name, widget) => ({
|
|
|
19
19
|
});
|
|
20
20
|
/**
|
|
21
21
|
* A plugin's UI config from its module, read as a `protobase.ui.tsx` module and merged with the configs it extends,
|
|
22
|
-
* named after the plugin: its components, field types
|
|
23
|
-
* when the configs it merges define a name twice.
|
|
22
|
+
* named after the plugin: its components, field types, shell slots and `beforeSignIn` draw inside the plugin. Throws
|
|
23
|
+
* `UiConflictError` when the configs it merges define a name twice.
|
|
24
24
|
*/
|
|
25
25
|
const pluginUi = (plugin, module) => {
|
|
26
26
|
const ui = mergeUi({
|
|
@@ -33,12 +33,14 @@ const pluginUi = (plugin, module) => {
|
|
|
33
33
|
...ui.shell.actions && { actions: within(plugin, ui.shell.actions, "shell.actions") },
|
|
34
34
|
...ui.shell.rightPanel && { rightPanel: within(plugin, ui.shell.rightPanel, "shell.rightPanel") }
|
|
35
35
|
};
|
|
36
|
+
const beforeSignIn = ui.beforeSignIn && within(plugin, ui.beforeSignIn, "beforeSignIn");
|
|
36
37
|
return {
|
|
37
38
|
name: plugin.name,
|
|
38
39
|
components,
|
|
39
40
|
...ui.actions && { actions: ui.actions },
|
|
40
41
|
...fields && { fields },
|
|
41
|
-
...shell && { shell }
|
|
42
|
+
...shell && { shell },
|
|
43
|
+
...beforeSignIn && { beforeSignIn }
|
|
42
44
|
};
|
|
43
45
|
};
|
|
44
46
|
//#endregion
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadPlugin } from "./load-plugin.js";
|
|
2
1
|
import { useEffect, useState } from "react";
|
|
3
2
|
//#region src/plugins/use-plugin-loads.ts
|
|
4
3
|
/** How long the app waits for its plugins before it draws without the ones still loading, which arrive later. */
|
|
@@ -8,7 +7,7 @@ const none = [];
|
|
|
8
7
|
* The plugins `refs` names, in their order, as they load. `settled` once every one has loaded or failed, or after
|
|
9
8
|
* `pluginWaitMs`, so a slow plugin delays the app only that long. Without plugins it is settled at once.
|
|
10
9
|
*/
|
|
11
|
-
const usePluginLoads = (refs,
|
|
10
|
+
const usePluginLoads = (refs, source) => {
|
|
12
11
|
const key = JSON.stringify(refs);
|
|
13
12
|
const [state, setState] = useState({
|
|
14
13
|
key: "[]",
|
|
@@ -34,7 +33,7 @@ const usePluginLoads = (refs, loader) => {
|
|
|
34
33
|
publish();
|
|
35
34
|
}, pluginWaitMs);
|
|
36
35
|
refs.forEach((ref, index) => {
|
|
37
|
-
|
|
36
|
+
source.load(ref).then((load) => {
|
|
38
37
|
results[index] = load;
|
|
39
38
|
publish();
|
|
40
39
|
});
|
|
@@ -43,7 +42,7 @@ const usePluginLoads = (refs, loader) => {
|
|
|
43
42
|
active = false;
|
|
44
43
|
clearTimeout(timer);
|
|
45
44
|
};
|
|
46
|
-
}, [key,
|
|
45
|
+
}, [key, source]);
|
|
47
46
|
if (refs.length === 0) return {
|
|
48
47
|
loads: none,
|
|
49
48
|
settled: true
|