metaowl 0.4.1 → 0.6.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/CHANGELOG.md +50 -0
- package/README.md +267 -2
- package/build/runtime/bin/metaowl-build.js +10 -0
- package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
- package/build/runtime/bin/metaowl-dev.js +10 -0
- package/build/runtime/bin/metaowl-generate.js +231 -0
- package/build/runtime/bin/metaowl-lint.js +58 -0
- package/build/runtime/bin/utils.js +68 -0
- package/build/runtime/index.js +144 -0
- package/build/runtime/modules/app-mounter.js +73 -0
- package/build/runtime/modules/auto-import.js +140 -0
- package/build/runtime/modules/cache.js +49 -0
- package/build/runtime/modules/composables.js +353 -0
- package/build/runtime/modules/constants.js +38 -0
- package/build/runtime/modules/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +207 -0
- package/build/runtime/modules/fonts.js +172 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +180 -0
- package/build/runtime/modules/image.js +175 -0
- package/build/runtime/modules/layouts.js +214 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +265 -0
- package/build/runtime/modules/pwa.js +272 -0
- package/build/runtime/modules/router.js +384 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +198 -0
- package/build/runtime/modules/templates-manager.js +52 -0
- package/build/runtime/modules/test-utils.js +238 -0
- package/build/runtime/vite/plugin.js +197 -0
- package/eslint.js +29 -0
- package/package.json +45 -27
- package/CONTRIBUTING.md +0 -49
- package/bin/metaowl-build.js +0 -12
- package/bin/metaowl-dev.js +0 -12
- package/bin/metaowl-generate.js +0 -339
- package/bin/metaowl-lint.js +0 -71
- package/bin/utils.js +0 -82
- package/eslint.config.js +0 -3
- package/index.js +0 -328
- package/modules/app-mounter.js +0 -104
- package/modules/auto-import.js +0 -225
- package/modules/cache.js +0 -59
- package/modules/composables.js +0 -600
- package/modules/error-boundary.js +0 -228
- package/modules/fetch.js +0 -51
- package/modules/file-router.js +0 -478
- package/modules/forms.js +0 -353
- package/modules/i18n.js +0 -333
- package/modules/layouts.js +0 -431
- package/modules/link.js +0 -255
- package/modules/meta.js +0 -119
- package/modules/odoo-rpc.js +0 -511
- package/modules/pwa.js +0 -515
- package/modules/router.js +0 -769
- package/modules/seo.js +0 -501
- package/modules/store.js +0 -409
- package/modules/templates-manager.js +0 -89
- package/modules/test-utils.js +0 -532
- package/test/auto-import.test.js +0 -110
- package/test/cache.test.js +0 -55
- package/test/composables.test.js +0 -103
- package/test/dynamic-routes.test.js +0 -469
- package/test/error-boundary.test.js +0 -126
- package/test/fetch.test.js +0 -100
- package/test/file-router.test.js +0 -55
- package/test/forms.test.js +0 -203
- package/test/i18n.test.js +0 -188
- package/test/layouts.test.js +0 -395
- package/test/link.test.js +0 -189
- package/test/meta.test.js +0 -146
- package/test/odoo-rpc.test.js +0 -547
- package/test/pwa.test.js +0 -154
- package/test/router-guards.test.js +0 -229
- package/test/router.test.js +0 -77
- package/test/seo.test.js +0 -353
- package/test/store.test.js +0 -476
- package/test/templates-manager.test.js +0 -83
- package/test/test-utils.test.js +0 -314
- package/vite/plugin.js +0 -290
- package/vitest.config.js +0 -8
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Composables
|
|
3
|
+
*
|
|
4
|
+
* Reusable composables/hooks for MetaOwl OWL applications.
|
|
5
|
+
*/
|
|
6
|
+
import { onMounted, onWillUnmount, useState } from '@odoo/owl';
|
|
7
|
+
import Cache from './cache.js';
|
|
8
|
+
import Fetch from './fetch.js';
|
|
9
|
+
function createRef(initialValue) {
|
|
10
|
+
const resolvedValue = typeof initialValue === 'function'
|
|
11
|
+
? initialValue()
|
|
12
|
+
: initialValue;
|
|
13
|
+
const state = useState({
|
|
14
|
+
value: resolvedValue,
|
|
15
|
+
__value: resolvedValue
|
|
16
|
+
});
|
|
17
|
+
if (state.__value === undefined) {
|
|
18
|
+
state.__value = resolvedValue;
|
|
19
|
+
}
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
22
|
+
function getRefValue(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && 'value' in value
|
|
24
|
+
? value.value
|
|
25
|
+
: value;
|
|
26
|
+
}
|
|
27
|
+
export function useAuth() {
|
|
28
|
+
const user = createRef(null);
|
|
29
|
+
const isLoggedIn = createRef(false);
|
|
30
|
+
const isLoading = createRef(false);
|
|
31
|
+
let unsubscribe = null;
|
|
32
|
+
onMounted(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const { OdooService } = await import('./odoo-rpc.js');
|
|
35
|
+
isLoggedIn.value = OdooService.isAuthenticated();
|
|
36
|
+
user.value = OdooService.getSession();
|
|
37
|
+
unsubscribe = OdooService.onAuthChange((session) => {
|
|
38
|
+
user.value = session;
|
|
39
|
+
isLoggedIn.value = session !== null;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// OdooService not available, auth stays false
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
onWillUnmount(() => {
|
|
47
|
+
if (unsubscribe) {
|
|
48
|
+
unsubscribe();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const login = async (credentials) => {
|
|
52
|
+
isLoading.value = true;
|
|
53
|
+
try {
|
|
54
|
+
const { OdooService } = await import('./odoo-rpc.js');
|
|
55
|
+
await OdooService.authenticate(credentials?.username, credentials?.password);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
isLoading.value = false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const logout = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const { OdooService } = await import('./odoo-rpc.js');
|
|
68
|
+
OdooService.logout();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore errors
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const checkAuth = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const { OdooService } = await import('./odoo-rpc.js');
|
|
77
|
+
return OdooService.isAuthenticated();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
user,
|
|
85
|
+
isLoggedIn,
|
|
86
|
+
isLoading,
|
|
87
|
+
login,
|
|
88
|
+
logout,
|
|
89
|
+
checkAuth
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function useLocalStorage(key, defaultValue = null) {
|
|
93
|
+
const state = createRef(() => {
|
|
94
|
+
try {
|
|
95
|
+
const item = localStorage.getItem(key);
|
|
96
|
+
return item !== null ? JSON.parse(item) : defaultValue;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return defaultValue;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
Object.defineProperty(state, 'value', {
|
|
103
|
+
get() {
|
|
104
|
+
return state.__value;
|
|
105
|
+
},
|
|
106
|
+
set(newValue) {
|
|
107
|
+
state.__value = newValue;
|
|
108
|
+
try {
|
|
109
|
+
if (newValue === null) {
|
|
110
|
+
localStorage.removeItem(key);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
localStorage.setItem(key, JSON.stringify(newValue));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore storage errors
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const handleStorage = (event) => {
|
|
122
|
+
if (event.key === key) {
|
|
123
|
+
try {
|
|
124
|
+
state.__value = event.newValue !== null
|
|
125
|
+
? JSON.parse(event.newValue)
|
|
126
|
+
: defaultValue;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
state.__value = defaultValue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
onMounted(() => {
|
|
134
|
+
window.addEventListener('storage', handleStorage);
|
|
135
|
+
});
|
|
136
|
+
onWillUnmount(() => {
|
|
137
|
+
window.removeEventListener('storage', handleStorage);
|
|
138
|
+
});
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
export function useFetch(url, options = {}) {
|
|
142
|
+
const { initialData = null, immediate = true, transform = (data) => data, onError = null, method = 'GET', data: requestData = null, internal = true, triggerErrorHandler = true } = options;
|
|
143
|
+
const data = createRef(initialData);
|
|
144
|
+
const loading = createRef(false);
|
|
145
|
+
const error = createRef(null);
|
|
146
|
+
const execute = async (executeUrl = null) => {
|
|
147
|
+
const fetchUrl = executeUrl || getRefValue(url);
|
|
148
|
+
if (!fetchUrl)
|
|
149
|
+
return undefined;
|
|
150
|
+
loading.value = true;
|
|
151
|
+
error.value = null;
|
|
152
|
+
try {
|
|
153
|
+
const result = await Fetch.url(String(fetchUrl), method, requestData, internal, triggerErrorHandler);
|
|
154
|
+
if (result === null) {
|
|
155
|
+
throw new Error('Request failed');
|
|
156
|
+
}
|
|
157
|
+
data.value = transform(result);
|
|
158
|
+
return data.value;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
error.value = err;
|
|
162
|
+
if (onError) {
|
|
163
|
+
onError(err);
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
loading.value = false;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const refresh = () => execute();
|
|
172
|
+
onMounted(() => {
|
|
173
|
+
if (immediate) {
|
|
174
|
+
void execute();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
data,
|
|
179
|
+
loading,
|
|
180
|
+
error,
|
|
181
|
+
refresh,
|
|
182
|
+
execute
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function useDebounce(value, wait = 300) {
|
|
186
|
+
const debouncedValue = createRef(value.value);
|
|
187
|
+
let timeout = null;
|
|
188
|
+
Object.defineProperty(value, 'value', {
|
|
189
|
+
get() {
|
|
190
|
+
return value.__value;
|
|
191
|
+
},
|
|
192
|
+
set(newValue) {
|
|
193
|
+
value.__value = newValue;
|
|
194
|
+
if (timeout) {
|
|
195
|
+
clearTimeout(timeout);
|
|
196
|
+
}
|
|
197
|
+
timeout = setTimeout(() => {
|
|
198
|
+
debouncedValue.value = newValue;
|
|
199
|
+
}, wait);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
onWillUnmount(() => {
|
|
203
|
+
if (timeout) {
|
|
204
|
+
clearTimeout(timeout);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return debouncedValue;
|
|
208
|
+
}
|
|
209
|
+
export function useThrottle(fn, wait = 300) {
|
|
210
|
+
let lastCall = 0;
|
|
211
|
+
let timeout = null;
|
|
212
|
+
const throttled = (...args) => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const remaining = wait - (now - lastCall);
|
|
215
|
+
if (remaining <= 0) {
|
|
216
|
+
if (timeout) {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
timeout = null;
|
|
219
|
+
}
|
|
220
|
+
lastCall = now;
|
|
221
|
+
fn(...args);
|
|
222
|
+
}
|
|
223
|
+
else if (!timeout) {
|
|
224
|
+
timeout = setTimeout(() => {
|
|
225
|
+
lastCall = Date.now();
|
|
226
|
+
timeout = null;
|
|
227
|
+
fn(...args);
|
|
228
|
+
}, remaining);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
onWillUnmount(() => {
|
|
232
|
+
if (timeout) {
|
|
233
|
+
clearTimeout(timeout);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return throttled;
|
|
237
|
+
}
|
|
238
|
+
export function useWindowSize() {
|
|
239
|
+
const width = createRef(window.innerWidth);
|
|
240
|
+
const height = createRef(window.innerHeight);
|
|
241
|
+
const handleResize = () => {
|
|
242
|
+
width.value = window.innerWidth;
|
|
243
|
+
height.value = window.innerHeight;
|
|
244
|
+
};
|
|
245
|
+
onMounted(() => {
|
|
246
|
+
window.addEventListener('resize', handleResize);
|
|
247
|
+
});
|
|
248
|
+
onWillUnmount(() => {
|
|
249
|
+
window.removeEventListener('resize', handleResize);
|
|
250
|
+
});
|
|
251
|
+
return { width, height };
|
|
252
|
+
}
|
|
253
|
+
export function useOnlineStatus() {
|
|
254
|
+
const isOnline = createRef(navigator.onLine);
|
|
255
|
+
const handleOnline = () => {
|
|
256
|
+
isOnline.value = true;
|
|
257
|
+
};
|
|
258
|
+
const handleOffline = () => {
|
|
259
|
+
isOnline.value = false;
|
|
260
|
+
};
|
|
261
|
+
onMounted(() => {
|
|
262
|
+
window.addEventListener('online', handleOnline);
|
|
263
|
+
window.addEventListener('offline', handleOffline);
|
|
264
|
+
});
|
|
265
|
+
onWillUnmount(() => {
|
|
266
|
+
window.removeEventListener('online', handleOnline);
|
|
267
|
+
window.removeEventListener('offline', handleOffline);
|
|
268
|
+
});
|
|
269
|
+
return isOnline;
|
|
270
|
+
}
|
|
271
|
+
export function useAsyncState(asyncFn, options = {}) {
|
|
272
|
+
const { immediate = false, initialData = null } = options;
|
|
273
|
+
const state = createRef(null);
|
|
274
|
+
const data = createRef(initialData);
|
|
275
|
+
const error = createRef(null);
|
|
276
|
+
const execute = async (...args) => {
|
|
277
|
+
state.value = 'loading';
|
|
278
|
+
error.value = null;
|
|
279
|
+
try {
|
|
280
|
+
const result = await asyncFn(...args);
|
|
281
|
+
data.value = result;
|
|
282
|
+
state.value = 'success';
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
error.value = err;
|
|
287
|
+
state.value = 'error';
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
if (immediate) {
|
|
292
|
+
onMounted(() => {
|
|
293
|
+
void execute(...[]);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
state,
|
|
298
|
+
data,
|
|
299
|
+
error,
|
|
300
|
+
execute,
|
|
301
|
+
isLoading: () => state.value === 'loading',
|
|
302
|
+
isSuccess: () => state.value === 'success',
|
|
303
|
+
isError: () => state.value === 'error'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export function useCache(key, defaultValue = null) {
|
|
307
|
+
const value = createRef(defaultValue);
|
|
308
|
+
onMounted(() => {
|
|
309
|
+
void Cache.get(key)
|
|
310
|
+
.then((cached) => {
|
|
311
|
+
value.value = cached ?? defaultValue;
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {
|
|
314
|
+
value.value = defaultValue;
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
const set = (newValue) => {
|
|
318
|
+
value.value = newValue;
|
|
319
|
+
void Cache.set(key, newValue);
|
|
320
|
+
};
|
|
321
|
+
const get = async () => {
|
|
322
|
+
const cached = await Cache.get(key);
|
|
323
|
+
value.value = cached ?? defaultValue;
|
|
324
|
+
return cached;
|
|
325
|
+
};
|
|
326
|
+
const remove = () => {
|
|
327
|
+
value.value = defaultValue;
|
|
328
|
+
void Cache.remove(key);
|
|
329
|
+
};
|
|
330
|
+
const clear = () => {
|
|
331
|
+
value.value = defaultValue;
|
|
332
|
+
void Cache.clear();
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
value,
|
|
336
|
+
set,
|
|
337
|
+
get,
|
|
338
|
+
remove,
|
|
339
|
+
clear
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export const Composables = {
|
|
343
|
+
useAuth,
|
|
344
|
+
useLocalStorage,
|
|
345
|
+
useFetch,
|
|
346
|
+
useDebounce,
|
|
347
|
+
useThrottle,
|
|
348
|
+
useWindowSize,
|
|
349
|
+
useOnlineStatus,
|
|
350
|
+
useAsyncState,
|
|
351
|
+
useCache
|
|
352
|
+
};
|
|
353
|
+
export default Composables;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const MAGIC_STRINGS = {
|
|
2
|
+
STORE_SESSION_KEY: 'metaowl:odoo:session',
|
|
3
|
+
STORE_CSRF_KEY: 'metaowl:odoo:csrf',
|
|
4
|
+
MOUNT_ELEMENT_ID: 'metaowl',
|
|
5
|
+
LINK_TEMPLATE_NAME: 'Link'
|
|
6
|
+
};
|
|
7
|
+
export const ROUTE_PATTERN_CONFIG = {
|
|
8
|
+
optionalParam: /\/:([^/(]+)\?/g,
|
|
9
|
+
catchAll: /\/:([^/(]+)\(\.\*\)/g,
|
|
10
|
+
namedParam: /:([^/(?\s]+)/g,
|
|
11
|
+
wildcard: /\*/g,
|
|
12
|
+
separator: /\//g
|
|
13
|
+
};
|
|
14
|
+
export function buildRouteRegexPattern(path) {
|
|
15
|
+
const pattern = path
|
|
16
|
+
.replace(ROUTE_PATTERN_CONFIG.separator, '\\/')
|
|
17
|
+
.replace(ROUTE_PATTERN_CONFIG.catchAll, '/(.*)')
|
|
18
|
+
.replace(ROUTE_PATTERN_CONFIG.optionalParam, '(?:/([^/]+))?')
|
|
19
|
+
.replace(ROUTE_PATTERN_CONFIG.namedParam, '([^/]+)')
|
|
20
|
+
.replace(ROUTE_PATTERN_CONFIG.wildcard, '(.*)');
|
|
21
|
+
return '^' + pattern + '$';
|
|
22
|
+
}
|
|
23
|
+
export function buildSimpleRoutePattern(routePath) {
|
|
24
|
+
const pattern = routePath
|
|
25
|
+
.replace(ROUTE_PATTERN_CONFIG.separator, '\\/')
|
|
26
|
+
.replace(ROUTE_PATTERN_CONFIG.catchAll, '(.*)')
|
|
27
|
+
.replace(ROUTE_PATTERN_CONFIG.optionalParam, '(?:/([^/]+))?')
|
|
28
|
+
.replace(ROUTE_PATTERN_CONFIG.namedParam, '([^/]+)')
|
|
29
|
+
.replace(ROUTE_PATTERN_CONFIG.wildcard, '(.*)');
|
|
30
|
+
return '^' + pattern + '$';
|
|
31
|
+
}
|
|
32
|
+
export function normalizeRoutePath(path) {
|
|
33
|
+
return (path.replace(/\/$/, '') || '/');
|
|
34
|
+
}
|
|
35
|
+
export function normalizePathForComparison(path) {
|
|
36
|
+
return normalizeRoutePath(path);
|
|
37
|
+
}
|
|
38
|
+
export const EXTERNAL_URL_REGEX = /^(https?:|\/\/|mailto:|tel:|ftp:|file:|javascript:)/i;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ErrorBoundary
|
|
3
|
+
*
|
|
4
|
+
* Error boundaries for OWL applications.
|
|
5
|
+
*/
|
|
6
|
+
import { Component, useState, xml } from '@odoo/owl';
|
|
7
|
+
const globalErrorHandlers = [];
|
|
8
|
+
let errorContext = {};
|
|
9
|
+
export class ErrorBoundary extends Component {
|
|
10
|
+
static template = xml `
|
|
11
|
+
<t t-if="state.hasError">
|
|
12
|
+
<t t-component="props.Fallback || fallback"
|
|
13
|
+
t-props="{ error: state.error, errorInfo: state.errorInfo }"/>
|
|
14
|
+
</t>
|
|
15
|
+
<t t-else="">
|
|
16
|
+
<t t-slot="default"/>
|
|
17
|
+
</t>
|
|
18
|
+
`;
|
|
19
|
+
static defaultProps = {
|
|
20
|
+
Fallback: null
|
|
21
|
+
};
|
|
22
|
+
state;
|
|
23
|
+
setup() {
|
|
24
|
+
this.state = useState({
|
|
25
|
+
hasError: false,
|
|
26
|
+
error: null,
|
|
27
|
+
errorInfo: null
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
onError(error, errorInfo) {
|
|
31
|
+
this.state.hasError = true;
|
|
32
|
+
this.state.error = error;
|
|
33
|
+
this.state.errorInfo = errorInfo;
|
|
34
|
+
for (const handler of globalErrorHandlers) {
|
|
35
|
+
handler(error, { ...errorContext, ...errorInfo });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class DefaultErrorFallback extends Component {
|
|
40
|
+
static template = xml `
|
|
41
|
+
<div class="error-boundary-fallback">
|
|
42
|
+
<h2>Something went wrong</h2>
|
|
43
|
+
<t t-if="props.error">
|
|
44
|
+
<details>
|
|
45
|
+
<summary>Error details</summary>
|
|
46
|
+
<pre t-esc="props.error.stack || props.error.message || props.error"/>
|
|
47
|
+
</details>
|
|
48
|
+
</t>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
export function onError(handler) {
|
|
53
|
+
globalErrorHandlers.push(handler);
|
|
54
|
+
return () => {
|
|
55
|
+
const index = globalErrorHandlers.indexOf(handler);
|
|
56
|
+
if (index > -1) {
|
|
57
|
+
globalErrorHandlers.splice(index, 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function setErrorContext(context) {
|
|
62
|
+
errorContext = { ...errorContext, ...context };
|
|
63
|
+
}
|
|
64
|
+
export function getErrorContext() {
|
|
65
|
+
return { ...errorContext };
|
|
66
|
+
}
|
|
67
|
+
export function clearErrorContext() {
|
|
68
|
+
errorContext = {};
|
|
69
|
+
}
|
|
70
|
+
export function captureError(error, context = {}) {
|
|
71
|
+
const fullContext = { ...errorContext, ...context };
|
|
72
|
+
for (const handler of globalErrorHandlers) {
|
|
73
|
+
handler(error, fullContext);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function errorBoundary(options = {}) {
|
|
77
|
+
return function decorator(componentClass) {
|
|
78
|
+
componentClass.errorBoundary = true;
|
|
79
|
+
if (options.Fallback) {
|
|
80
|
+
componentClass.fallback = options.Fallback;
|
|
81
|
+
}
|
|
82
|
+
return componentClass;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function withErrorBoundary(componentClass, options = {}) {
|
|
86
|
+
return class WithErrorBoundary extends Component {
|
|
87
|
+
static template = xml `
|
|
88
|
+
<ErrorBoundary Fallback="props.Fallback || fallback">
|
|
89
|
+
<t t-component="Component" t-props="props"/>
|
|
90
|
+
</ErrorBoundary>
|
|
91
|
+
`;
|
|
92
|
+
static components = { ErrorBoundary };
|
|
93
|
+
Component;
|
|
94
|
+
fallback;
|
|
95
|
+
setup() {
|
|
96
|
+
this.Component = componentClass;
|
|
97
|
+
this.fallback = options.Fallback || DefaultErrorFallback;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function initGlobalErrorHandling() {
|
|
102
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
103
|
+
captureError(error || new Error(String(message)), {
|
|
104
|
+
type: 'window.onerror',
|
|
105
|
+
source,
|
|
106
|
+
lineno,
|
|
107
|
+
colno
|
|
108
|
+
});
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
window.onunhandledrejection = (event) => {
|
|
112
|
+
captureError(event.reason, {
|
|
113
|
+
type: 'unhandledrejection'
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Fetch
|
|
3
|
+
*
|
|
4
|
+
* A static class wrapping the Fetch API with a configurable base URL and
|
|
5
|
+
* error handling. All internal requests automatically prepend the configured
|
|
6
|
+
* baseUrl and return parsed JSON.
|
|
7
|
+
*/
|
|
8
|
+
export default class Fetch {
|
|
9
|
+
static _baseUrl = '';
|
|
10
|
+
static _onError = null;
|
|
11
|
+
static configure({ baseUrl = '', onError = null } = {}) {
|
|
12
|
+
Fetch._baseUrl = baseUrl;
|
|
13
|
+
Fetch._onError = onError;
|
|
14
|
+
}
|
|
15
|
+
static async url(url, method = 'GET', data = null, internal = true, triggerErrorHandler = true) {
|
|
16
|
+
const fullUrl = `${internal ? Fetch._baseUrl : ''}${url}`;
|
|
17
|
+
const response = await fetch(fullUrl, {
|
|
18
|
+
method,
|
|
19
|
+
body: data ? JSON.stringify(data) : null
|
|
20
|
+
}).catch((error) => {
|
|
21
|
+
console.warn('[metaowl] Fetch error:', error);
|
|
22
|
+
if (triggerErrorHandler && Fetch._onError) {
|
|
23
|
+
Fetch._onError(error);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
});
|
|
27
|
+
if (!response)
|
|
28
|
+
return null;
|
|
29
|
+
return await response.json();
|
|
30
|
+
}
|
|
31
|
+
}
|