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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -0
- 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 +141 -0
- package/build/runtime/modules/app-mounter.js +65 -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/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -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 +183 -0
- package/eslint.js +29 -0
- package/package.json +28 -10
- 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,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
|
+
}
|