@lowdefy/client 6.1.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 +6 -4
- 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 +72 -5
- 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 +234 -98
- package/package.json +11 -10
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
*/ // Page lifecycle events. Browser signals a page can react to, so apps do not
|
|
16
|
+
// have to poll to notice that a tab went away and came back, that the network
|
|
17
|
+
// dropped, or that the window was resized.
|
|
18
|
+
//
|
|
19
|
+
// - onVisible document became visible again, or the window regained focus.
|
|
20
|
+
// - onHidden document became hidden, or the window lost focus.
|
|
21
|
+
// - onOnline the browser reports the network is back.
|
|
22
|
+
// - onOffline the browser reports the network is gone.
|
|
23
|
+
// - onResize the window was resized.
|
|
24
|
+
//
|
|
25
|
+
// None of these fire on the initial page load - the active state is seeded from
|
|
26
|
+
// the document when the page mounts, and only transitions away from that seed
|
|
27
|
+
// trigger an event.
|
|
28
|
+
//
|
|
29
|
+
// The same resize listener keeps _media current: once a resize settles, the
|
|
30
|
+
// engine re-evaluates the blocks that read a media value that changed.
|
|
31
|
+
const VISIBILITY_DEBOUNCE_MS = 300;
|
|
32
|
+
const RESIZE_DEBOUNCE_MS = 150;
|
|
33
|
+
function createPageLifecycleManager() {
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
let removeListeners = [];
|
|
36
|
+
let timeouts = [];
|
|
37
|
+
function init(context) {
|
|
38
|
+
destroyed = false;
|
|
39
|
+
const globals = context?._internal?.lowdefy?._internal?.globals ?? {};
|
|
40
|
+
const win = globals.window;
|
|
41
|
+
const doc = globals.document;
|
|
42
|
+
// Server side render guard - there are no browser listeners to attach to.
|
|
43
|
+
if (typeof document === 'undefined' || !win || !doc) return;
|
|
44
|
+
if (typeof win.addEventListener !== 'function' || typeof doc.addEventListener !== 'function') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const events = context._internal.rootBlock?.events ?? {};
|
|
48
|
+
const configured = (name)=>!!events[name];
|
|
49
|
+
const trigger = (name, event)=>{
|
|
50
|
+
if (destroyed) return;
|
|
51
|
+
// Page lifecycle chains run like onMountAsync - non blocking, and a
|
|
52
|
+
// failure in the chain must not break the page.
|
|
53
|
+
try {
|
|
54
|
+
Promise.resolve(context._internal.triggerPageEvent({
|
|
55
|
+
name,
|
|
56
|
+
event
|
|
57
|
+
})).catch((error)=>{
|
|
58
|
+
context._internal.lowdefy._internal.handleError?.(error);
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
context._internal.lowdefy._internal.handleError?.(error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const setTimer = (fn, ms)=>{
|
|
65
|
+
const timeout = setTimeout(()=>{
|
|
66
|
+
timeouts = timeouts.filter((t)=>t !== timeout);
|
|
67
|
+
fn();
|
|
68
|
+
}, ms);
|
|
69
|
+
timeouts.push(timeout);
|
|
70
|
+
return timeout;
|
|
71
|
+
};
|
|
72
|
+
const listen = (target, name, handler)=>{
|
|
73
|
+
target.addEventListener(name, handler);
|
|
74
|
+
removeListeners.push(()=>target.removeEventListener(name, handler));
|
|
75
|
+
};
|
|
76
|
+
// --- onVisible / onHidden ------------------------------------------------
|
|
77
|
+
if (configured('onVisible') || configured('onHidden')) {
|
|
78
|
+
// Seeded from the document so the initial load never fires an event.
|
|
79
|
+
let active = doc.visibilityState !== 'hidden';
|
|
80
|
+
let pending = null;
|
|
81
|
+
const flush = ()=>{
|
|
82
|
+
const next = pending;
|
|
83
|
+
pending = null;
|
|
84
|
+
if (destroyed || !next) return;
|
|
85
|
+
// A focus that only restores a state we are already in is not an event.
|
|
86
|
+
if (next.active === active) return;
|
|
87
|
+
active = next.active;
|
|
88
|
+
trigger(next.active ? 'onVisible' : 'onHidden', {
|
|
89
|
+
visible: next.active,
|
|
90
|
+
reason: next.reason
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
// A tab coming back to the front fires visibilitychange and focus. Both
|
|
94
|
+
// are collected into one window so the event chain only runs once.
|
|
95
|
+
const signal = (nextActive, reason)=>{
|
|
96
|
+
if (destroyed) return;
|
|
97
|
+
if (pending) {
|
|
98
|
+
pending.active = nextActive;
|
|
99
|
+
// A visibilitychange is the more precise reason, so it wins the pair.
|
|
100
|
+
if (reason === 'visibility') pending.reason = 'visibility';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
pending = {
|
|
104
|
+
active: nextActive,
|
|
105
|
+
reason
|
|
106
|
+
};
|
|
107
|
+
setTimer(flush, VISIBILITY_DEBOUNCE_MS);
|
|
108
|
+
};
|
|
109
|
+
listen(doc, 'visibilitychange', ()=>{
|
|
110
|
+
signal(doc.visibilityState !== 'hidden', 'visibility');
|
|
111
|
+
});
|
|
112
|
+
listen(win, 'focus', ()=>signal(true, 'focus'));
|
|
113
|
+
listen(win, 'blur', ()=>signal(false, 'focus'));
|
|
114
|
+
}
|
|
115
|
+
// --- onOnline / onOffline ------------------------------------------------
|
|
116
|
+
if (configured('onOnline')) {
|
|
117
|
+
listen(win, 'online', ()=>trigger('onOnline', {
|
|
118
|
+
online: true
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
if (configured('onOffline')) {
|
|
122
|
+
listen(win, 'offline', ()=>trigger('onOffline', {
|
|
123
|
+
online: false
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
// --- onResize and _media -------------------------------------------------
|
|
127
|
+
// The operator registry holds the operators of every page loaded so far, so
|
|
128
|
+
// a page that can not reach _media attaches no listener for it.
|
|
129
|
+
const watchMedia = !!context._internal.lowdefy._internal.operators._media;
|
|
130
|
+
const onResize = configured('onResize');
|
|
131
|
+
if (watchMedia || onResize) {
|
|
132
|
+
let resizeTimeout = null;
|
|
133
|
+
// One trailing debounce for the whole page: a drag fires resize at frame
|
|
134
|
+
// rate, and this evaluates once when it settles. A requestAnimationFrame
|
|
135
|
+
// throttle would evaluate every frame of the drag instead.
|
|
136
|
+
const flush = ()=>{
|
|
137
|
+
resizeTimeout = null;
|
|
138
|
+
if (destroyed) return;
|
|
139
|
+
// Blocks first, so onResize actions see the page evaluated at the new size.
|
|
140
|
+
if (watchMedia) {
|
|
141
|
+
context._internal.updateMedia();
|
|
142
|
+
}
|
|
143
|
+
if (onResize) {
|
|
144
|
+
trigger('onResize', {
|
|
145
|
+
width: win.innerWidth,
|
|
146
|
+
height: win.innerHeight
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
listen(win, 'resize', ()=>{
|
|
151
|
+
if (resizeTimeout) {
|
|
152
|
+
clearTimeout(resizeTimeout);
|
|
153
|
+
timeouts = timeouts.filter((t)=>t !== resizeTimeout);
|
|
154
|
+
}
|
|
155
|
+
resizeTimeout = setTimer(flush, RESIZE_DEBOUNCE_MS);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function destroy() {
|
|
160
|
+
destroyed = true;
|
|
161
|
+
removeListeners.forEach((remove)=>remove());
|
|
162
|
+
removeListeners = [];
|
|
163
|
+
timeouts.forEach((timeout)=>clearTimeout(timeout));
|
|
164
|
+
timeouts = [];
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
init,
|
|
168
|
+
destroy
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export { RESIZE_DEBOUNCE_MS, VISIBILITY_DEBOUNCE_MS };
|
|
172
|
+
export default createPageLifecycleManager;
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
// Pencil -> "Pencil", ArrowLeftRight -> "Arrow left right", more-vertical ->
|
|
17
|
+
// "More vertical", lucide:Pencil -> "Pencil".
|
|
18
|
+
function formatIconTitle(name) {
|
|
19
|
+
if (!type.isString(name)) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
const iconName = name.slice(name.indexOf(':') + 1);
|
|
23
|
+
const words = iconName.replace(/-/g, ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
|
|
24
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
25
|
+
}
|
|
26
|
+
export default formatIconTitle;
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
// A language tag: a 2–3 letter language and optional subtags ("en", "en-US",
|
|
17
|
+
// "zh-Hant-TW"). Intl throws on anything else, such as "en_US".
|
|
18
|
+
const LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
|
|
19
|
+
// The app's active locale that useLocale publishes, for formatting HTML dates
|
|
20
|
+
// and numbers. Undefined (the browser's locale) when the app has no i18n or the
|
|
21
|
+
// code is not a language tag Intl accepts.
|
|
22
|
+
function getActiveLocale(window) {
|
|
23
|
+
const locale = window.__lowdefy_locale;
|
|
24
|
+
if (!type.isString(locale) || !LANGUAGE_TAG.test(locale)) return undefined;
|
|
25
|
+
return locale;
|
|
26
|
+
}
|
|
27
|
+
export default getActiveLocale;
|
|
@@ -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
|
+
*/ const contextKeys = new WeakMap();
|
|
16
|
+
let lastKey = 0;
|
|
17
|
+
// A React key per engine context object. A page context rebuilt in place - an
|
|
18
|
+
// organization switch, a dev config reset - is a new object under the same
|
|
19
|
+
// instance key, and keying the page tree by it remounts the tree onto the new
|
|
20
|
+
// context rather than reconciling mounted blocks against it, which React can
|
|
21
|
+
// silently drop.
|
|
22
|
+
function getContextKey(context) {
|
|
23
|
+
if (!contextKeys.has(context)) {
|
|
24
|
+
lastKey += 1;
|
|
25
|
+
contextKeys.set(context, lastKey);
|
|
26
|
+
}
|
|
27
|
+
return contextKeys.get(context);
|
|
28
|
+
}
|
|
29
|
+
export default getContextKey;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
*/ // Engine and DisplayMessage messages render through antd's App holders, above
|
|
16
|
+
// the page's ConfigProvider, so each message carries its status icon itself.
|
|
17
|
+
const messageIcons = {
|
|
18
|
+
error: {
|
|
19
|
+
name: 'error',
|
|
20
|
+
title: ''
|
|
21
|
+
},
|
|
22
|
+
info: {
|
|
23
|
+
name: 'info',
|
|
24
|
+
title: ''
|
|
25
|
+
},
|
|
26
|
+
loading: {
|
|
27
|
+
name: 'loading',
|
|
28
|
+
spin: true,
|
|
29
|
+
title: ''
|
|
30
|
+
},
|
|
31
|
+
success: {
|
|
32
|
+
name: 'success',
|
|
33
|
+
title: ''
|
|
34
|
+
},
|
|
35
|
+
warning: {
|
|
36
|
+
name: 'warning',
|
|
37
|
+
title: ''
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function getMessageIcon({ status }) {
|
|
41
|
+
return messageIcons[status];
|
|
42
|
+
}
|
|
43
|
+
export default getMessageIcon;
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { rolesHash, type } from '@lowdefy/helpers';
|
|
16
|
+
// True when the server answered the rendered caller with other roles than the
|
|
17
|
+
// page renders - a role was granted or taken away since the page loaded - so
|
|
18
|
+
// _user.roles and role-gated blocks would show access the person no longer
|
|
19
|
+
// holds, or hide access they now have. stamp is the response's parsed
|
|
20
|
+
// x-lowdefy-caller header, user the rendered caller. A stamp that names no
|
|
21
|
+
// roles (no header, or an anonymous caller) claims none.
|
|
22
|
+
function hasOtherRoles({ stamp, user }) {
|
|
23
|
+
if (type.isNone(user) || type.isNone(stamp?.roles)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return stamp.roles !== rolesHash({
|
|
27
|
+
user
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export default hasOtherRoles;
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,6 @@
|
|
|
15
15
|
*/ import Client from './Client.js';
|
|
16
16
|
export default Client;
|
|
17
17
|
export { default as getOrCreateAntdCssContainer } from './getOrCreateAntdCssContainer.js';
|
|
18
|
+
export { default as IconProvider } from './IconProvider.js';
|
|
18
19
|
export { default as useDarkMode } from './useDarkMode.js';
|
|
19
20
|
export { default as useLocale } from './useLocale.js';
|
|
@@ -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;
|