metaowl 0.4.0 → 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 (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  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 +29 -11
  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/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. 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
+ }