@protobase/ui 0.40.6 → 0.40.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +7 -3
- package/dist/app/comments/comment-composer.js +2 -0
- package/dist/app/create/create-page.js +10 -2
- package/dist/app/messages.js +12 -0
- package/dist/app/new-version/messages.js +20 -0
- package/dist/app/new-version/new-version-button.js +27 -0
- package/dist/app/new-version/new-version-toast.js +43 -0
- package/dist/app/new-version/use-modal-open.js +19 -0
- package/dist/app/pages/blocks/modal-form.js +4 -0
- package/dist/app/record-page.js +1 -1
- package/dist/app/router.js +68 -26
- package/dist/data/api-provider.js +1 -0
- package/dist/data/meta-data.d.ts +9 -0
- package/dist/data/use-meta.d.ts +3 -2
- package/dist/data/use-meta.js +8 -4
- package/dist/data/version-freeze.js +17 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BpvlppLd.js +9 -0
- package/dist/shell/assets/{index-GTJA-4i3.js → index-B37wY6Go.js} +2 -2
- package/dist/shell/assets/{records-map-DVCcd765.js → records-map-DD0ZBUZe.js} +2 -2
- package/dist/shell/assets/{shared-protobase-ui-D1hRi4LJ.js → shared-protobase-ui-pUw1X1dc.js} +2 -2
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-BpvlppLd.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-GTJA-4i3.js.map → index-B37wY6Go.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-DVCcd765.js.map → records-map-DD0ZBUZe.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-D1hRi4LJ.js.map → shared-protobase-ui-pUw1X1dc.js.map} +1 -1
- package/dist/toasts/toast-provider.js +11 -2
- package/dist/toasts/toast.js +3 -2
- package/package.json +4 -4
- package/dist/shell/assets/app-Di5oi8xq.js +0 -9
- package/dist/shell-source-maps/assets/app-Di5oi8xq.js.map +0 -1
package/dist/app/app.js
CHANGED
|
@@ -52,6 +52,8 @@ import { PluginNotice } from "../plugins/plugin-notice.js";
|
|
|
52
52
|
import { RecordPage } from "./record-page.js";
|
|
53
53
|
import { SourceNotice } from "./source-notice.js";
|
|
54
54
|
import { useGlobalSearch } from "./search/use-global-search.js";
|
|
55
|
+
import { NewVersionButton } from "./new-version/new-version-button.js";
|
|
56
|
+
import { NewVersionToast } from "./new-version/new-version-toast.js";
|
|
55
57
|
import { RuntimeUpdateButton } from "./runtime/runtime-update.js";
|
|
56
58
|
import { StaffBanner } from "./staff-banner.js";
|
|
57
59
|
import { useNavRecent } from "./use-nav-recent.js";
|
|
@@ -140,6 +142,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
140
142
|
url: runtimeUrl,
|
|
141
143
|
client: runtime
|
|
142
144
|
});
|
|
145
|
+
const reloadButton = meta.versionChange && /* @__PURE__ */ jsx(NewVersionButton, {});
|
|
143
146
|
const role = useActiveRole(useCallback(() => reopenIn(basePath), [basePath]), Boolean(user));
|
|
144
147
|
const acting = user && actingUser(user, role.current);
|
|
145
148
|
const admin = acting ? isAdmin(acting) : false;
|
|
@@ -250,12 +253,13 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
250
253
|
groups: search.groups,
|
|
251
254
|
onSelect: open
|
|
252
255
|
} : void 0,
|
|
253
|
-
actions: (Actions || runtimeButton || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
256
|
+
actions: (Actions || reloadButton || runtimeButton || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
254
257
|
Actions && /* @__PURE__ */ jsx(CustomBoundary, {
|
|
255
258
|
name: "shell.actions",
|
|
256
259
|
fallback: null,
|
|
257
260
|
children: /* @__PURE__ */ jsx(Actions, {})
|
|
258
261
|
}),
|
|
262
|
+
reloadButton,
|
|
259
263
|
runtimeButton,
|
|
260
264
|
assistantButton
|
|
261
265
|
] }),
|
|
@@ -291,7 +295,7 @@ const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, a
|
|
|
291
295
|
sources,
|
|
292
296
|
token: state.kind === "visitor" ? void 0 : session.token,
|
|
293
297
|
onUnauthenticated: markSignedOut,
|
|
294
|
-
children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */
|
|
298
|
+
children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [/* @__PURE__ */ jsx(NewVersionToast, {}), /* @__PURE__ */ jsx(PluginGate, {
|
|
295
299
|
ui,
|
|
296
300
|
children: /* @__PURE__ */ jsx(Routes, {
|
|
297
301
|
workspace,
|
|
@@ -299,7 +303,7 @@ const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, a
|
|
|
299
303
|
assistant,
|
|
300
304
|
runtime
|
|
301
305
|
})
|
|
302
|
-
}) }) })
|
|
306
|
+
})] }) })
|
|
303
307
|
});
|
|
304
308
|
};
|
|
305
309
|
const PublicFormGate = ({ baseUrl, client, ui, children }) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { Button } from "../../primitives/button.js";
|
|
3
|
+
import { useNavigationGuard } from "../router.js";
|
|
3
4
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
5
|
import { refusalReason } from "../displays/board-columns.js";
|
|
5
6
|
import { Textarea } from "../../primitives/textarea.js";
|
|
@@ -29,6 +30,7 @@ const CommentComposer = ({ resource, recordKey }) => {
|
|
|
29
30
|
const listId = useId();
|
|
30
31
|
const toast = useToast();
|
|
31
32
|
const post = usePostComment(resource, recordKey);
|
|
33
|
+
useNavigationGuard(draft.trim() !== "" || post.isPending, "input-lost");
|
|
32
34
|
const mention = activeMention(draft, caret);
|
|
33
35
|
const open = focused && mention !== void 0 && mention.start !== closed;
|
|
34
36
|
const people = useMentionable(resource, recordKey, open ? mention.query : void 0);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { text } from "../messages.js";
|
|
2
|
+
import { useNavigationGuard } from "../router.js";
|
|
2
3
|
import { useClient } from "../../data/api-provider.js";
|
|
3
4
|
import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
4
5
|
import { humanize } from "../../live/naming.js";
|
|
@@ -50,6 +51,12 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
50
51
|
const [errors, setErrors] = useState({});
|
|
51
52
|
const [rejections, setRejections] = useState(0);
|
|
52
53
|
const formRef = useFocusFirstInvalid(rejections);
|
|
54
|
+
const [newRecord, setNewRecord] = useState();
|
|
55
|
+
const [touched, setTouched] = useState(false);
|
|
56
|
+
useNavigationGuard(!newRecord && (touched || feedback.phase === "saving"), "input-lost");
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (newRecord) go(newRecord, { replace: true });
|
|
59
|
+
}, [newRecord, go]);
|
|
53
60
|
useEffect(() => {
|
|
54
61
|
for (const [name, key] of Object.entries(values ?? {})) {
|
|
55
62
|
const target = meta.resources[model.fields[name]?.relation?.resource ?? ""];
|
|
@@ -74,6 +81,7 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
74
81
|
]);
|
|
75
82
|
const change = (name, value) => {
|
|
76
83
|
setErrors((current) => without(current, name));
|
|
84
|
+
setTouched(true);
|
|
77
85
|
setDraft((current) => ({
|
|
78
86
|
...current,
|
|
79
87
|
[name]: value
|
|
@@ -97,11 +105,11 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
97
105
|
"facets",
|
|
98
106
|
"series"
|
|
99
107
|
]) queryClient.invalidateQueries({ queryKey: [prefix, resource] });
|
|
100
|
-
|
|
108
|
+
setNewRecord({
|
|
101
109
|
kind: "record",
|
|
102
110
|
resource,
|
|
103
111
|
key: recordKey(model, created.record)
|
|
104
|
-
}
|
|
112
|
+
});
|
|
105
113
|
});
|
|
106
114
|
const formSections = sections.map((section) => ({
|
|
107
115
|
title: section.title,
|
package/dist/app/messages.js
CHANGED
|
@@ -47,8 +47,14 @@ const text = defineMessages({
|
|
|
47
47
|
loading: "Loading",
|
|
48
48
|
leaveTitle: "Leave without saving?",
|
|
49
49
|
leaveDescription: "You have unsaved changes on this page. They stay in this browser, and come back when you open the record again.",
|
|
50
|
+
leaveInputLost: "You have unsaved changes on this page. They are lost when you leave.",
|
|
50
51
|
stay: "Stay",
|
|
51
52
|
leave: "Leave",
|
|
53
|
+
reloadTitle: "Reload without saving?",
|
|
54
|
+
reloadDraftKept: "You have unsaved changes on this page. They stay in this browser, and come back after the page reloads.",
|
|
55
|
+
reloadInputLost: "You have unsaved changes on this page. They are lost when the page reloads.",
|
|
56
|
+
keepEditing: "Keep editing",
|
|
57
|
+
reload: "Reload",
|
|
52
58
|
show: (label) => `Show ${label}`,
|
|
53
59
|
hide: (label) => `Hide ${label}`,
|
|
54
60
|
couldNotShow: (label) => `Could not show the ${label}`,
|
|
@@ -110,8 +116,14 @@ const text = defineMessages({
|
|
|
110
116
|
loading: "Laden",
|
|
111
117
|
leaveTitle: "Weggaan zonder te bewaren?",
|
|
112
118
|
leaveDescription: "Je hebt niet-bewaarde wijzigingen op deze pagina. Ze blijven in deze browser, en komen terug als je het record weer opent.",
|
|
119
|
+
leaveInputLost: "Je hebt niet-bewaarde wijzigingen op deze pagina. Ze gaan verloren als je weggaat.",
|
|
113
120
|
stay: "Blijven",
|
|
114
121
|
leave: "Weggaan",
|
|
122
|
+
reloadTitle: "Opnieuw laden zonder op te slaan?",
|
|
123
|
+
reloadDraftKept: "Er zijn niet-opgeslagen wijzigingen op deze pagina. Ze blijven bewaard in deze browser en komen terug zodra de pagina opnieuw is geladen.",
|
|
124
|
+
reloadInputLost: "Er zijn niet-opgeslagen wijzigingen op deze pagina. Ze gaan verloren als de pagina opnieuw wordt geladen.",
|
|
125
|
+
keepEditing: "Verder bewerken",
|
|
126
|
+
reload: "Opnieuw laden",
|
|
115
127
|
show: (label) => `${label} tonen`,
|
|
116
128
|
hide: (label) => `${label} verbergen`,
|
|
117
129
|
couldNotShow: (label) => `Kon ${label} niet tonen`,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineMessages } from "../../locale/messages.js";
|
|
2
|
+
//#region src/app/new-version/messages.ts
|
|
3
|
+
const text = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
title: "This app has been updated",
|
|
6
|
+
description: "Reload the page to use the new version, when it suits you.",
|
|
7
|
+
reload: "Reload",
|
|
8
|
+
reloadPage: "Reload page",
|
|
9
|
+
reloadPageHint: "Reload page to use the new version"
|
|
10
|
+
},
|
|
11
|
+
nl: {
|
|
12
|
+
title: "Deze app is bijgewerkt",
|
|
13
|
+
description: "Laad de pagina opnieuw voor de nieuwe versie, wanneer het uitkomt.",
|
|
14
|
+
reload: "Opnieuw laden",
|
|
15
|
+
reloadPage: "Opnieuw laden",
|
|
16
|
+
reloadPageHint: "Opnieuw laden om de nieuwe versie te gebruiken"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { text };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button } from "../../primitives/button.js";
|
|
2
|
+
import { useRouter } from "../router.js";
|
|
3
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
4
|
+
import { text } from "./messages.js";
|
|
5
|
+
import { RotateCw } from "lucide-react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/app/new-version/new-version-button.tsx
|
|
8
|
+
/** The top bar's Reload page button, from the moment the page keeps its `/meta` for a new version until it reloads. */
|
|
9
|
+
const NewVersionButton = () => {
|
|
10
|
+
const changed = Boolean(useAdminMeta().versionChange);
|
|
11
|
+
const { reload } = useRouter();
|
|
12
|
+
if (!changed) return null;
|
|
13
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
14
|
+
size: "sm",
|
|
15
|
+
variant: "ghost",
|
|
16
|
+
"aria-label": text("reloadPageHint"),
|
|
17
|
+
title: text("reloadPageHint"),
|
|
18
|
+
onClick: reload,
|
|
19
|
+
className: "max-md:size-9 max-md:p-0",
|
|
20
|
+
children: [/* @__PURE__ */ jsx(RotateCw, { className: "size-3.5 text-primary" }), /* @__PURE__ */ jsx("span", {
|
|
21
|
+
className: "max-md:sr-only",
|
|
22
|
+
children: text("reloadPage")
|
|
23
|
+
})]
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { NewVersionButton };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useRouter } from "../router.js";
|
|
2
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
3
|
+
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
|
+
import { text } from "./messages.js";
|
|
5
|
+
import { useModalOpen } from "./use-modal-open.js";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
//#region src/app/new-version/new-version-toast.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Once the server runs another Protobase version than the page started with, a toast that says so and reloads the
|
|
10
|
+
* page, asking first while a guard holds unsaved changes. It stays until Reload or ×, and after × it does not come
|
|
11
|
+
* back; the top bar's `NewVersionButton` stays. It steps aside while a modal is open, whose buttons it could cover on a
|
|
12
|
+
* phone. The page keeps the `/meta` it has meanwhile (see `keepOnVersionChange`).
|
|
13
|
+
*/
|
|
14
|
+
const NewVersionToast = () => {
|
|
15
|
+
const changed = Boolean(useAdminMeta().versionChange);
|
|
16
|
+
const modalOpen = useModalOpen();
|
|
17
|
+
const [closed, setClosed] = useState(false);
|
|
18
|
+
const toast = useToast();
|
|
19
|
+
const { reload } = useRouter();
|
|
20
|
+
const shown = changed && !closed && !modalOpen;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!shown) return;
|
|
23
|
+
const id = toast.show({
|
|
24
|
+
state: "info",
|
|
25
|
+
sticky: true,
|
|
26
|
+
title: text("title"),
|
|
27
|
+
description: text("description"),
|
|
28
|
+
action: {
|
|
29
|
+
label: text("reload"),
|
|
30
|
+
onClick: reload
|
|
31
|
+
},
|
|
32
|
+
onClose: () => setClosed(true)
|
|
33
|
+
});
|
|
34
|
+
return () => toast.dismiss(id);
|
|
35
|
+
}, [
|
|
36
|
+
shown,
|
|
37
|
+
toast,
|
|
38
|
+
reload
|
|
39
|
+
]);
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { NewVersionToast };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
//#region src/app/new-version/use-modal-open.ts
|
|
3
|
+
const modalOpen = () => document.querySelector("[aria-modal=\"true\"]") !== null;
|
|
4
|
+
/** Whether a modal, a dialog or a sheet, is open anywhere on the page, following them as they open and close. */
|
|
5
|
+
const useModalOpen = () => {
|
|
6
|
+
const [open, setOpen] = useState(modalOpen);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const observer = new MutationObserver(() => setOpen(modalOpen()));
|
|
9
|
+
observer.observe(document.body, {
|
|
10
|
+
childList: true,
|
|
11
|
+
subtree: true
|
|
12
|
+
});
|
|
13
|
+
setOpen(modalOpen());
|
|
14
|
+
return () => observer.disconnect();
|
|
15
|
+
}, []);
|
|
16
|
+
return open;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { useModalOpen };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button } from "../../../primitives/button.js";
|
|
2
2
|
import { Dialog } from "../../../primitives/dialog.js";
|
|
3
|
+
import { useNavigationGuard } from "../../router.js";
|
|
3
4
|
import { useClient } from "../../../data/api-provider.js";
|
|
4
5
|
import { useAdminMeta, usePermissions } from "../../meta-gate.js";
|
|
5
6
|
import { text } from "../messages.js";
|
|
@@ -45,6 +46,8 @@ const ModalFormDialog = ({ model, record, mode, label, title, fields, values, on
|
|
|
45
46
|
const formRef = useFocusFirstInvalid(rejections);
|
|
46
47
|
const [failure, setFailure] = useState();
|
|
47
48
|
const [saving, setSaving] = useState(false);
|
|
49
|
+
const [touched, setTouched] = useState(false);
|
|
50
|
+
useNavigationGuard(touched || saving, "input-lost");
|
|
48
51
|
const submit = async () => {
|
|
49
52
|
setSaving(true);
|
|
50
53
|
setFailure(void 0);
|
|
@@ -113,6 +116,7 @@ const ModalFormDialog = ({ model, record, mode, label, title, fields, values, on
|
|
|
113
116
|
resource: model.name,
|
|
114
117
|
onChange: (next) => {
|
|
115
118
|
setErrors(({ [name]: _, ...rest }) => rest);
|
|
119
|
+
setTouched(true);
|
|
116
120
|
setDraft((current) => ({
|
|
117
121
|
...current,
|
|
118
122
|
[name]: next
|
package/dist/app/record-page.js
CHANGED
|
@@ -153,7 +153,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
153
153
|
const restore = useRestore(model);
|
|
154
154
|
const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
|
|
155
155
|
const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
|
|
156
|
-
useNavigationGuard(editor.dirty && shown === "form");
|
|
156
|
+
useNavigationGuard(editor.dirty && shown === "form" || editor.saving);
|
|
157
157
|
usePauseFocusRefresh(editor.dirty);
|
|
158
158
|
const openGroups = address?.kind === "record" ? address.open : void 0;
|
|
159
159
|
const atEdit = address?.kind === "record" && address.edit === true;
|
package/dist/app/router.js
CHANGED
|
@@ -34,13 +34,15 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
34
34
|
url: initialUrl ?? browserUrl(),
|
|
35
35
|
entry: memory ? newEntry() : historyEntry()
|
|
36
36
|
}));
|
|
37
|
-
const guards = useRef(/* @__PURE__ */ new
|
|
37
|
+
const guards = useRef(/* @__PURE__ */ new Map());
|
|
38
|
+
const losesInput = () => [...guards.current.values()].includes("input-lost");
|
|
38
39
|
const current = useRef(page);
|
|
39
40
|
current.current = page;
|
|
40
41
|
const position = useRef(memory ? 0 : historyIndex());
|
|
41
42
|
const undoing = useRef(false);
|
|
42
43
|
const proceeding = useRef(false);
|
|
43
44
|
const leaveAfterUndo = useRef(void 0);
|
|
45
|
+
const reloading = useRef(false);
|
|
44
46
|
const [pending, setPending] = useState();
|
|
45
47
|
useEffect(() => {
|
|
46
48
|
if (memory) return;
|
|
@@ -64,7 +66,8 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
64
66
|
window.history.go(-delta);
|
|
65
67
|
setPending({
|
|
66
68
|
kind: "history",
|
|
67
|
-
delta
|
|
69
|
+
delta,
|
|
70
|
+
inputLost: losesInput()
|
|
68
71
|
});
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
@@ -97,19 +100,32 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
97
100
|
if (pathOf(full) !== pathOf(current.current.url) && guards.current.size > 0) return setPending({
|
|
98
101
|
kind: "link",
|
|
99
102
|
full,
|
|
100
|
-
replace
|
|
103
|
+
replace,
|
|
104
|
+
inputLost: losesInput()
|
|
101
105
|
});
|
|
102
106
|
go(full, replace);
|
|
103
107
|
}, [basePath, go]);
|
|
104
|
-
const registerGuard = useCallback(() => {
|
|
108
|
+
const registerGuard = useCallback((kind = "draft-kept") => {
|
|
105
109
|
const token = Symbol("guard");
|
|
106
|
-
guards.current.
|
|
110
|
+
guards.current.set(token, kind);
|
|
107
111
|
return () => void guards.current.delete(token);
|
|
108
112
|
}, []);
|
|
113
|
+
const reload = useCallback(() => {
|
|
114
|
+
if (guards.current.size > 0) return setPending({
|
|
115
|
+
kind: "reload",
|
|
116
|
+
inputLost: losesInput()
|
|
117
|
+
});
|
|
118
|
+
window.location.reload();
|
|
119
|
+
}, []);
|
|
120
|
+
const reloadConfirmed = useCallback(() => reloading.current, []);
|
|
109
121
|
const leave = () => {
|
|
110
122
|
if (!pending) return;
|
|
111
123
|
setPending(void 0);
|
|
112
124
|
if (pending.kind === "link") return go(pending.full, pending.replace);
|
|
125
|
+
if (pending.kind === "reload") {
|
|
126
|
+
reloading.current = true;
|
|
127
|
+
return window.location.reload();
|
|
128
|
+
}
|
|
113
129
|
proceeding.current = true;
|
|
114
130
|
if (undoing.current) leaveAfterUndo.current = pending.delta;
|
|
115
131
|
else window.history.go(pending.delta);
|
|
@@ -122,29 +138,49 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
122
138
|
basePath,
|
|
123
139
|
entry: page.entry,
|
|
124
140
|
navigate,
|
|
125
|
-
registerGuard
|
|
141
|
+
registerGuard,
|
|
142
|
+
reload,
|
|
143
|
+
reloadConfirmed
|
|
126
144
|
};
|
|
127
145
|
}, [
|
|
128
146
|
page,
|
|
129
147
|
basePath,
|
|
130
148
|
navigate,
|
|
131
|
-
registerGuard
|
|
149
|
+
registerGuard,
|
|
150
|
+
reload,
|
|
151
|
+
reloadConfirmed
|
|
132
152
|
]);
|
|
133
153
|
return /* @__PURE__ */ jsxs(RouterContext.Provider, {
|
|
134
154
|
value,
|
|
135
|
-
children: [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
children:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
children: [
|
|
156
|
+
children,
|
|
157
|
+
pending?.kind === "reload" && /* @__PURE__ */ jsx(Dialog, {
|
|
158
|
+
title: text("reloadTitle"),
|
|
159
|
+
description: text(pending.inputLost ? "reloadInputLost" : "reloadDraftKept"),
|
|
160
|
+
onClose: () => setPending(void 0),
|
|
161
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
162
|
+
onClick: () => setPending(void 0),
|
|
163
|
+
children: text("keepEditing")
|
|
164
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
165
|
+
variant: "primary",
|
|
166
|
+
onClick: leave,
|
|
167
|
+
children: text("reload")
|
|
168
|
+
})] })
|
|
169
|
+
}),
|
|
170
|
+
pending && pending.kind !== "reload" && /* @__PURE__ */ jsx(Dialog, {
|
|
171
|
+
title: text("leaveTitle"),
|
|
172
|
+
description: text(pending.inputLost ? "leaveInputLost" : "leaveDescription"),
|
|
173
|
+
onClose: () => setPending(void 0),
|
|
174
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
175
|
+
onClick: () => setPending(void 0),
|
|
176
|
+
children: text("stay")
|
|
177
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
178
|
+
variant: "primary",
|
|
179
|
+
onClick: leave,
|
|
180
|
+
children: text("leave")
|
|
181
|
+
})] })
|
|
182
|
+
})
|
|
183
|
+
]
|
|
148
184
|
});
|
|
149
185
|
};
|
|
150
186
|
/** A link to a path below the base path, as `appPath` writes it, that the router follows without reloading the page. */
|
|
@@ -158,16 +194,22 @@ const Link = ({ to, replace, ...rest }) => {
|
|
|
158
194
|
...rest
|
|
159
195
|
});
|
|
160
196
|
};
|
|
161
|
-
/** While `active`, leaving the page asks first (in-app links and closing the tab). */
|
|
162
|
-
const useNavigationGuard = (active) => {
|
|
163
|
-
const { registerGuard } = useRouter();
|
|
164
|
-
useEffect(() => active ? registerGuard() : void 0, [
|
|
197
|
+
/** While `active`, leaving the page asks first (in-app links, a reload and closing the tab). */
|
|
198
|
+
const useNavigationGuard = (active, kind = "draft-kept") => {
|
|
199
|
+
const { registerGuard, reloadConfirmed } = useRouter();
|
|
200
|
+
useEffect(() => active ? registerGuard(kind) : void 0, [
|
|
201
|
+
active,
|
|
202
|
+
kind,
|
|
203
|
+
registerGuard
|
|
204
|
+
]);
|
|
165
205
|
useEffect(() => {
|
|
166
206
|
if (!active) return;
|
|
167
|
-
const warn = (event) =>
|
|
207
|
+
const warn = (event) => {
|
|
208
|
+
if (!reloadConfirmed()) event.preventDefault();
|
|
209
|
+
};
|
|
168
210
|
window.addEventListener("beforeunload", warn);
|
|
169
211
|
return () => window.removeEventListener("beforeunload", warn);
|
|
170
|
-
}, [active]);
|
|
212
|
+
}, [active, reloadConfirmed]);
|
|
171
213
|
};
|
|
172
214
|
//#endregion
|
|
173
215
|
export { Link, Router, useNavigationGuard, useRouter };
|
|
@@ -66,6 +66,7 @@ const ApiProvider = ({ baseUrl, client: given, sources, fetch, queryClient: give
|
|
|
66
66
|
const previous = metaVersion.current;
|
|
67
67
|
metaVersion.current = version;
|
|
68
68
|
if (previous === void 0 || previous === version) return;
|
|
69
|
+
if (queryClient.getQueryData(keys.meta)?.versionChange) return;
|
|
69
70
|
queryClient.invalidateQueries({ queryKey: keys.meta }, { cancelRefetch: false });
|
|
70
71
|
}
|
|
71
72
|
}), [
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export type MetaData = {
|
|
|
7
7
|
etag: string;
|
|
8
8
|
/** The app's name, which ends the tab titles. */
|
|
9
9
|
appName: string;
|
|
10
|
+
/** The Protobase version the server ran when this `/meta` was read. */
|
|
11
|
+
protobaseVersion?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Set once the server runs another Protobase version than the page started with: `to` is that version, when the server
|
|
14
|
+
* names one. The page keeps this `/meta` and stops reading it until it is reloaded (see `keepOnVersionChange`).
|
|
15
|
+
*/
|
|
16
|
+
versionChange?: {
|
|
17
|
+
to?: string;
|
|
18
|
+
};
|
|
10
19
|
/** The path of the app's own icon, when the project has one, which the sidebar's logo draws. */
|
|
11
20
|
appIcon?: string;
|
|
12
21
|
resources: Record<string, ResourceModel>;
|
package/dist/data/use-meta.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { UseQueryResult } from "@tanstack/react-query";
|
|
|
3
3
|
//#region src/data/use-meta.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The resource and view models. Refetches on window focus with `If-None-Match`, so an unchanged
|
|
6
|
-
* `/meta` costs a 304, and the cached object keeps its identity.
|
|
7
|
-
*
|
|
6
|
+
* `/meta` costs a 304, and the cached object keeps its identity. Once the server runs another Protobase version than
|
|
7
|
+
* the page started with, it keeps the `/meta` it has and stops reading it (see `keepOnVersionChange`). Below the app's
|
|
8
|
+
* `MetaGate`, `data` holds the resources of its sources too, as every page of the app reads them.
|
|
8
9
|
*/
|
|
9
10
|
export declare const useMeta: () => UseQueryResult<MetaData>;
|
|
10
11
|
//#endregion
|
package/dist/data/use-meta.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
3
|
import { MetaContext } from "./meta-context.js";
|
|
4
|
+
import { keepOnVersionChange } from "./version-freeze.js";
|
|
4
5
|
import { useContext, useMemo } from "react";
|
|
5
6
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
7
|
//#region src/data/use-meta.ts
|
|
7
8
|
/**
|
|
8
9
|
* The resource and view models. Refetches on window focus with `If-None-Match`, so an unchanged
|
|
9
|
-
* `/meta` costs a 304, and the cached object keeps its identity.
|
|
10
|
-
*
|
|
10
|
+
* `/meta` costs a 304, and the cached object keeps its identity. Once the server runs another Protobase version than
|
|
11
|
+
* the page started with, it keeps the `/meta` it has and stops reading it (see `keepOnVersionChange`). Below the app's
|
|
12
|
+
* `MetaGate`, `data` holds the resources of its sources too, as every page of the app reads them.
|
|
11
13
|
*/
|
|
12
14
|
const useMeta = () => {
|
|
13
15
|
const client = useClient();
|
|
@@ -17,6 +19,7 @@ const useMeta = () => {
|
|
|
17
19
|
queryKey: keys.meta,
|
|
18
20
|
staleTime: 0,
|
|
19
21
|
refetchOnWindowFocus: "always",
|
|
22
|
+
enabled: (current) => !current.state.data?.versionChange,
|
|
20
23
|
queryFn: async () => {
|
|
21
24
|
const previous = queryClient.getQueryData(keys.meta);
|
|
22
25
|
const result = await client.meta(previous?.etag);
|
|
@@ -24,9 +27,10 @@ const useMeta = () => {
|
|
|
24
27
|
if (!previous) throw new Error("The server answered 304 to a request without If-None-Match");
|
|
25
28
|
return previous;
|
|
26
29
|
}
|
|
27
|
-
return {
|
|
30
|
+
return keepOnVersionChange(previous, {
|
|
28
31
|
etag: result.etag,
|
|
29
32
|
appName: result.meta.appName,
|
|
33
|
+
...result.meta.protobaseVersion && { protobaseVersion: result.meta.protobaseVersion },
|
|
30
34
|
...result.meta.appIcon && { appIcon: result.meta.appIcon },
|
|
31
35
|
resources: Object.fromEntries(result.meta.resources.map((model) => [model.name, model])),
|
|
32
36
|
views: Object.fromEntries(result.meta.views.map((model) => [model.resource, model])),
|
|
@@ -46,7 +50,7 @@ const useMeta = () => {
|
|
|
46
50
|
source,
|
|
47
51
|
state: "loading"
|
|
48
52
|
})) }
|
|
49
|
-
};
|
|
53
|
+
});
|
|
50
54
|
}
|
|
51
55
|
});
|
|
52
56
|
return useMemo(() => merged && query.data ? {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/data/version-freeze.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `/meta` the page keeps after reading it again: `next`, unless the server now runs another Protobase version than
|
|
4
|
+
* the page started with. The page then keeps the `/meta` it has, marked with the version the server moved to, and
|
|
5
|
+
* `useMeta` stops reading it, so nothing on the page changes until it is reloaded.
|
|
6
|
+
*/
|
|
7
|
+
const keepOnVersionChange = (previous, next) => {
|
|
8
|
+
if (!previous) return next;
|
|
9
|
+
if (previous.versionChange) return previous;
|
|
10
|
+
if (next.protobaseVersion === previous.protobaseVersion) return next;
|
|
11
|
+
return {
|
|
12
|
+
...previous,
|
|
13
|
+
versionChange: { ...next.protobaseVersion && { to: next.protobaseVersion } }
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { keepOnVersionChange };
|
package/dist/package.js
CHANGED