metaowl 0.4.1 → 0.5.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +28 -10
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/eslint.config.js +0 -3
  39. package/index.js +0 -328
  40. package/modules/app-mounter.js +0 -104
  41. package/modules/auto-import.js +0 -225
  42. package/modules/cache.js +0 -59
  43. package/modules/composables.js +0 -600
  44. package/modules/error-boundary.js +0 -228
  45. package/modules/fetch.js +0 -51
  46. package/modules/file-router.js +0 -478
  47. package/modules/forms.js +0 -353
  48. package/modules/i18n.js +0 -333
  49. package/modules/layouts.js +0 -431
  50. package/modules/link.js +0 -255
  51. package/modules/meta.js +0 -119
  52. package/modules/odoo-rpc.js +0 -511
  53. package/modules/pwa.js +0 -515
  54. package/modules/router.js +0 -769
  55. package/modules/seo.js +0 -501
  56. package/modules/store.js +0 -409
  57. package/modules/templates-manager.js +0 -89
  58. package/modules/test-utils.js +0 -532
  59. package/test/auto-import.test.js +0 -110
  60. package/test/cache.test.js +0 -55
  61. package/test/composables.test.js +0 -103
  62. package/test/dynamic-routes.test.js +0 -469
  63. package/test/error-boundary.test.js +0 -126
  64. package/test/fetch.test.js +0 -100
  65. package/test/file-router.test.js +0 -55
  66. package/test/forms.test.js +0 -203
  67. package/test/i18n.test.js +0 -188
  68. package/test/layouts.test.js +0 -395
  69. package/test/link.test.js +0 -189
  70. package/test/meta.test.js +0 -146
  71. package/test/odoo-rpc.test.js +0 -547
  72. package/test/pwa.test.js +0 -154
  73. package/test/router-guards.test.js +0 -229
  74. package/test/router.test.js +0 -77
  75. package/test/seo.test.js +0 -353
  76. package/test/store.test.js +0 -476
  77. package/test/templates-manager.test.js +0 -83
  78. package/test/test-utils.test.js +0 -314
  79. package/vite/plugin.js +0 -290
  80. package/vitest.config.js +0 -8
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @module AutoImport
3
+ *
4
+ * Automatic component importing for MetaOwl applications.
5
+ */
6
+ import { globSync } from 'glob';
7
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
8
+ let importMap = null;
9
+ export function generateComponentMap(componentsDir, pattern = '*.js') {
10
+ const map = new Map();
11
+ const globPattern = pattern.includes('/') ? pattern : `${componentsDir}/${pattern}`;
12
+ const files = globSync(globPattern);
13
+ for (const file of files) {
14
+ if (file.includes('.test.') || file.includes('.spec.'))
15
+ continue;
16
+ const name = getComponentName(file);
17
+ if (!name)
18
+ continue;
19
+ const importPath = `/@components/${relative(componentsDir, file).replace(/\\/g, '/')}`;
20
+ map.set(name, importPath);
21
+ }
22
+ return map;
23
+ }
24
+ function getComponentName(filePath) {
25
+ const ext = extname(filePath);
26
+ const base = basename(filePath, ext);
27
+ if (basename(filePath) === base + ext) {
28
+ return base;
29
+ }
30
+ const dir = relative(process.cwd(), filePath).split('/');
31
+ if (base === 'index' && dir.length > 1) {
32
+ return toPascalCase(dir[dir.length - 2] ?? '');
33
+ }
34
+ return toPascalCase(base);
35
+ }
36
+ function toPascalCase(str) {
37
+ return str
38
+ .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
39
+ .replace(/^[a-z]/, (char) => char.toUpperCase());
40
+ }
41
+ export async function scanComponents(componentsDir, options = {}) {
42
+ const { pattern: _pattern = '*.js' } = options;
43
+ const absoluteDir = resolve(componentsDir);
44
+ const fs = await import('node:fs/promises');
45
+ const { join } = await import('node:path');
46
+ const components = [];
47
+ async function scanDir(dir) {
48
+ try {
49
+ const entries = await fs.readdir(dir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ const fullPath = join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ await scanDir(fullPath);
54
+ }
55
+ else if (entry.isFile() && entry.name.endsWith('.js')) {
56
+ if (entry.name.includes('.test.') || entry.name.includes('.spec.'))
57
+ continue;
58
+ const name = getComponentName(fullPath);
59
+ if (name && !components.includes(name)) {
60
+ components.push(name);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ // Directory doesn't exist or can't be read
67
+ }
68
+ }
69
+ await scanDir(absoluteDir);
70
+ return components;
71
+ }
72
+ export async function generateComponentDts(components, outputPath) {
73
+ const { mkdirSync, writeFileSync } = await import('node:fs');
74
+ const dir = dirname(outputPath);
75
+ mkdirSync(dir, { recursive: true });
76
+ const declarations = components
77
+ .map((name) => ` ${name}: typeof import('./components/${name}/${name}.js').default`)
78
+ .join('\n');
79
+ const content = `// Auto-generated by metaowl - do not edit\ndeclare module '@metaowl/components' {\n${declarations}\n}\n`;
80
+ writeFileSync(outputPath, content, 'utf-8');
81
+ }
82
+ export function generateImports(componentMap) {
83
+ const lines = [];
84
+ for (const [name, path] of componentMap) {
85
+ lines.push(`import ${name} from '${path}'`);
86
+ }
87
+ return lines.join('\n');
88
+ }
89
+ export function generateComponentsObject(componentMap) {
90
+ const entries = Array.from(componentMap.keys())
91
+ .map((name) => ` ${name}`)
92
+ .join(',\n');
93
+ return `{\n${entries}\n}`;
94
+ }
95
+ export function createAutoImportPlugin(options = {}) {
96
+ const { enabled = false, componentsDir = 'src/components', pattern = '**/*.js' } = options;
97
+ if (!enabled) {
98
+ return null;
99
+ }
100
+ importMap = generateComponentMap(componentsDir, `${componentsDir}/${pattern}`);
101
+ return {
102
+ name: 'metaowl:auto-import',
103
+ enforce: 'pre',
104
+ config(config) {
105
+ config.resolve ||= {};
106
+ config.resolve.alias ||= {};
107
+ config.resolve.alias['/@components'] = resolve(process.cwd(), componentsDir);
108
+ },
109
+ transform(code, id) {
110
+ if (!id.includes('/pages/') || !/\.[jt]s$/.test(id)) {
111
+ return null;
112
+ }
113
+ if (!code.includes('/* auto-import */') && !code.includes('// auto-import')) {
114
+ return null;
115
+ }
116
+ if (!importMap) {
117
+ return null;
118
+ }
119
+ const imports = generateImports(importMap);
120
+ const componentsObj = generateComponentsObject(importMap);
121
+ let transformed = imports + '\n\n' + code;
122
+ if (transformed.includes('extends Component')) {
123
+ transformed = transformed.replace(/(class\s+\w+\s+extends\s+Component\s*\{)/, `$1\n static components = ${componentsObj}\n`);
124
+ }
125
+ return { code: transformed, map: null };
126
+ }
127
+ };
128
+ }
129
+ export function registerAutoImport(name, path) {
130
+ if (!importMap) {
131
+ importMap = new Map();
132
+ }
133
+ importMap.set(name, path);
134
+ }
135
+ export function getAutoImportMap() {
136
+ return importMap ? new Map(importMap) : null;
137
+ }
138
+ export function clearAutoImports() {
139
+ importMap = null;
140
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module Cache
3
+ *
4
+ * Async-style localStorage wrapper.
5
+ *
6
+ * Values are automatically JSON-serialised on write and deserialised on read.
7
+ * All methods return Promises so they are interchangeable with IndexedDB-based
8
+ * alternatives without changing call-sites.
9
+ */
10
+ export default class Cache {
11
+ /**
12
+ * Retrieve a value by key.
13
+ */
14
+ static async get(key) {
15
+ const rawValue = localStorage.getItem(key);
16
+ return rawValue === null ? null : JSON.parse(rawValue);
17
+ }
18
+ /**
19
+ * Store a value under the given key.
20
+ */
21
+ static async set(key, value) {
22
+ localStorage.setItem(key, JSON.stringify(value));
23
+ }
24
+ /**
25
+ * Remove a single entry.
26
+ */
27
+ static async remove(key) {
28
+ localStorage.removeItem(key);
29
+ }
30
+ /**
31
+ * Remove all entries from localStorage.
32
+ */
33
+ static async clear() {
34
+ localStorage.clear();
35
+ }
36
+ /**
37
+ * Return all keys currently stored in localStorage.
38
+ */
39
+ static async keys() {
40
+ const keys = [];
41
+ for (let index = 0; index < localStorage.length; index++) {
42
+ const key = localStorage.key(index);
43
+ if (key !== null) {
44
+ keys.push(key);
45
+ }
46
+ }
47
+ return keys;
48
+ }
49
+ }
@@ -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,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
+ }