@lowdefy/client 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Client.js +54 -17
- package/dist/Context.js +30 -12
- package/dist/HtmlOverlay.js +127 -0
- package/dist/IconProvider.js +24 -0
- package/dist/MountEvents.js +3 -3
- package/dist/adapters/createRouter.js +9 -3
- package/dist/adapters/url.js +1 -7
- package/dist/auth/createAuthMethods.js +1034 -41
- package/dist/block/Block.js +6 -3
- package/dist/block/CategorySwitch.js +6 -0
- package/dist/block/Container.js +3 -0
- package/dist/block/InputContainer.js +8 -2
- package/dist/block/List.js +3 -0
- package/dist/block/LoadingBlock.js +6 -12
- package/dist/block/LoadingContainer.js +6 -1
- package/dist/block/LoadingList.js +6 -1
- package/dist/block/loadingBlockMethods.js +26 -0
- package/dist/claimCallerReload.js +38 -0
- package/dist/createAntdIconConfig.js +91 -0
- package/dist/createCallAPI.js +6 -2
- package/dist/createCallRequest.js +6 -2
- package/dist/createHandleError.js +21 -11
- package/dist/createIcon.js +138 -36
- package/dist/createPageLifecycleManager.js +172 -0
- package/dist/formatIconTitle.js +26 -0
- package/dist/getActiveLocale.js +27 -0
- package/dist/getContextKey.js +29 -0
- package/dist/getMessageIcon.js +43 -0
- package/dist/hasOtherRoles.js +30 -0
- package/dist/index.js +1 -0
- package/dist/initLowdefyContext.js +103 -4
- package/dist/isOtherCaller.js +29 -0
- package/dist/recordCallerStamp.js +31 -0
- package/dist/renderIconNodes.js +46 -0
- package/dist/request.js +74 -6
- package/dist/setLowdefyUser.js +32 -0
- package/dist/setupLink.js +19 -13
- package/dist/shouldReloadForAuthError.js +39 -0
- package/dist/shouldReloadForBuild.js +44 -0
- package/dist/shouldReloadForCaller.js +50 -0
- package/dist/storage/clearUserStorage.js +36 -0
- package/dist/storage/clearUserStorageOnOwnerChange.js +41 -0
- package/dist/storage/createStorage.js +48 -0
- package/dist/storage/listUserStorageKeys.js +36 -0
- package/dist/storage/readStorageItem.js +31 -0
- package/dist/storage/userStorageKey.js +24 -0
- package/dist/storage/userStoragePrefix.js +25 -0
- package/dist/useDarkMode.js +17 -4
- package/dist/useLocale.js +18 -5
- package/dist/websocket/createWebSocketClient.js +239 -100
- package/package.json +11 -10
|
@@ -12,25 +12,52 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import
|
|
15
|
+
*/ import React from 'react';
|
|
16
|
+
import { registerHtmlEnhancements } from '@lowdefy/block-utils';
|
|
17
|
+
import { rememberPath, resolveTarget } from '@lowdefy/engine';
|
|
18
|
+
import { translate, type } from '@lowdefy/helpers';
|
|
16
19
|
import createCallAPI from './createCallAPI.js';
|
|
17
20
|
import createAuthMethods from './auth/createAuthMethods.js';
|
|
18
21
|
import createCallRequest from './createCallRequest.js';
|
|
19
22
|
import createWebSocketClient from './websocket/createWebSocketClient.js';
|
|
20
23
|
import createIcon from './createIcon.js';
|
|
21
24
|
import createShortcutBadge from './createShortcutBadge.js';
|
|
25
|
+
import createStorage from './storage/createStorage.js';
|
|
22
26
|
import createLinkComponent from './createLinkComponent.js';
|
|
23
27
|
import createHandleError from './createHandleError.js';
|
|
28
|
+
import getActiveLocale from './getActiveLocale.js';
|
|
24
29
|
import { createBrowserLogger } from '@lowdefy/logger/browser';
|
|
30
|
+
import setLowdefyUser from './setLowdefyUser.js';
|
|
25
31
|
import setupLink from './setupLink.js';
|
|
26
|
-
|
|
32
|
+
import { createUrl } from './adapters/url.js';
|
|
33
|
+
// loadAllIcons (production server only) loads every icon the app bundles into
|
|
34
|
+
// types.icons; a page loads only its own icons, so a name that arrives at
|
|
35
|
+
// runtime may need it. Dev and e2e bundle every icon.
|
|
36
|
+
//
|
|
37
|
+
// matchedPath and pathParams are the server's match of the shown page: the
|
|
38
|
+
// path it was matched on (no basePath, no leading "/") and the values it
|
|
39
|
+
// carries.
|
|
40
|
+
function initLowdefyContext({ auth, Components, config, loadAllIcons, lowdefy, matchedPath, pathParams, router, stage, types, window }) {
|
|
41
|
+
// The path memory lasts the session. A dev config reload clears initialised
|
|
42
|
+
// to rebuild the page contexts, but the paths this tab followed still name
|
|
43
|
+
// the same pages, and the page response it fetches next rewrites its own.
|
|
44
|
+
if (type.isUndefined(lowdefy.pathMemory)) {
|
|
45
|
+
lowdefy.pathMemory = new Map();
|
|
46
|
+
}
|
|
27
47
|
if (!lowdefy._internal?.initialised) {
|
|
28
48
|
lowdefy._internal = {
|
|
29
49
|
actions: types.actions,
|
|
30
50
|
blockComponents: types.blocks,
|
|
31
51
|
blockMetas: types.blockMetas ?? {},
|
|
52
|
+
// The last caller stamp a response carried (recordCallerStamp). It names
|
|
53
|
+
// the session, not the page context, so it outlives a dev config reload,
|
|
54
|
+
// and the dev server records the first page config's before this runs.
|
|
55
|
+
callerStamp: lowdefy._internal?.callerStamp ?? null,
|
|
32
56
|
components: {
|
|
33
|
-
Icon: createIcon(
|
|
57
|
+
Icon: createIcon({
|
|
58
|
+
icons: types.icons,
|
|
59
|
+
loadAllIcons
|
|
60
|
+
}),
|
|
34
61
|
ShortcutBadge: createShortcutBadge(lowdefy)
|
|
35
62
|
},
|
|
36
63
|
displayMessage: ({ content })=>{
|
|
@@ -54,12 +81,21 @@ function initLowdefyContext({ auth, Components, config, lowdefy, router, stage,
|
|
|
54
81
|
router
|
|
55
82
|
};
|
|
56
83
|
lowdefy.apiResponses = {};
|
|
84
|
+
// Rebuilt with the page contexts, so a dev config reload runs the app events again.
|
|
85
|
+
lowdefy.appContext = null;
|
|
57
86
|
lowdefy.basePath = router.basePath;
|
|
58
87
|
lowdefy.contexts = {};
|
|
59
88
|
lowdefy.inputs = {};
|
|
89
|
+
lowdefy.pageInstances = {};
|
|
60
90
|
lowdefy.lowdefyApp = config.rootConfig.lowdefyApp;
|
|
61
91
|
lowdefy.lowdefyGlobal = config.rootConfig.lowdefyGlobal;
|
|
62
92
|
lowdefy.theme = config.rootConfig.theme ?? {};
|
|
93
|
+
lowdefy._internal.storage = createStorage({
|
|
94
|
+
basePath: lowdefy.basePath,
|
|
95
|
+
getUserId: ()=>lowdefy.user?.id ?? null,
|
|
96
|
+
slug: lowdefy.lowdefyApp?.slug,
|
|
97
|
+
window
|
|
98
|
+
});
|
|
63
99
|
lowdefy._internal.callAPI = createCallAPI(lowdefy);
|
|
64
100
|
lowdefy._internal.auth = createAuthMethods(lowdefy, auth);
|
|
65
101
|
lowdefy._internal.callRequest = createCallRequest(lowdefy);
|
|
@@ -74,14 +110,77 @@ function initLowdefyContext({ auth, Components, config, lowdefy, router, stage,
|
|
|
74
110
|
lowdefy._internal.logger = createBrowserLogger();
|
|
75
111
|
lowdefy._internal.handleError = createHandleError(lowdefy);
|
|
76
112
|
lowdefy._internal.components.handleError = lowdefy._internal.handleError;
|
|
113
|
+
// HtmlComponent (block-utils) gives data-* attributes meaning in every
|
|
114
|
+
// sanitised HTML string. The overlay pulls in antd Tooltip and Popover, so
|
|
115
|
+
// it loads the first time HTML needs one. Links build hrefs with createUrl,
|
|
116
|
+
// the one place basePath is applied, and navigate like the Link action. A
|
|
117
|
+
// page link's pathname comes from resolveTarget, as the Link action's does.
|
|
118
|
+
registerHtmlEnhancements({
|
|
119
|
+
createHref: ({ pathname, query })=>createUrl({
|
|
120
|
+
basePath: lowdefy.basePath,
|
|
121
|
+
pathname,
|
|
122
|
+
query
|
|
123
|
+
}),
|
|
124
|
+
createPageHref: ({ pageId, pathParams, urlQuery })=>{
|
|
125
|
+
const target = resolveTarget({
|
|
126
|
+
lowdefy,
|
|
127
|
+
target: {
|
|
128
|
+
pageId,
|
|
129
|
+
pathParams,
|
|
130
|
+
urlQuery
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return createUrl({
|
|
134
|
+
basePath: lowdefy.basePath,
|
|
135
|
+
pathname: target.pathname,
|
|
136
|
+
query: target.query
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
getLocale: ()=>getActiveLocale(window),
|
|
140
|
+
// The overlay is lazy, so the app's Icon is bound in when it loads.
|
|
141
|
+
HtmlOverlay: /*#__PURE__*/ React.lazy(async ()=>{
|
|
142
|
+
const { default: HtmlOverlay } = await import('./HtmlOverlay.js');
|
|
143
|
+
const Icon = lowdefy._internal.components.Icon;
|
|
144
|
+
function HtmlOverlayWithIcon(props) {
|
|
145
|
+
return /*#__PURE__*/ React.createElement(HtmlOverlay, {
|
|
146
|
+
...props,
|
|
147
|
+
Icon: Icon
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
default: HtmlOverlayWithIcon
|
|
152
|
+
};
|
|
153
|
+
}),
|
|
154
|
+
Icon: lowdefy._internal.components.Icon,
|
|
155
|
+
icons: types.icons,
|
|
156
|
+
link: lowdefy._internal.link,
|
|
157
|
+
loadAllIcons,
|
|
158
|
+
translate: lowdefy._internal.translate
|
|
159
|
+
});
|
|
77
160
|
if (stage === 'dev' || stage === 'e2e') {
|
|
78
161
|
window.lowdefy = lowdefy;
|
|
79
162
|
}
|
|
80
163
|
}
|
|
81
164
|
lowdefy.home = config.rootConfig.home || {};
|
|
165
|
+
lowdefy.linkPaths = config.pageConfig.linkPaths;
|
|
82
166
|
lowdefy.menus = config.rootConfig.menus;
|
|
83
167
|
lowdefy.pageId = config.pageConfig.pageId;
|
|
84
|
-
lowdefy.
|
|
168
|
+
lowdefy.pagePaths = config.rootConfig.pagePaths;
|
|
169
|
+
setLowdefyUser({
|
|
170
|
+
lowdefy,
|
|
171
|
+
user: auth?.user
|
|
172
|
+
});
|
|
173
|
+
// Runs on every render, so it sees the person signed in on load and any later change.
|
|
174
|
+
lowdefy._internal.storage.clearUserStorageOnOwnerChange();
|
|
175
|
+
// The shown page's path joins the path memory before its context is built,
|
|
176
|
+
// so the first load's page is known before any plugin reads the memory.
|
|
177
|
+
rememberPath({
|
|
178
|
+
lowdefy,
|
|
179
|
+
path: matchedPath,
|
|
180
|
+
pageId: config.pageConfig.pageId,
|
|
181
|
+
pathParams,
|
|
182
|
+
pattern: config.pageConfig.path
|
|
183
|
+
});
|
|
85
184
|
return lowdefy;
|
|
86
185
|
}
|
|
87
186
|
export default initLowdefyContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { callerHash, type } from '@lowdefy/helpers';
|
|
16
|
+
// True when the server answered as another caller than the one this page
|
|
17
|
+
// renders: stamp is the response's parsed x-lowdefy-caller header (see the
|
|
18
|
+
// server's stampCaller and parseCallerStamp), user the rendered caller.
|
|
19
|
+
function isOtherCaller({ stamp, user }) {
|
|
20
|
+
// A response with no header was not answered by a route that resolved a
|
|
21
|
+
// caller (it failed before, or came from a proxy), so it claims no caller.
|
|
22
|
+
if (type.isNone(stamp)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return stamp.caller !== callerHash({
|
|
26
|
+
user
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export default isOtherCaller;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { parseCallerStamp, type } from '@lowdefy/helpers';
|
|
16
|
+
// Keeps the last caller stamp a response carried (see the server's stampCaller)
|
|
17
|
+
// on lowdefy._internal.callerStamp, as { caller, session, roles, memberships }:
|
|
18
|
+
// who the server last answered as, in full. A response with no header claims
|
|
19
|
+
// no caller and leaves the last stamp in place.
|
|
20
|
+
function recordCallerStamp({ lowdefy, serverCaller }) {
|
|
21
|
+
const stamp = parseCallerStamp(serverCaller);
|
|
22
|
+
if (type.isNone(stamp)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// The dev server fetches the first page config before the page context exists
|
|
26
|
+
// (initLowdefyContext), which carries this stamp over when it builds
|
|
27
|
+
// _internal.
|
|
28
|
+
lowdefy._internal = lowdefy._internal ?? {};
|
|
29
|
+
lowdefy._internal.callerStamp = stamp;
|
|
30
|
+
}
|
|
31
|
+
export default recordCallerStamp;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import React from 'react';
|
|
16
|
+
const shapeTags = new Set([
|
|
17
|
+
'circle',
|
|
18
|
+
'ellipse',
|
|
19
|
+
'line',
|
|
20
|
+
'path',
|
|
21
|
+
'polygon',
|
|
22
|
+
'polyline',
|
|
23
|
+
'rect'
|
|
24
|
+
]);
|
|
25
|
+
// lucide-react applies vector-effect only to the top-level nodes it renders
|
|
26
|
+
// itself; icon data goes in as children, so shapes at every depth get it here.
|
|
27
|
+
function renderIconNodes({ node, nonScalingStroke }) {
|
|
28
|
+
return node.map(([tag, attrs, children], index)=>{
|
|
29
|
+
const props = nonScalingStroke && shapeTags.has(tag) ? {
|
|
30
|
+
vectorEffect: 'non-scaling-stroke',
|
|
31
|
+
...attrs,
|
|
32
|
+
key: index
|
|
33
|
+
} : {
|
|
34
|
+
...attrs,
|
|
35
|
+
key: index
|
|
36
|
+
};
|
|
37
|
+
if (!children) {
|
|
38
|
+
return /*#__PURE__*/ React.createElement(tag, props);
|
|
39
|
+
}
|
|
40
|
+
return /*#__PURE__*/ React.createElement(tag, props, renderIconNodes({
|
|
41
|
+
node: children,
|
|
42
|
+
nonScalingStroke
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export default renderIconNodes;
|
package/dist/request.js
CHANGED
|
@@ -12,19 +12,87 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import {
|
|
16
|
-
|
|
15
|
+
*/ import { decodeServerError } from '@lowdefy/engine';
|
|
16
|
+
import { expectedErrorNames, lowdefyErrorTypes } from '@lowdefy/errors';
|
|
17
|
+
import { translate, type } from '@lowdefy/helpers';
|
|
18
|
+
import recordCallerStamp from './recordCallerStamp.js';
|
|
19
|
+
import shouldReloadForAuthError from './shouldReloadForAuthError.js';
|
|
20
|
+
import shouldReloadForBuild from './shouldReloadForBuild.js';
|
|
21
|
+
import shouldReloadForCaller from './shouldReloadForCaller.js';
|
|
22
|
+
// buildId is the build this bundle was made from. The production server
|
|
23
|
+
// refuses a call from another build with a 409 before running anything, since
|
|
24
|
+
// an old page's payload can be the wrong shape for the new build's request and
|
|
25
|
+
// still be written. The tab then reloads once onto the current build. The
|
|
26
|
+
// promise never settles: the page is unloading, and a rejection would run the
|
|
27
|
+
// action's catch actions and report the refusal as an error. Only the
|
|
28
|
+
// production client carries a build id; the server lets a call naming none
|
|
29
|
+
// through (dev and e2e pages, third-party webhooks).
|
|
30
|
+
//
|
|
31
|
+
// lowdefy.user is the caller the page renders. The tab also reloads when the
|
|
32
|
+
// server answered as another caller or with other roles, or refused the
|
|
33
|
+
// rendered caller in a way that means the page itself is no longer theirs
|
|
34
|
+
// (shouldReloadForCaller, shouldReloadForAuthError), so it never shows one
|
|
35
|
+
// caller's data to another. Otherwise the response's caller stamp is kept as
|
|
36
|
+
// the last one seen (recordCallerStamp).
|
|
37
|
+
// reloadOnAuthorizationError is set for page requests, which share the page's
|
|
38
|
+
// auth, and not for endpoints, which carry their own.
|
|
39
|
+
async function request({ buildId, url, method = 'GET', body, reloadOnAuthorizationError = false, lowdefy }) {
|
|
40
|
+
// Read per call: the rendered caller changes with the session.
|
|
41
|
+
const { user } = lowdefy;
|
|
42
|
+
const headers = {
|
|
43
|
+
'Content-Type': 'application/json'
|
|
44
|
+
};
|
|
45
|
+
if (!type.isNone(buildId)) {
|
|
46
|
+
headers['x-lowdefy-build'] = buildId;
|
|
47
|
+
}
|
|
17
48
|
const res = await fetch(url, {
|
|
18
49
|
method,
|
|
19
|
-
headers
|
|
20
|
-
'Content-Type': 'application/json'
|
|
21
|
-
},
|
|
50
|
+
headers,
|
|
22
51
|
body: JSON.stringify(body)
|
|
23
52
|
});
|
|
53
|
+
const serverCaller = res.headers.get('x-lowdefy-caller');
|
|
54
|
+
if (shouldReloadForCaller({
|
|
55
|
+
serverCaller,
|
|
56
|
+
user,
|
|
57
|
+
window
|
|
58
|
+
})) {
|
|
59
|
+
window.location.reload();
|
|
60
|
+
return new Promise(()=>{});
|
|
61
|
+
}
|
|
62
|
+
recordCallerStamp({
|
|
63
|
+
lowdefy,
|
|
64
|
+
serverCaller
|
|
65
|
+
});
|
|
24
66
|
if (!res.ok) {
|
|
25
67
|
const body = await res.json();
|
|
68
|
+
if (res.status === 409 && shouldReloadForBuild({
|
|
69
|
+
bundleBuildId: buildId,
|
|
70
|
+
serverBuildId: body?.buildId,
|
|
71
|
+
window
|
|
72
|
+
})) {
|
|
73
|
+
window.location.reload();
|
|
74
|
+
return new Promise(()=>{});
|
|
75
|
+
}
|
|
26
76
|
if (body?.['~e']) {
|
|
27
|
-
throw
|
|
77
|
+
throw decodeServerError(body);
|
|
78
|
+
}
|
|
79
|
+
// The server answers an expected outcome (an auth gate's 401 or 403, a
|
|
80
|
+
// UserError's 400) itself, as { name, message } with no serialized error.
|
|
81
|
+
// Rebuilding its class keeps the name the engine, handleError and the dev
|
|
82
|
+
// tools tell an expected outcome from a fault by.
|
|
83
|
+
if (expectedErrorNames.has(body?.name)) {
|
|
84
|
+
const ExpectedError = lowdefyErrorTypes[body.name];
|
|
85
|
+
const error = new ExpectedError(body.message);
|
|
86
|
+
if (shouldReloadForAuthError({
|
|
87
|
+
error,
|
|
88
|
+
reloadOnAuthorizationError,
|
|
89
|
+
user,
|
|
90
|
+
window
|
|
91
|
+
})) {
|
|
92
|
+
window.location.reload();
|
|
93
|
+
return new Promise(()=>{});
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
28
96
|
}
|
|
29
97
|
throw new Error(body.message || translate({
|
|
30
98
|
key: 'client.requestError'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
// Sets the caller the app renders. CallAPI results (lowdefy.apiResponses, read
|
|
17
|
+
// by _api) are app-level, so they are kept per organization as page contexts
|
|
18
|
+
// are (see the engine's getContext): when the caller's organization changes,
|
|
19
|
+
// the results fetched under the previous one are dropped, so none is read
|
|
20
|
+
// under another organization. Results fetched with no organization hold no
|
|
21
|
+
// organization's data and are kept when the caller gains one. A call still in
|
|
22
|
+
// flight writes into the dropped object. Author global state is left alone.
|
|
23
|
+
function setLowdefyUser({ lowdefy, user }) {
|
|
24
|
+
const organizationId = user?.organization_id ?? null;
|
|
25
|
+
const resultsOrganizationId = lowdefy._internal.apiResponsesOrganizationId;
|
|
26
|
+
if (!type.isNone(resultsOrganizationId) && resultsOrganizationId !== organizationId) {
|
|
27
|
+
lowdefy.apiResponses = {};
|
|
28
|
+
}
|
|
29
|
+
lowdefy._internal.apiResponsesOrganizationId = organizationId;
|
|
30
|
+
lowdefy.user = user ?? null;
|
|
31
|
+
}
|
|
32
|
+
export default setLowdefyUser;
|
package/dist/setupLink.js
CHANGED
|
@@ -13,12 +13,27 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { createLink } from '@lowdefy/engine';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
16
17
|
import { createUrl } from './adapters/url.js';
|
|
17
18
|
function setupLink(lowdefy) {
|
|
18
19
|
const { router } = lowdefy._internal;
|
|
19
20
|
const { window } = lowdefy._internal.globals;
|
|
20
21
|
const backLink = ()=>router.back();
|
|
21
22
|
const disabledLink = ()=>{};
|
|
23
|
+
// window.open returns null when the browser blocks the popup - tell the user
|
|
24
|
+
// rather than throwing a TypeError on the null handle.
|
|
25
|
+
const openNewTab = (target)=>{
|
|
26
|
+
const handle = window.open(target, '_blank');
|
|
27
|
+
if (!handle) {
|
|
28
|
+
lowdefy._internal.displayMessage({
|
|
29
|
+
content: lowdefy._internal.translate('client.popupBlocked'),
|
|
30
|
+
status: 'info',
|
|
31
|
+
duration: 10
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return handle.focus();
|
|
36
|
+
};
|
|
22
37
|
// `href` wins over `url`, and is used verbatim — the same precedence the anchor
|
|
23
38
|
// renderer applies (createLinkComponent.js). createLink only ever sets one of
|
|
24
39
|
// them, and it gives `url` a protocol before we see it while leaving `href`
|
|
@@ -29,16 +44,7 @@ function setupLink(lowdefy) {
|
|
|
29
44
|
const newOriginLink = ({ href, url, query, newTab })=>{
|
|
30
45
|
const target = href ?? `${url}${query ? `?${query}` : ''}`;
|
|
31
46
|
if (newTab) {
|
|
32
|
-
|
|
33
|
-
if (!handle) {
|
|
34
|
-
lowdefy._internal.displayMessage({
|
|
35
|
-
content: lowdefy._internal.translate('client.popupBlocked'),
|
|
36
|
-
status: 'info',
|
|
37
|
-
duration: 10
|
|
38
|
-
});
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
return handle.focus();
|
|
47
|
+
return openNewTab(target);
|
|
42
48
|
}
|
|
43
49
|
return window.location.assign(target);
|
|
44
50
|
};
|
|
@@ -47,11 +53,11 @@ function setupLink(lowdefy) {
|
|
|
47
53
|
// let a same-page Link reflect state into the url without moving the page.
|
|
48
54
|
const sameOriginLink = ({ newTab, pathname, query, replace, scroll, setInput })=>{
|
|
49
55
|
if (newTab) {
|
|
50
|
-
return
|
|
56
|
+
return openNewTab(`${window.location.origin}${createUrl({
|
|
51
57
|
basePath: lowdefy.basePath,
|
|
52
58
|
pathname,
|
|
53
59
|
query
|
|
54
|
-
})}
|
|
60
|
+
})}`);
|
|
55
61
|
}
|
|
56
62
|
setInput();
|
|
57
63
|
const navigate = replace ? router.replace : router.push;
|
|
@@ -62,7 +68,7 @@ function setupLink(lowdefy) {
|
|
|
62
68
|
});
|
|
63
69
|
};
|
|
64
70
|
const noLink = ()=>{
|
|
65
|
-
throw new
|
|
71
|
+
throw new ConfigError('Invalid Link: no target resolved.');
|
|
66
72
|
};
|
|
67
73
|
return createLink({
|
|
68
74
|
backLink,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import claimCallerReload from './claimCallerReload.js';
|
|
17
|
+
// True when an auth refusal means the page shows a caller the server no longer
|
|
18
|
+
// admits, so it must reload and let the server render the page again. A 401
|
|
19
|
+
// while a user is rendered means the session ended; with no user it is an
|
|
20
|
+
// ordinary refusal on a public page and is thrown, so it cannot loop. A
|
|
21
|
+
// request's 403 reloads too: page requests share their page's auth, so the
|
|
22
|
+
// caller lost the page. An endpoint's 403 does not, since endpoints carry their
|
|
23
|
+
// own roles and a refusal is an ordinary outcome there. A two-factor enrolment
|
|
24
|
+
// refusal never reloads. Matched by name: plugins bundle their own
|
|
25
|
+
// @lowdefy/errors copy.
|
|
26
|
+
function shouldReloadForAuthError({ error, reloadOnAuthorizationError, user, window }) {
|
|
27
|
+
if (error.name === 'AuthenticationError' && !type.isNone(user)) {
|
|
28
|
+
return claimCallerReload({
|
|
29
|
+
window
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (error.name === 'AuthorizationError' && reloadOnAuthorizationError === true) {
|
|
33
|
+
return claimCallerReload({
|
|
34
|
+
window
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export default shouldReloadForAuthError;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
// sessionStorage key holding the server build this tab last reloaded for.
|
|
17
|
+
const RELOADED_FOR_BUILD_KEY = 'lowdefy.reloadedForBuild';
|
|
18
|
+
// True when the server answered from a different build than the bundle this
|
|
19
|
+
// tab is running, so the tab must do a full load to pick up the current
|
|
20
|
+
// bundle. Without this a page config from the new build can reference _js
|
|
21
|
+
// functions the old bundle never shipped, and the old page's requests can
|
|
22
|
+
// name requests, endpoints or auth routes the new build renamed or removed.
|
|
23
|
+
//
|
|
24
|
+
// A tab reloads at most once per server build: if the freshly loaded bundle
|
|
25
|
+
// still disagrees with the server (a rolling deploy answering from mixed
|
|
26
|
+
// versions, a proxy caching old HTML), reloading again would loop forever.
|
|
27
|
+
// The old-config-on-old-bundle throw is the lesser failure in that case.
|
|
28
|
+
function shouldReloadForBuild({ bundleBuildId, serverBuildId, window }) {
|
|
29
|
+
if (type.isNone(serverBuildId) || serverBuildId === bundleBuildId) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if (window.sessionStorage.getItem(RELOADED_FOR_BUILD_KEY) === serverBuildId) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
window.sessionStorage.setItem(RELOADED_FOR_BUILD_KEY, serverBuildId);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Browsers with storage disabled throw on sessionStorage access. Without
|
|
39
|
+
// the once-per-build record the loop guard cannot work, so do not reload.
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
export default shouldReloadForBuild;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { AuthenticationError } from '@lowdefy/errors';
|
|
16
|
+
import { parseCallerStamp } from '@lowdefy/helpers';
|
|
17
|
+
import claimCallerReload from './claimCallerReload.js';
|
|
18
|
+
import hasOtherRoles from './hasOtherRoles.js';
|
|
19
|
+
import isOtherCaller from './isOtherCaller.js';
|
|
20
|
+
// True when the page must reload because the server answered as another caller
|
|
21
|
+
// than the page renders - another person signed in, or the organization was
|
|
22
|
+
// switched, in another tab - or answered the same caller with other roles, so
|
|
23
|
+
// the page never shows two callers' data or access the person no longer holds.
|
|
24
|
+
// serverCaller is the response's x-lowdefy-caller header. A changed session or
|
|
25
|
+
// membership list with the same person, organization and roles does not reload.
|
|
26
|
+
// A second difference within the reload interval throws instead of reloading
|
|
27
|
+
// again (see claimCallerReload).
|
|
28
|
+
function shouldReloadForCaller({ serverCaller, user, window }) {
|
|
29
|
+
const stamp = parseCallerStamp(serverCaller);
|
|
30
|
+
const otherCaller = isOtherCaller({
|
|
31
|
+
stamp,
|
|
32
|
+
user
|
|
33
|
+
});
|
|
34
|
+
if (!otherCaller && !hasOtherRoles({
|
|
35
|
+
stamp,
|
|
36
|
+
user
|
|
37
|
+
})) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (claimCallerReload({
|
|
41
|
+
window
|
|
42
|
+
})) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (otherCaller) {
|
|
46
|
+
throw new AuthenticationError('The server answered as a different user or organization than this page shows. Reload the page.');
|
|
47
|
+
}
|
|
48
|
+
throw new AuthenticationError('The server answered with different roles than this page shows. Reload the page.');
|
|
49
|
+
}
|
|
50
|
+
export default shouldReloadForCaller;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import listUserStorageKeys from './listUserStorageKeys.js';
|
|
17
|
+
// Removes every user key of this app not owned by keepUserId (all of them when keepUserId is
|
|
18
|
+
// null), and the session storage Lowdefy writes for the person browsing. Device preferences and
|
|
19
|
+
// the build reload markers stay.
|
|
20
|
+
function clearUserStorage({ basePath, keepUserId, slug, window }) {
|
|
21
|
+
try {
|
|
22
|
+
listUserStorageKeys({
|
|
23
|
+
basePath,
|
|
24
|
+
slug,
|
|
25
|
+
storage: window.localStorage
|
|
26
|
+
}).filter(({ userId })=>type.isNone(keepUserId) || userId !== keepUserId).forEach(({ key })=>window.localStorage.removeItem(key));
|
|
27
|
+
} catch {
|
|
28
|
+
// Storage the browser blocks holds nothing to clear.
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
window.sessionStorage.removeItem('lowdefy_scroll');
|
|
32
|
+
} catch {
|
|
33
|
+
// Storage the browser blocks holds nothing to clear.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default clearUserStorage;
|