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
@@ -1,600 +0,0 @@
1
- /**
2
- * @module Composables
3
- *
4
- * Reusable composables/hooks for MetaOwl OWL applications.
5
- *
6
- * This module provides a collection of commonly used patterns
7
- * that can be shared across components.
8
- *
9
- * Available composables:
10
- * - useAuth: Authentication state management
11
- * - useLocalStorage: Reactive localStorage access
12
- * - useFetch: Data fetching with loading states
13
- * - useDebounce: Debounced values
14
- * - useThrottle: Throttled function execution
15
- * - useWindowSize: Reactive window dimensions
16
- * - useOnlineStatus: Network connectivity detection
17
- * - useAsyncState: Async operation state management
18
- *
19
- * Usage:
20
- * import { useAuth, useLocalStorage, useFetch } from 'metaowl'
21
- *
22
- * class MyComponent extends Component {
23
- * setup() {
24
- * const { user, isLoggedIn, login, logout } = useAuth()
25
- * const theme = useLocalStorage('theme', 'light')
26
- * const { data, loading, error, refresh } = useFetch('/api/data')
27
- *
28
- * return { user, theme, data, loading, error }
29
- * }
30
- * }
31
- */
32
-
33
- import { useState, onMounted, onWillUnmount } from '@odoo/owl'
34
- import Cache from './cache.js'
35
- import Fetch from './fetch.js'
36
-
37
- /**
38
- * Authentication state composable.
39
- *
40
- * Integrates with the OdooService for authentication state.
41
- * Automatically syncs with auth changes.
42
- *
43
- * @returns {Object} Auth state and methods
44
- * @property {Ref<Object|null>} user - Current user info
45
- * @property {Ref<boolean>} isLoggedIn - Authentication status
46
- * @property {Ref<boolean>} isLoading - Loading state
47
- * @property {Function} login - Login method
48
- * @property {Function} logout - Logout method
49
- * @property {Function} checkAuth - Check current auth status
50
- *
51
- * @example
52
- * const { user, isLoggedIn, logout } = useAuth()
53
- *
54
- * // In template
55
- * <div t-if="isLoggedIn.value">
56
- * Welcome, <t t-esc="user.value?.name"/>
57
- * <button t-on-click="logout">Logout</button>
58
- * </div>
59
- */
60
- export function useAuth() {
61
- const user = useState(null)
62
- const isLoggedIn = useState(false)
63
- const isLoading = useState(false)
64
- let unsubscribe = null
65
-
66
- onMounted(async () => {
67
- // Try to import OdooService dynamically to avoid circular deps
68
- try {
69
- const { OdooService } = await import('./odoo-rpc.js')
70
-
71
- // Set initial state
72
- isLoggedIn.value = OdooService.isAuthenticated()
73
- user.value = OdooService.getSession()
74
-
75
- // Subscribe to auth changes
76
- unsubscribe = OdooService.onAuthChange((session) => {
77
- user.value = session
78
- isLoggedIn.value = session !== null
79
- })
80
- } catch {
81
- // OdooService not available, auth stays false
82
- }
83
- })
84
-
85
- onWillUnmount(() => {
86
- if (unsubscribe) {
87
- unsubscribe()
88
- }
89
- })
90
-
91
- const login = async (credentials) => {
92
- isLoading.value = true
93
- try {
94
- const { OdooService } = await import('./odoo-rpc.js')
95
- await OdooService.authenticate(credentials?.username, credentials?.password)
96
- return true
97
- } catch (error) {
98
- return false
99
- } finally {
100
- isLoading.value = false
101
- }
102
- }
103
-
104
- const logout = async () => {
105
- try {
106
- const { OdooService } = await import('./odoo-rpc.js')
107
- OdooService.logout()
108
- } catch {
109
- // Ignore errors
110
- }
111
- }
112
-
113
- const checkAuth = async () => {
114
- try {
115
- const { OdooService } = await import('./odoo-rpc.js')
116
- return OdooService.isAuthenticated()
117
- } catch {
118
- return false
119
- }
120
- }
121
-
122
- return {
123
- user,
124
- isLoggedIn,
125
- isLoading,
126
- login,
127
- logout,
128
- checkAuth
129
- }
130
- }
131
-
132
- /**
133
- * Reactive localStorage composable.
134
- *
135
- * Automatically syncs with localStorage and other components
136
- * using the same key.
137
- *
138
- * @param {string} key - localStorage key
139
- * @param {any} defaultValue - Default value if not found
140
- * @returns {Ref<any>} Reactive reference to stored value
141
- *
142
- * @example
143
- * const theme = useLocalStorage('theme', 'light')
144
- *
145
- * // Update value (automatically saves to localStorage)
146
- * theme.value = 'dark'
147
- *
148
- * // Access value
149
- * console.log(theme.value) // 'dark'
150
- */
151
- export function useLocalStorage(key, defaultValue = null) {
152
- const state = useState(() => {
153
- try {
154
- const item = localStorage.getItem(key)
155
- return item !== null ? JSON.parse(item) : defaultValue
156
- } catch {
157
- return defaultValue
158
- }
159
- })
160
-
161
- // Watch for changes and sync to localStorage
162
- const originalSet = state.__set
163
-
164
- // Override the setter to persist to localStorage
165
- Object.defineProperty(state, 'value', {
166
- get() {
167
- return state.__value
168
- },
169
- set(newValue) {
170
- state.__value = newValue
171
- try {
172
- if (newValue === null) {
173
- localStorage.removeItem(key)
174
- } else {
175
- localStorage.setItem(key, JSON.stringify(newValue))
176
- }
177
- } catch {
178
- // Ignore storage errors
179
- }
180
- }
181
- })
182
-
183
- // Listen for changes from other tabs/windows
184
- const handleStorage = (event) => {
185
- if (event.key === key) {
186
- try {
187
- state.__value = event.newValue !== null
188
- ? JSON.parse(event.newValue)
189
- : defaultValue
190
- } catch {
191
- state.__value = defaultValue
192
- }
193
- }
194
- }
195
-
196
- onMounted(() => {
197
- window.addEventListener('storage', handleStorage)
198
- })
199
-
200
- onWillUnmount(() => {
201
- window.removeEventListener('storage', handleStorage)
202
- })
203
-
204
- return state
205
- }
206
-
207
- /**
208
- * Data fetching composable.
209
- *
210
- * Provides reactive loading, error, and data states
211
- * for HTTP requests.
212
- *
213
- * @param {string|Ref<string>} url - URL to fetch
214
- * @param {Object} options - Fetch options
215
- * @param {Object} [options.initialData=null] - Initial data value
216
- * @param {boolean} [options.immediate=true] - Fetch immediately on mount
217
- * @param {Function} [options.transform] - Transform response data
218
- * @param {Function} [options.onError] - Error handler
219
- * @returns {Object} Fetch state and control methods
220
- * @property {Ref<any>} data - Fetched data
221
- * @property {Ref<boolean>} loading - Loading state
222
- * @property {Ref<Error|null>} error - Error state
223
- * @property {Function} refresh - Refetch data
224
- * @property {Function} execute - Execute fetch with optional new URL
225
- *
226
- * @example
227
- * const { data, loading, error, refresh } = useFetch('/api/users')
228
- *
229
- * // In template
230
- * <div t-if="loading.value">Loading...</div>
231
- * <div t-elif="error.value">Error: <t t-esc="error.value.message"/></div>
232
- * <div t-else="">
233
- * <div t-foreach="data.value" t-as="user" t-key="user.id">
234
- * <t t-esc="user.name"/>
235
- * </div>
236
- * </div>
237
- */
238
- export function useFetch(url, options = {}) {
239
- const {
240
- initialData = null,
241
- immediate = true,
242
- transform = (data) => data,
243
- onError = null,
244
- ...fetchOptions
245
- } = options
246
-
247
- const data = useState(initialData)
248
- const loading = useState(false)
249
- const error = useState(null)
250
-
251
- const execute = async (executeUrl = null) => {
252
- const fetchUrl = executeUrl || (typeof url === 'object' ? url.value : url)
253
-
254
- if (!fetchUrl) return
255
-
256
- loading.value = true
257
- error.value = null
258
-
259
- try {
260
- const result = await Fetch.url(fetchUrl, fetchOptions)
261
-
262
- if (result === null) {
263
- throw new Error('Request failed')
264
- }
265
-
266
- data.value = transform(result)
267
- return data.value
268
- } catch (err) {
269
- error.value = err
270
- if (onError) {
271
- onError(err)
272
- }
273
- throw err
274
- } finally {
275
- loading.value = false
276
- }
277
- }
278
-
279
- const refresh = () => execute()
280
-
281
- onMounted(() => {
282
- if (immediate) {
283
- execute()
284
- }
285
- })
286
-
287
- // Watch for URL changes if url is a reactive reference
288
- if (typeof url === 'object' && url.__owl__?.reactivity) {
289
- // In a real implementation, we'd use a watch effect here
290
- // For now, manual refresh is required
291
- }
292
-
293
- return {
294
- data,
295
- loading,
296
- error,
297
- refresh,
298
- execute
299
- }
300
- }
301
-
302
- /**
303
- * Debounce a reactive value.
304
- *
305
- * Delays updating the value until after wait milliseconds
306
- * have elapsed since the last update.
307
- *
308
- * @param {Ref<any>} value - Reactive value to debounce
309
- * @param {number} [wait=300] - Debounce wait time in ms
310
- * @returns {Ref<any>} Debounced value
311
- *
312
- * @example
313
- * const searchQuery = useState('')
314
- * const debouncedQuery = useDebounce(searchQuery, 500)
315
- *
316
- * // debouncedQuery updates only 500ms after searchQuery stops changing
317
- */
318
- export function useDebounce(value, wait = 300) {
319
- const debouncedValue = useState(value.value)
320
- let timeout = null
321
-
322
- // Create a proxy to watch for changes
323
- Object.defineProperty(value, 'value', {
324
- get() {
325
- return value.__value
326
- },
327
- set(newValue) {
328
- value.__value = newValue
329
- clearTimeout(timeout)
330
- timeout = setTimeout(() => {
331
- debouncedValue.value = newValue
332
- }, wait)
333
- }
334
- })
335
-
336
- onWillUnmount(() => {
337
- clearTimeout(timeout)
338
- })
339
-
340
- return debouncedValue
341
- }
342
-
343
- /**
344
- * Throttle a function.
345
- *
346
- * Ensures the function is called at most once per wait period.
347
- *
348
- * @param {Function} fn - Function to throttle
349
- * @param {number} [wait=300] - Throttle wait time in ms
350
- * @returns {Function} Throttled function
351
- *
352
- * @example
353
- * const throttledSearch = useThrottle((query) => {
354
- * performSearch(query)
355
- * }, 500)
356
- *
357
- * // Can call multiple times, but executes at most once per 500ms
358
- */
359
- export function useThrottle(fn, wait = 300) {
360
- let lastCall = 0
361
- let timeout = null
362
-
363
- const throttled = (...args) => {
364
- const now = Date.now()
365
- const remaining = wait - (now - lastCall)
366
-
367
- if (remaining <= 0) {
368
- clearTimeout(timeout)
369
- lastCall = now
370
- fn(...args)
371
- } else if (!timeout) {
372
- timeout = setTimeout(() => {
373
- lastCall = Date.now()
374
- timeout = null
375
- fn(...args)
376
- }, remaining)
377
- }
378
- }
379
-
380
- onWillUnmount(() => {
381
- clearTimeout(timeout)
382
- })
383
-
384
- return throttled
385
- }
386
-
387
- /**
388
- * Track window size reactively.
389
- *
390
- * @returns {Object} Window dimensions
391
- * @property {Ref<number>} width - Window inner width
392
- * @property {Ref<number>} height - Window inner height
393
- *
394
- * @example
395
- * const { width, height } = useWindowSize()
396
- *
397
- * // Reactive to window resizing
398
- * const isMobile = () => width.value < 768
399
- */
400
- export function useWindowSize() {
401
- const width = useState(window.innerWidth)
402
- const height = useState(window.innerHeight)
403
-
404
- const handleResize = () => {
405
- width.value = window.innerWidth
406
- height.value = window.innerHeight
407
- }
408
-
409
- onMounted(() => {
410
- window.addEventListener('resize', handleResize)
411
- })
412
-
413
- onWillUnmount(() => {
414
- window.removeEventListener('resize', handleResize)
415
- })
416
-
417
- return { width, height }
418
- }
419
-
420
- /**
421
- * Track online/offline status.
422
- *
423
- * @returns {Ref<boolean>} True if online
424
- *
425
- * @example
426
- * const isOnline = useOnlineStatus()
427
- *
428
- * <div t-if="!isOnline.value" class="offline-banner">
429
- * You are offline
430
- * </div>
431
- */
432
- export function useOnlineStatus() {
433
- const isOnline = useState(navigator.onLine)
434
-
435
- const handleOnline = () => {
436
- isOnline.value = true
437
- }
438
-
439
- const handleOffline = () => {
440
- isOnline.value = false
441
- }
442
-
443
- onMounted(() => {
444
- window.addEventListener('online', handleOnline)
445
- window.addEventListener('offline', handleOffline)
446
- })
447
-
448
- onWillUnmount(() => {
449
- window.removeEventListener('online', handleOnline)
450
- window.removeEventListener('offline', handleOffline)
451
- })
452
-
453
- return isOnline
454
- }
455
-
456
- /**
457
- * Manage async operation state.
458
- *
459
- * Tracks loading, error, and data states for any async function.
460
- *
461
- * @param {Function} asyncFn - Async function to execute
462
- * @param {Object} options - Options
463
- * @param {boolean} [options.immediate=false] - Execute immediately
464
- * @param {any} [options.initialData=null] - Initial data value
465
- * @returns {Object} Async state
466
- * @property {Ref<any>} state - Current state (null, 'loading', 'success', 'error')
467
- * @property {Ref<any>} data - Result data
468
- * @property {Ref<Error|null>} error - Error if any
469
- * @property {Function} execute - Execute the async function
470
- * @property {boolean} isLoading - True if loading
471
- * @property {boolean} isSuccess - True if succeeded
472
- * @property {boolean} isError - True if errored
473
- *
474
- * @example
475
- * const fetchUser = async (id) => {
476
- * return await api.getUser(id)
477
- * }
478
- *
479
- * const { state, data, execute, isLoading } = useAsyncState(fetchUser)
480
- *
481
- * // Execute
482
- * await execute(123)
483
- *
484
- * // Check state
485
- * if (isLoading.value) console.log('Loading...')
486
- * if (isSuccess.value) console.log('User:', data.value)
487
- */
488
- export function useAsyncState(asyncFn, options = {}) {
489
- const { immediate = false, initialData = null } = options
490
-
491
- const state = useState(null) // null, 'loading', 'success', 'error'
492
- const data = useState(initialData)
493
- const error = useState(null)
494
-
495
- const execute = async (...args) => {
496
- state.value = 'loading'
497
- error.value = null
498
-
499
- try {
500
- const result = await asyncFn(...args)
501
- data.value = result
502
- state.value = 'success'
503
- return result
504
- } catch (err) {
505
- error.value = err
506
- state.value = 'error'
507
- throw err
508
- }
509
- }
510
-
511
- if (immediate) {
512
- onMounted(() => {
513
- execute()
514
- })
515
- }
516
-
517
- return {
518
- state,
519
- data,
520
- error,
521
- execute,
522
- isLoading: () => state.value === 'loading',
523
- isSuccess: () => state.value === 'success',
524
- isError: () => state.value === 'error'
525
- }
526
- }
527
-
528
- /**
529
- * Reactive cache composable.
530
- *
531
- * Uses the MetaOwl Cache module with reactive updates.
532
- *
533
- * @param {string} key - Cache key
534
- * @param {any} defaultValue - Default value
535
- * @returns {Object} Cache operations
536
- * @property {Ref<any>} value - Cached value
537
- * @property {Function} set - Set cache value
538
- * @property {Function} get - Get cache value
539
- * @property {Function} remove - Remove from cache
540
- * @property {Function} clear - Clear entire cache
541
- *
542
- * @example
543
- * const { value, set } = useCache('user-preferences', {})
544
- *
545
- * set({ theme: 'dark' })
546
- * console.log(value.value) // { theme: 'dark' }
547
- */
548
- export function useCache(key, defaultValue = null) {
549
- const value = useState(() => {
550
- try {
551
- return Cache.get(key) || defaultValue
552
- } catch {
553
- return defaultValue
554
- }
555
- })
556
-
557
- const set = (newValue) => {
558
- value.value = newValue
559
- Cache.set(key, newValue)
560
- }
561
-
562
- const get = () => {
563
- const cached = Cache.get(key)
564
- value.value = cached
565
- return cached
566
- }
567
-
568
- const remove = () => {
569
- value.value = defaultValue
570
- Cache.remove(key)
571
- }
572
-
573
- const clear = () => {
574
- value.value = defaultValue
575
- Cache.clear()
576
- }
577
-
578
- return {
579
- value,
580
- set,
581
- get,
582
- remove,
583
- clear
584
- }
585
- }
586
-
587
- // Export all composables as a namespace
588
- export const Composables = {
589
- useAuth,
590
- useLocalStorage,
591
- useFetch,
592
- useDebounce,
593
- useThrottle,
594
- useWindowSize,
595
- useOnlineStatus,
596
- useAsyncState,
597
- useCache
598
- }
599
-
600
- export default Composables