metaowl 0.1.2 → 0.2.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/README.md +853 -10
- package/bin/metaowl-create.js +431 -9
- package/index.js +155 -1
- package/modules/app-mounter.js +7 -0
- package/modules/auto-import.js +225 -0
- package/modules/cache.js +2 -0
- package/modules/composables.js +600 -0
- package/modules/error-boundary.js +228 -0
- package/modules/fetch.js +7 -0
- package/modules/file-router.js +425 -19
- package/modules/forms.js +353 -0
- package/modules/i18n.js +333 -0
- package/modules/layouts.js +433 -0
- package/modules/odoo-rpc.js +511 -0
- package/modules/pwa.js +515 -0
- package/modules/router.js +593 -29
- package/modules/seo.js +501 -0
- package/modules/store.js +409 -0
- package/modules/templates-manager.js +5 -0
- package/modules/test-utils.js +532 -0
- package/package.json +1 -1
- package/test/auto-import.test.js +110 -0
- package/test/composables.test.js +103 -0
- package/test/dynamic-routes.test.js +520 -0
- package/test/error-boundary.test.js +126 -0
- package/test/forms.test.js +203 -0
- package/test/i18n.test.js +188 -0
- package/test/layouts.test.js +395 -0
- package/test/odoo-rpc.test.js +547 -0
- package/test/pwa.test.js +154 -0
- package/test/router-guards.test.js +617 -0
- package/test/seo.test.js +353 -0
- package/test/store.test.js +476 -0
- package/test/test-utils.test.js +314 -0
- package/vite/plugin.js +43 -5
package/README.md
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
# metaowl
|
|
2
2
|
|
|
3
|
-
> A
|
|
3
|
+
> A comprehensive meta-framework for [Odoo OWL](https://github.com/odoo/owl), built on top of [Vite](https://vitejs.dev).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/metaowl)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](https://github.com/dennisschott/metaowl/issues)
|
|
9
9
|
|
|
10
|
-
metaowl
|
|
10
|
+
metaowl is a complete solution for building production-ready OWL applications with everything you need out of the box:
|
|
11
|
+
|
|
12
|
+
**Core Infrastructure:** File-based routing with dynamic routes, layout system, navigation guards, Pinia-inspired state management, and zero-config app mounting.
|
|
13
|
+
|
|
14
|
+
**Odoo Integration:** Full JSON-RPC client with authentication, session management, and CRUD operations.
|
|
15
|
+
|
|
16
|
+
**Developer Experience:** Composables for common patterns (auth, localStorage, fetching), form handling with validation, error boundaries, and internationalization.
|
|
17
|
+
|
|
18
|
+
**SEO & PWA:** Sitemap/robots.txt generation, structured data support, service worker integration, web app manifest, and push notifications.
|
|
19
|
+
|
|
20
|
+
**Testing & Quality:** Mock stores, router mocking, component testing utilities, plus bundled ESLint and PostCSS configs.
|
|
21
|
+
|
|
22
|
+
All powered by a batteries-included Vite plugin that handles the build pipeline, so you can focus on building components instead of wiring infrastructure.
|
|
11
23
|
|
|
12
24
|
---
|
|
13
25
|
|
|
@@ -19,6 +31,16 @@ metaowl gives you everything you need to ship production-ready OWL applications
|
|
|
19
31
|
- [Create a New Project](#create-a-new-project)
|
|
20
32
|
- [Manual Setup](#manual-setup)
|
|
21
33
|
- [File-based Routing](#file-based-routing)
|
|
34
|
+
- [Dynamic Routes](#dynamic-routes)
|
|
35
|
+
- [Layouts](#layouts)
|
|
36
|
+
- [Navigation Guards](#navigation-guards)
|
|
37
|
+
- [State Management](#state-management-store)
|
|
38
|
+
- [Error Boundaries](#error-boundaries)
|
|
39
|
+
- [i18n / Internationalization](#i18n--internationalization)
|
|
40
|
+
- [Form Handling](#form-handling)
|
|
41
|
+
- [Auto-Import](#auto-import)
|
|
42
|
+
- [Odoo JSON-RPC Service](#odoo-json-rpc-service)
|
|
43
|
+
- [Composables / Hooks](#composables--hooks)
|
|
22
44
|
- [CLI Reference](#cli-reference)
|
|
23
45
|
- [API Reference](#api-reference)
|
|
24
46
|
- [boot](#bootroutes)
|
|
@@ -27,6 +49,14 @@ metaowl gives you everything you need to ship production-ready OWL applications
|
|
|
27
49
|
- [Meta](#meta)
|
|
28
50
|
- [configureOwl](#configureowlconfig)
|
|
29
51
|
- [buildRoutes](#buildroutesmodules)
|
|
52
|
+
- [Store](#store)
|
|
53
|
+
- [Layouts API](#layouts-api)
|
|
54
|
+
- [Router Guards](#router-guards-api)
|
|
55
|
+
- [Error Boundary](#error-boundary-api)
|
|
56
|
+
- [i18n](#i18n-api)
|
|
57
|
+
- [Forms](#forms-api)
|
|
58
|
+
- [OdooService](#odooservice-api)
|
|
59
|
+
- [Composables](#composables-api)
|
|
30
60
|
- [Vite Plugin](#vite-plugin)
|
|
31
61
|
- [metaowlPlugin](#metaowlpluginoptions)
|
|
32
62
|
- [metaowlConfig](#metaowlconfigoptions)
|
|
@@ -41,10 +71,23 @@ metaowl gives you everything you need to ship production-ready OWL applications
|
|
|
41
71
|
## Features
|
|
42
72
|
|
|
43
73
|
- **File-based routing** — mirrors Nuxt/Next.js conventions out of the box
|
|
74
|
+
- **Dynamic routes** — support for parameters `[id]`, optional params `[id]?`, and catch-all `[...path]`
|
|
75
|
+
- **Layouts** — share page structures across routes with automatic layout resolution
|
|
76
|
+
- **Navigation guards** — route middleware for authentication, authorization, and redirects
|
|
77
|
+
- **State management** — Pinia-like store system with mutations, actions, and getters
|
|
44
78
|
- **App mounting** — zero-config OWL component mounting with template merging
|
|
45
79
|
- **Fetch helper** — thin wrapper around the Fetch API with a configurable base URL and error handler
|
|
46
80
|
- **Cache** — async-style `localStorage` wrapper (`get`, `set`, `remove`, `clear`, `keys`)
|
|
47
81
|
- **Meta tags** — programmatic control over `<title>`, Open Graph, Twitter Card, canonical, and more
|
|
82
|
+
- **Error boundaries** — global error handling with context tracking and error pages
|
|
83
|
+
- **i18n** — internationalization with pluralization and interpolation support
|
|
84
|
+
- **Form handling** — schema validation with async support via `useForm()`
|
|
85
|
+
- **Auto-import** — automatic component registration with TypeScript declarations
|
|
86
|
+
- **Odoo RPC Service** — full JSON-RPC client with authentication and CRUD operations
|
|
87
|
+
- **Composables** — reusable hooks for auth, localStorage, fetching, and more
|
|
88
|
+
- **Testing Utilities** — mock store, router mocking, component mount helpers
|
|
89
|
+
- **SEO Utils** — sitemap, robots.txt, JSON-LD, Open Graph, Twitter Cards
|
|
90
|
+
- **PWA Support** — service worker, manifest generation, push notifications
|
|
48
91
|
- **SSG generator** — statically pre-renders HTML pages with correct meta tags at build time
|
|
49
92
|
- **Vite plugin** — handles `COMPONENTS` injection, XML template copying, CSS auto-import, chunk splitting, and env filtering
|
|
50
93
|
- **ESLint & PostCSS** — shareable configs included; no extra dev-dependencies needed in your project
|
|
@@ -72,17 +115,11 @@ npm install metaowl
|
|
|
72
115
|
|
|
73
116
|
## Create a New Project
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npx metaowl-create my-app
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Or install metaowl globally and run it interactively:
|
|
118
|
+
Install metaowl globally and run it interactively:
|
|
82
119
|
|
|
83
120
|
```bash
|
|
84
121
|
npm install -g metaowl
|
|
85
|
-
metaowl-create
|
|
122
|
+
metaowl-create my-app
|
|
86
123
|
```
|
|
87
124
|
|
|
88
125
|
This generates a ready-to-run project:
|
|
@@ -219,6 +256,470 @@ SSG path variants (`.html`, trailing slash, `index.html`) are added automaticall
|
|
|
219
256
|
|
|
220
257
|
---
|
|
221
258
|
|
|
259
|
+
### Dynamic Routes
|
|
260
|
+
|
|
261
|
+
File-based routing supports dynamic segments using bracket notation. The router supports required parameters, optional parameters, and catch-all routes.
|
|
262
|
+
|
|
263
|
+
| File | URL Pattern | Example URL | Params |
|
|
264
|
+
|---|---|---|---|
|
|
265
|
+
| `pages/user/[id]/User.js` | `/user/:id` | `/user/123` | `{ id: '123' }` |
|
|
266
|
+
| `pages/product/[category]/[slug]/Product.js` | `/product/:category/:slug` | `/product/tech/hello` | `{ category: 'tech', slug: 'hello' }` |
|
|
267
|
+
| `pages/blog/[id]/[slug?]/Blog.js` | `/blog/:id/:slug?` | `/blog/123` or `/blog/123/my-post` | `{ id: '123' }` or `{ id: '123', slug: 'my-post' }` |
|
|
268
|
+
| `pages/docs/[...path]/Docs.js` | `/docs/:path(.*)` | `/docs/api/routing` | `{ path: 'api/routing' }` |
|
|
269
|
+
| `pages/[...404]/NotFound.js` | `/:path(.*)` | `/any/unknown/path` | `{ path: 'any/unknown/path' }` |
|
|
270
|
+
|
|
271
|
+
**Param Types:**
|
|
272
|
+
|
|
273
|
+
- `[param]` — Required parameter, must be present in URL
|
|
274
|
+
- `[param?]` — Optional parameter, may be omitted
|
|
275
|
+
- `[...param]` — Catch-all parameter, matches any number of segments
|
|
276
|
+
|
|
277
|
+
Access parameters in your component:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
import { Component, xml } from '@odoo/owl'
|
|
281
|
+
|
|
282
|
+
export class UserPage extends Component {
|
|
283
|
+
static template = xml`
|
|
284
|
+
<div>
|
|
285
|
+
<h1>User Profile</h1>
|
|
286
|
+
<p>ID: <t t-esc="props.params.id"/></p>
|
|
287
|
+
</div>
|
|
288
|
+
`
|
|
289
|
+
|
|
290
|
+
static props = ['params']
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Layouts
|
|
297
|
+
|
|
298
|
+
Layouts provide shared page structures. Create a `layouts/` directory alongside your `pages/`:
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
src/
|
|
302
|
+
layouts/
|
|
303
|
+
default/
|
|
304
|
+
DefaultLayout.js
|
|
305
|
+
DefaultLayout.xml
|
|
306
|
+
admin/
|
|
307
|
+
AdminLayout.js
|
|
308
|
+
AdminLayout.xml
|
|
309
|
+
pages/
|
|
310
|
+
index/
|
|
311
|
+
Index.js → uses 'default' layout
|
|
312
|
+
admin/
|
|
313
|
+
dashboard/
|
|
314
|
+
Dashboard.js → can use 'admin' layout
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Use a layout by setting the static `layout` property:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
export class DashboardPage extends Component {
|
|
321
|
+
static template = 'DashboardPage'
|
|
322
|
+
static layout = 'admin'
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
If no layout is specified, the `default` layout is used automatically.
|
|
327
|
+
|
|
328
|
+
**Layout Template Convention:**
|
|
329
|
+
|
|
330
|
+
```xml
|
|
331
|
+
<templates>
|
|
332
|
+
<t t-name="DefaultLayout">
|
|
333
|
+
<div class="layout-default">
|
|
334
|
+
<header>The Header</header>
|
|
335
|
+
<main>
|
|
336
|
+
<t t-slot="default"/>
|
|
337
|
+
</main>
|
|
338
|
+
<footer>The Footer</footer>
|
|
339
|
+
</div>
|
|
340
|
+
</t>
|
|
341
|
+
</templates>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Navigation Guards
|
|
347
|
+
|
|
348
|
+
Navigation guards intercept route navigation and can:
|
|
349
|
+
- Block access to routes
|
|
350
|
+
- Redirect to other routes
|
|
351
|
+
- Perform async checks (authentication, permissions)
|
|
352
|
+
|
|
353
|
+
### Global Guards
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
import { beforeEach, afterEach } from 'metaowl'
|
|
357
|
+
|
|
358
|
+
// Run before navigation
|
|
359
|
+
beforeEach((to, from, next) => {
|
|
360
|
+
const auth = useAuthStore()
|
|
361
|
+
|
|
362
|
+
if (to.meta.requiresAuth && !auth.state.loggedIn) {
|
|
363
|
+
next('/login') // redirect
|
|
364
|
+
} else {
|
|
365
|
+
next() // proceed
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// Run after navigation
|
|
370
|
+
afterEach((to, from) => {
|
|
371
|
+
console.log(`Navigated to ${to.path}`)
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Per-Route Guards
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
export class AdminPage extends Component {
|
|
379
|
+
static route = {
|
|
380
|
+
path: '/admin',
|
|
381
|
+
meta: { requiresAuth: true, role: 'admin' },
|
|
382
|
+
beforeEnter: (to, from, next) => {
|
|
383
|
+
// Check specific permissions
|
|
384
|
+
if (!hasAdminRole()) {
|
|
385
|
+
next('/unauthorized')
|
|
386
|
+
} else {
|
|
387
|
+
next()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Guard Behavior:**
|
|
395
|
+
|
|
396
|
+
- `next()` — proceed to next guard
|
|
397
|
+
- `next(false)` — abort navigation
|
|
398
|
+
- `next('/path')` — redirect to path
|
|
399
|
+
- `next(error)` — abort with error
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## State Management (Store)
|
|
404
|
+
|
|
405
|
+
A Pinia-inspired store system with mutations, actions, and getters.
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
import { Store } from 'metaowl'
|
|
409
|
+
|
|
410
|
+
const useUserStore = Store.define('user', {
|
|
411
|
+
state: () => ({
|
|
412
|
+
name: '',
|
|
413
|
+
loggedIn: false
|
|
414
|
+
}),
|
|
415
|
+
|
|
416
|
+
getters: {
|
|
417
|
+
displayName: (state) => state.name || 'Guest'
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
mutations: {
|
|
421
|
+
setName: (state, name) => { state.name = name },
|
|
422
|
+
setLoggedIn: (state, value) => { state.loggedIn = value }
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
actions: {
|
|
426
|
+
async login({ commit }, credentials) {
|
|
427
|
+
const result = await Fetch.url('/api/login', 'POST', credentials)
|
|
428
|
+
commit('setName', result.name)
|
|
429
|
+
commit('setLoggedIn', true)
|
|
430
|
+
return result
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
logout({ commit }) {
|
|
434
|
+
commit('setName', '')
|
|
435
|
+
commit('setLoggedIn', false)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**In a component:**
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
const store = useUserStore()
|
|
445
|
+
|
|
446
|
+
store.commit('setName', 'John') // synchronous mutation
|
|
447
|
+
await store.dispatch('login', { email, password }) // async action
|
|
448
|
+
console.log(store.getters.displayName.value) // computed getter
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Persistence:**
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
import { Store, createPersistencePlugin } from 'metaowl'
|
|
455
|
+
|
|
456
|
+
// Automatically persist state to localStorage
|
|
457
|
+
Store.use(createPersistencePlugin({
|
|
458
|
+
storage: localStorage,
|
|
459
|
+
paths: ['user', 'preferences'] // only persist specific paths
|
|
460
|
+
}))
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Error Boundaries
|
|
466
|
+
|
|
467
|
+
Handle runtime errors gracefully with automatic fallback UI:
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
import { ErrorBoundary } from 'metaowl'
|
|
471
|
+
|
|
472
|
+
// Wrap your app
|
|
473
|
+
const errorBoundary = ErrorBoundary.wrap(AppComponent)
|
|
474
|
+
errorBoundary.mount(document.body)
|
|
475
|
+
|
|
476
|
+
// Global error handler
|
|
477
|
+
ErrorBoundary.onError((error, context) => {
|
|
478
|
+
console.error('App error:', error, context)
|
|
479
|
+
// Send to error tracking service
|
|
480
|
+
analytics.track('error', { message: error.message, path: context?.route })
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Error Pages:**
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
// src/pages/error.js
|
|
488
|
+
export default class ErrorPage extends Component {
|
|
489
|
+
static template = xml`
|
|
490
|
+
<div class="error-page">
|
|
491
|
+
<h1>Error <t t-esc="props.code || 500"/></h1>
|
|
492
|
+
<p t-esc="props.message"/>
|
|
493
|
+
<button t-on-click="goHome">Go Home</button>
|
|
494
|
+
</div>
|
|
495
|
+
`
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## i18n / Internationalization
|
|
502
|
+
|
|
503
|
+
Full-featured translation system with pluralization:
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
import { I18n } from 'metaowl'
|
|
507
|
+
|
|
508
|
+
await I18n.load({
|
|
509
|
+
locale: 'de',
|
|
510
|
+
messages: {
|
|
511
|
+
welcome: 'Willkommen, {name}!',
|
|
512
|
+
items: '{count, plural, one {# Item} other {# Items}}'
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**In templates:**
|
|
518
|
+
|
|
519
|
+
```xml
|
|
520
|
+
<div t-esc="I18n.t('welcome', { name: state.username })"/>
|
|
521
|
+
<span t-esc="I18n.t('items', { count: state.cartItems })"/>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Pluralization:**
|
|
525
|
+
|
|
526
|
+
```js
|
|
527
|
+
I18n.t('items', { count: 1 }) // "1 Item"
|
|
528
|
+
I18n.t('items', { count: 5 }) // "5 Items"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Form Handling
|
|
534
|
+
|
|
535
|
+
Declarative forms with validation support:
|
|
536
|
+
|
|
537
|
+
```js
|
|
538
|
+
import { useForm } from 'metaowl'
|
|
539
|
+
|
|
540
|
+
class LoginPage extends Component {
|
|
541
|
+
setup() {
|
|
542
|
+
this.form = useForm({
|
|
543
|
+
schema: {
|
|
544
|
+
email: { required: true, type: 'email' },
|
|
545
|
+
password: { required: true, minLength: 8 }
|
|
546
|
+
},
|
|
547
|
+
onSubmit: async (values) => {
|
|
548
|
+
await Fetch.post('/api/login', values)
|
|
549
|
+
this.env.router.navigate('/dashboard')
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
```xml
|
|
557
|
+
<form t-on-submit.prevent="form.submit">
|
|
558
|
+
<input t-model="form.values.email" />
|
|
559
|
+
<span t-if="form.errors.email" t-esc="form.errors.email"/>
|
|
560
|
+
|
|
561
|
+
<input type="password" t-model="form.values.password" />
|
|
562
|
+
<span t-if="form.errors.password" t-esc="form.errors.password"/>
|
|
563
|
+
|
|
564
|
+
<button type="submit" t-att-disabled="form.isSubmitting">
|
|
565
|
+
<t t-if="form.isSubmitting">Loading...</t>
|
|
566
|
+
<t t-else="">Login</t>
|
|
567
|
+
</button>
|
|
568
|
+
</form>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Auto-Import
|
|
574
|
+
|
|
575
|
+
Optional automatic component registration for development productivity:
|
|
576
|
+
|
|
577
|
+
**Enable in vite.config.js:**
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
import { metaowlConfig } from 'metaowl/vite'
|
|
581
|
+
|
|
582
|
+
export default metaowlConfig({
|
|
583
|
+
autoImport: {
|
|
584
|
+
enabled: true,
|
|
585
|
+
pattern: '**/*.js'
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**How it works:**
|
|
591
|
+
|
|
592
|
+
1. Components in `src/components/` are auto-scanned
|
|
593
|
+
2. Type declarations are generated in `.metaowl/components.d.ts`
|
|
594
|
+
3. Use components without manual imports:
|
|
595
|
+
|
|
596
|
+
```js
|
|
597
|
+
// No import needed!
|
|
598
|
+
export default class MyPage extends Component {
|
|
599
|
+
static template = xml`
|
|
600
|
+
<div>
|
|
601
|
+
<Button color="primary"/>
|
|
602
|
+
<Card>
|
|
603
|
+
<Modal t-if="state.showModal"/>
|
|
604
|
+
</Card>
|
|
605
|
+
</div>
|
|
606
|
+
`
|
|
607
|
+
// Components are automatically available
|
|
608
|
+
// from src/components/Button/Button.js, etc.
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Note:** Auto-import is opt-in and primarily useful during development. For production, consider explicit imports for better tree-shaking and clarity.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Odoo JSON-RPC Service
|
|
617
|
+
|
|
618
|
+
Connect to Odoo backends with a full-featured JSON-RPC client:
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
import { OdooService } from 'metaowl'
|
|
622
|
+
|
|
623
|
+
// Configure connection
|
|
624
|
+
OdooService.configure({
|
|
625
|
+
baseUrl: 'https://my-odoo-instance.com',
|
|
626
|
+
database: 'my_database',
|
|
627
|
+
username: 'admin',
|
|
628
|
+
password: 'admin' // or apiKey
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
// Authenticate
|
|
632
|
+
const session = await OdooService.authenticate()
|
|
633
|
+
console.log(`Logged in as ${session.name}`)
|
|
634
|
+
|
|
635
|
+
// Search and read records
|
|
636
|
+
const partners = await OdooService.searchRead('res.partner', {
|
|
637
|
+
domain: [['is_company', '=', true]],
|
|
638
|
+
fields: ['name', 'email', 'phone'],
|
|
639
|
+
limit: 10
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
// Call any model method
|
|
643
|
+
await OdooService.call('res.partner', 'create', [{
|
|
644
|
+
name: 'New Partner',
|
|
645
|
+
email: 'partner@example.com'
|
|
646
|
+
}])
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**CRUD Operations:**
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
// Create
|
|
653
|
+
const id = await OdooService.create('res.partner', { name: 'Test' })
|
|
654
|
+
|
|
655
|
+
// Read
|
|
656
|
+
const records = await OdooService.read('res.partner', [id], ['name'])
|
|
657
|
+
|
|
658
|
+
// Update
|
|
659
|
+
await OdooService.write('res.partner', [id], { name: 'Updated' })
|
|
660
|
+
|
|
661
|
+
// Delete
|
|
662
|
+
await OdooService.unlink('res.partner', [id])
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Session Management:**
|
|
666
|
+
|
|
667
|
+
```js
|
|
668
|
+
// Check authentication
|
|
669
|
+
if (OdooService.isAuthenticated()) {
|
|
670
|
+
console.log('User:', OdooService.getSession().name)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Logout
|
|
674
|
+
OdooService.logout()
|
|
675
|
+
|
|
676
|
+
// Listen to auth changes
|
|
677
|
+
OdooService.onAuthChange((session) => {
|
|
678
|
+
console.log(session ? 'Logged in' : 'Logged out')
|
|
679
|
+
})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Composables / Hooks
|
|
685
|
+
|
|
686
|
+
Reusable OWL hooks for common patterns:
|
|
687
|
+
|
|
688
|
+
```js
|
|
689
|
+
import { useAuth, useLocalStorage, useFetch } from 'metaowl'
|
|
690
|
+
|
|
691
|
+
class MyComponent extends Component {
|
|
692
|
+
setup() {
|
|
693
|
+
// Authentication state
|
|
694
|
+
const { user, isLoggedIn, logout } = useAuth()
|
|
695
|
+
|
|
696
|
+
// Persisted state
|
|
697
|
+
const theme = useLocalStorage('theme', 'light')
|
|
698
|
+
|
|
699
|
+
// Data fetching
|
|
700
|
+
const { data, loading, error, refresh } = useFetch('/api/users')
|
|
701
|
+
|
|
702
|
+
return { user, theme, data, loading, error, refresh }
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Available Composables:**
|
|
708
|
+
|
|
709
|
+
| Composable | Description |
|
|
710
|
+
|---|---|
|
|
711
|
+
| `useAuth()` | Authentication state linked to OdooService |
|
|
712
|
+
| `useLocalStorage(key, default)` | Reactive localStorage access |
|
|
713
|
+
| `useFetch(url, options)` | Data fetching with loading/error states |
|
|
714
|
+
| `useDebounce(value, wait)` | Debounced reactive value |
|
|
715
|
+
| `useThrottle(fn, wait)` | Throttled function |
|
|
716
|
+
| `useWindowSize()` | Reactive window dimensions |
|
|
717
|
+
| `useOnlineStatus()` | Network connectivity state |
|
|
718
|
+
| `useAsyncState(fn)` | Async operation state management |
|
|
719
|
+
| `useCache(key, default)` | Reactive cache access |
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
222
723
|
## CLI Reference
|
|
223
724
|
|
|
224
725
|
metaowl ships four CLI commands that use its own bundled Vite, Prettier, and ESLint binaries — no need to install them separately in your project.
|
|
@@ -392,6 +893,340 @@ buildRoutes(modules: Record<string, object>): RouteDefinition[]
|
|
|
392
893
|
|
|
393
894
|
---
|
|
394
895
|
|
|
896
|
+
### `Store`
|
|
897
|
+
|
|
898
|
+
Pinia-inspired state management system with mutations, actions, and getters.
|
|
899
|
+
|
|
900
|
+
#### `Store.define(id, config)`
|
|
901
|
+
|
|
902
|
+
Creates a store factory function.
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
const useStore = Store.define('storeId', {
|
|
906
|
+
state: () => ({ count: 0 }),
|
|
907
|
+
getters: { double: (state) => state.count * 2 },
|
|
908
|
+
mutations: { increment: (state) => state.count++ },
|
|
909
|
+
actions: { async fetchData({ commit }) { ... } }
|
|
910
|
+
})
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
#### Store Instance Methods
|
|
914
|
+
|
|
915
|
+
| Method | Description |
|
|
916
|
+
|---|---|
|
|
917
|
+
| `commit(mutation, payload)` | Execute synchronous mutation |
|
|
918
|
+
| `dispatch(action, payload)` | Execute async action |
|
|
919
|
+
| `subscribe(callback)` | Listen to mutations `(mutation, state, prevState) => void` |
|
|
920
|
+
| `subscribeAction(callback)` | Listen to actions `(action, status, result) => void` |
|
|
921
|
+
| `reset()` | Reset state to initial values |
|
|
922
|
+
|
|
923
|
+
#### `Store.use(plugin)`
|
|
924
|
+
|
|
925
|
+
Register a global plugin applied to all stores.
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
import { Store, createPersistencePlugin } from 'metaowl'
|
|
929
|
+
|
|
930
|
+
Store.use(createPersistencePlugin({ storage: localStorage }))
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
### `Layouts API`
|
|
936
|
+
|
|
937
|
+
Functions for layout management.
|
|
938
|
+
|
|
939
|
+
| Function | Description |
|
|
940
|
+
|---|---|
|
|
941
|
+
| `registerLayout(name, Component)` | Register a layout |
|
|
942
|
+
| `getLayout(name)` | Get layout component by name |
|
|
943
|
+
| `setDefaultLayout(name)` | Set default layout |
|
|
944
|
+
| `resolveLayout(Component, path?)` | Resolve layout for component |
|
|
945
|
+
| `subscribeToLayouts(callback)` | Listen to layout events |
|
|
946
|
+
|
|
947
|
+
**Component Layout Property:**
|
|
948
|
+
|
|
949
|
+
```js
|
|
950
|
+
export class MyPage extends Component {
|
|
951
|
+
static layout = 'admin' // Use 'admin' layout
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
### `Router Guards API`
|
|
958
|
+
|
|
959
|
+
Functions for navigation control.
|
|
960
|
+
|
|
961
|
+
| Function | Description |
|
|
962
|
+
|---|---|
|
|
963
|
+
| `beforeEach(guard)` | Register global guard (returns unsubscribe) |
|
|
964
|
+
| `afterEach(hook)` | Register global after hook (returns unsubscribe) |
|
|
965
|
+
| `getCurrentRoute()` | Get current route object |
|
|
966
|
+
| `getPreviousRoute()` | Get previous route object |
|
|
967
|
+
| `push(path)` | Navigate to path |
|
|
968
|
+
| `replace(path)` | Replace current history entry |
|
|
969
|
+
| `back()` / `forward()` / `go(n)` | History navigation |
|
|
970
|
+
|
|
971
|
+
**Router Singleton:**
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
import { router } from 'metaowl'
|
|
975
|
+
|
|
976
|
+
router.beforeEach((to, from, next) => { ... })
|
|
977
|
+
router.push('/new-path')
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
### `Error Boundary API`
|
|
983
|
+
|
|
984
|
+
| Function | Description |
|
|
985
|
+
|---|---|
|
|
986
|
+
| `ErrorBoundary.wrap(Component)` | Wrap component with error handling |
|
|
987
|
+
| `ErrorBoundary.onError(callback)` | Register global error handler `(error, context) => void` |
|
|
988
|
+
| `ErrorBoundary.getLastError()` | Get most recent error |
|
|
989
|
+
| `ErrorBoundary.clearError()` | Clear error state |
|
|
990
|
+
|
|
991
|
+
**Error Context:**
|
|
992
|
+
|
|
993
|
+
```ts
|
|
994
|
+
{
|
|
995
|
+
route?: string,
|
|
996
|
+
component?: string,
|
|
997
|
+
timestamp: number
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
### `i18n API`
|
|
1004
|
+
|
|
1005
|
+
**`I18n.load(config)`**
|
|
1006
|
+
|
|
1007
|
+
```ts
|
|
1008
|
+
I18n.load({
|
|
1009
|
+
locale: string,
|
|
1010
|
+
messages: Record<string, string | MessageFunction>,
|
|
1011
|
+
numberFormats?: Record<string, object>,
|
|
1012
|
+
dateFormats?: Record<string, object>
|
|
1013
|
+
})
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
**`I18n.t(key, values?)`**
|
|
1017
|
+
|
|
1018
|
+
Translate a message with optional interpolation:
|
|
1019
|
+
|
|
1020
|
+
```ts
|
|
1021
|
+
I18n.t('welcome', { name: 'John' }) // "Welcome, John!"
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
**`I18n.n(value, format?)`** / **`I18n.d(value, format?)`**
|
|
1025
|
+
|
|
1026
|
+
Format numbers and dates:
|
|
1027
|
+
|
|
1028
|
+
```ts
|
|
1029
|
+
I18n.n(1234.5, 'currency') // "€1,234.50"
|
|
1030
|
+
I18n.d(new Date(), 'short') // "12.03.2026"
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
**Locale Switching:**
|
|
1034
|
+
|
|
1035
|
+
```js
|
|
1036
|
+
await I18n.setLocale('en')
|
|
1037
|
+
console.log(I18n.locale) // "en"
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
---
|
|
1041
|
+
|
|
1042
|
+
### `Forms API`
|
|
1043
|
+
|
|
1044
|
+
**`useForm(options)`**
|
|
1045
|
+
|
|
1046
|
+
```ts
|
|
1047
|
+
useForm({
|
|
1048
|
+
schema?: ValidationSchema,
|
|
1049
|
+
initialValues?: Record<string, any>,
|
|
1050
|
+
onSubmit?: (values: Record<string, any>) => Promise<void>,
|
|
1051
|
+
validateOnChange?: boolean,
|
|
1052
|
+
validateOnBlur?: boolean
|
|
1053
|
+
}): FormInstance
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
**Form Instance:**
|
|
1057
|
+
|
|
1058
|
+
| Property | Type | Description |
|
|
1059
|
+
|---|---|---|
|
|
1060
|
+
| `values` | `object` | Current form values |
|
|
1061
|
+
| `errors` | `object` | Validation errors by field |
|
|
1062
|
+
| `touched` | `object` | Fields that have been touched |
|
|
1063
|
+
| `isSubmitting` | `boolean` | Submit in progress |
|
|
1064
|
+
| `isValid` | `boolean` | All validation passed |
|
|
1065
|
+
| `isDirty` | `boolean` | Values differ from initial |
|
|
1066
|
+
|
|
1067
|
+
| Method | Description |
|
|
1068
|
+
|---|---|
|
|
1069
|
+
| `submit(event?)` | Trigger form submission |
|
|
1070
|
+
| `setValue(field, value)` | Set a field value |
|
|
1071
|
+
| `setValues(values)` | Set multiple values |
|
|
1072
|
+
| `setError(field, message)` | Set a field error |
|
|
1073
|
+
| `clearErrors()` | Clear all errors |
|
|
1074
|
+
| `reset()` | Reset to initial values |
|
|
1075
|
+
| `validate()` | Trigger validation |
|
|
1076
|
+
|
|
1077
|
+
**Validation Schema:**
|
|
1078
|
+
|
|
1079
|
+
```js
|
|
1080
|
+
{
|
|
1081
|
+
email: {
|
|
1082
|
+
required: true,
|
|
1083
|
+
type: 'email'
|
|
1084
|
+
},
|
|
1085
|
+
age: {
|
|
1086
|
+
type: 'number',
|
|
1087
|
+
min: 0,
|
|
1088
|
+
max: 120
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
1095
|
+
### `OdooService API`
|
|
1096
|
+
|
|
1097
|
+
| Method | Description |
|
|
1098
|
+
|---|---|
|
|
1099
|
+
| `configure(config)` | Configure Odoo connection (baseUrl, database, credentials) |
|
|
1100
|
+
| `authenticate()` | Login and get session |
|
|
1101
|
+
| `logout()` | Clear session |
|
|
1102
|
+
| `isAuthenticated()` | Check if currently logged in |
|
|
1103
|
+
| `getSession()` | Get current session info |
|
|
1104
|
+
| `onAuthChange(callback)` | Subscribe to auth state changes (returns unsubscribe) |
|
|
1105
|
+
|
|
1106
|
+
**CRUD Operations:**
|
|
1107
|
+
|
|
1108
|
+
| Method | Description |
|
|
1109
|
+
|---|---|
|
|
1110
|
+
| `searchRead(model, options)` | Search and read records |
|
|
1111
|
+
| `call(model, method, args, kwargs)` | Call any model method |
|
|
1112
|
+
| `read(model, ids, fields)` | Read specific records |
|
|
1113
|
+
| `create(model, values)` | Create new record |
|
|
1114
|
+
| `write(model, ids, values)` | Update records |
|
|
1115
|
+
| `unlink(model, ids)` | Delete records |
|
|
1116
|
+
| `searchCount(model, domain)` | Get count of matching records |
|
|
1117
|
+
|
|
1118
|
+
**Utility Methods:**
|
|
1119
|
+
|
|
1120
|
+
| Method | Description |
|
|
1121
|
+
|---|---|
|
|
1122
|
+
| `listDatabases()` | Get available databases |
|
|
1123
|
+
| `versionInfo()` | Get Odoo server version info |
|
|
1124
|
+
|
|
1125
|
+
**Configuration Options:**
|
|
1126
|
+
|
|
1127
|
+
```ts
|
|
1128
|
+
{
|
|
1129
|
+
baseUrl: string, // Odoo instance URL
|
|
1130
|
+
database: string, // Database name
|
|
1131
|
+
username?: string, // Username (or use in authenticate())
|
|
1132
|
+
password?: string, // Password (or apiKey)
|
|
1133
|
+
apiKey?: string, // API Key alternative to password
|
|
1134
|
+
persistSession?: boolean // Persist to localStorage (default: true)
|
|
1135
|
+
}
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
### `Composables API`
|
|
1141
|
+
|
|
1142
|
+
**`useAuth()`**
|
|
1143
|
+
|
|
1144
|
+
Authentication state for Odoo integration.
|
|
1145
|
+
|
|
1146
|
+
```ts
|
|
1147
|
+
{
|
|
1148
|
+
user: Ref<Session|null>, // Current user info
|
|
1149
|
+
isLoggedIn: Ref<boolean>, // Auth status
|
|
1150
|
+
isLoading: Ref<boolean>, // Loading state
|
|
1151
|
+
login: (credentials) => Promise<boolean>,
|
|
1152
|
+
logout: () => Promise<void>,
|
|
1153
|
+
checkAuth: () => Promise<boolean>
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
**`useLocalStorage(key, defaultValue)`**
|
|
1158
|
+
|
|
1159
|
+
Reactive localStorage access with cross-tab sync.
|
|
1160
|
+
|
|
1161
|
+
```ts
|
|
1162
|
+
const theme = useLocalStorage('theme', 'light')
|
|
1163
|
+
|
|
1164
|
+
theme.value = 'dark' // Automatically saves to localStorage
|
|
1165
|
+
// Other tabs are notified via storage event
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
**`useFetch(url, options)`**
|
|
1169
|
+
|
|
1170
|
+
Data fetching with reactive states.
|
|
1171
|
+
|
|
1172
|
+
```ts
|
|
1173
|
+
{
|
|
1174
|
+
data: Ref<any>, // Fetched data
|
|
1175
|
+
loading: Ref<boolean>,
|
|
1176
|
+
error: Ref<Error|null>,
|
|
1177
|
+
refresh: () => Promise<void>,
|
|
1178
|
+
execute: (url?) => Promise<void>
|
|
1179
|
+
}
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
Options: `initialData`, `immediate`, `transform`, `onError`
|
|
1183
|
+
|
|
1184
|
+
**`useDebounce(value, wait)`**
|
|
1185
|
+
|
|
1186
|
+
```ts
|
|
1187
|
+
const searchQuery = useState('')
|
|
1188
|
+
const debounced = useDebounce(searchQuery, 500)
|
|
1189
|
+
// debounced updates 500ms after searchQuery stops changing
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
**`useThrottle(fn, wait)`**
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
const throttledSearch = useThrottle((query) => {
|
|
1196
|
+
performSearch(query)
|
|
1197
|
+
}, 500)
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
**`useWindowSize()`**
|
|
1201
|
+
|
|
1202
|
+
```ts
|
|
1203
|
+
const { width, height } = useWindowSize()
|
|
1204
|
+
const isMobile = computed(() => width.value < 768)
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
**`useOnlineStatus()`**
|
|
1208
|
+
|
|
1209
|
+
```ts
|
|
1210
|
+
const isOnline = useOnlineStatus()
|
|
1211
|
+
// Reactive to network state changes
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
**`useAsyncState(asyncFn, options)`**
|
|
1215
|
+
|
|
1216
|
+
```ts
|
|
1217
|
+
const { state, data, execute, isLoading, isSuccess, isError } =
|
|
1218
|
+
useAsyncState(fetchUserData, { immediate: true })
|
|
1219
|
+
// state: null | 'loading' | 'success' | 'error'
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
**`useCache(key, defaultValue)`**
|
|
1223
|
+
|
|
1224
|
+
```ts
|
|
1225
|
+
const { value, set, get, remove, clear } = useCache('user-prefs', {})
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
395
1230
|
## Vite Plugin
|
|
396
1231
|
|
|
397
1232
|
### `metaowlPlugin(options)`
|
|
@@ -409,6 +1244,14 @@ Returns an array of Vite plugins that configure the full metaowl build pipeline.
|
|
|
409
1244
|
| `frameworkEntry` | `'./node_modules/metaowl/index.js'` | Entry for the `framework` chunk |
|
|
410
1245
|
| `restartGlobs` | `[]` | Additional globs that trigger dev-server restart |
|
|
411
1246
|
| `envPrefix` | `undefined` | Only expose `process.env` vars with this prefix (plus `NODE_ENV`) |
|
|
1247
|
+
| `autoImport` | `{ enabled: false }` | Auto-import configuration: `{ enabled, pattern }` |
|
|
1248
|
+
|
|
1249
|
+
**Auto-Import Options:**
|
|
1250
|
+
|
|
1251
|
+
| Option | Default | Description |
|
|
1252
|
+
|---|---|---|
|
|
1253
|
+
| `enabled` | `false` | Enable component auto-import |
|
|
1254
|
+
| `pattern` | `'*.js'` | Glob pattern for scanning components |
|
|
412
1255
|
|
|
413
1256
|
**What the plugin does:**
|
|
414
1257
|
|