@mapomodule/form 2.0.0-beta.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 +155 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +34 -0
- package/dist/module.d.cts +2 -0
- package/dist/module.d.mts +2 -0
- package/dist/module.d.ts +2 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +18 -0
- package/dist/types.d.mts +13 -0
- package/package.json +78 -0
- package/src/__tests__/__mocks__/nuxt-imports.ts +5 -0
- package/src/__tests__/registry.test.ts +105 -0
- package/src/__tests__/useFormFromSchema.test.ts +415 -0
- package/src/__tests__/useMapoForm.test.ts +319 -0
- package/src/index.ts +49 -0
- package/src/module.ts +213 -0
- package/src/runtime/components/MapoFocusPortal.vue +154 -0
- package/src/runtime/components/MapoForm.vue +247 -0
- package/src/runtime/components/MapoFormField.vue +350 -0
- package/src/runtime/components/MapoFormFlatSection.vue +163 -0
- package/src/runtime/components/MapoFormGroup.vue +283 -0
- package/src/runtime/components/MapoFormTabs.vue +116 -0
- package/src/runtime/components/MapoUnknownField.vue +74 -0
- package/src/runtime/components/fields/MapoDateField.vue +98 -0
- package/src/runtime/components/fields/MapoDateTimeField.vue +192 -0
- package/src/runtime/components/fields/MapoFileField.vue +106 -0
- package/src/runtime/components/fields/MapoFksField.vue +151 -0
- package/src/runtime/components/fields/MapoMapField.vue +117 -0
- package/src/runtime/components/fields/MapoMapFieldClient.vue +92 -0
- package/src/runtime/components/fields/MapoRepeater.vue +553 -0
- package/src/runtime/components/fields/MapoRepeaterItem.vue +409 -0
- package/src/runtime/components/fields/MapoSeoPreview.vue +137 -0
- package/src/runtime/components/fields/MapoTimeField.vue +59 -0
- package/src/runtime/components/fields/MapoWygEditor.vue +256 -0
- package/src/runtime/components/fields/nui/NuiCheckbox.vue +8 -0
- package/src/runtime/components/fields/nui/NuiInput.vue +8 -0
- package/src/runtime/components/fields/nui/NuiSelectMenu.vue +27 -0
- package/src/runtime/components/fields/nui/NuiSlider.vue +8 -0
- package/src/runtime/components/fields/nui/NuiSwitch.vue +8 -0
- package/src/runtime/components/fields/nui/NuiTextarea.vue +8 -0
- package/src/runtime/components/fields/nui/useNuiFieldAttrs.ts +30 -0
- package/src/runtime/composables/defineFormField.ts +62 -0
- package/src/runtime/composables/useCurrentLang.ts +16 -0
- package/src/runtime/composables/useFocusMode.ts +86 -0
- package/src/runtime/composables/useFormDraft.ts +119 -0
- package/src/runtime/composables/useFormFromSchema.ts +253 -0
- package/src/runtime/composables/useMapoForm.ts +484 -0
- package/src/runtime/composables/useProgressiveDisclosure.ts +101 -0
- package/src/runtime/composables/useUnsavedChangesGuard.ts +58 -0
- package/src/runtime/devtools/DevtoolsFormsPage.vue +256 -0
- package/src/runtime/plugins/form-registry.ts +43 -0
- package/src/runtime/registry/defaults.ts +97 -0
- package/src/runtime/types/descriptor.ts +411 -0
- package/src/runtime/types/index.ts +40 -0
- package/src/runtime/types/layout.ts +22 -0
- package/src/runtime/types/registry.ts +98 -0
- package/src/runtime/utils/flattenFieldGroups.ts +86 -0
- package/src/vue-shim.d.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @mapomodule/form
|
|
2
|
+
|
|
3
|
+
Form engine for Mapo v2 — descriptor-driven, typesafe, headless-ready admin forms for Nuxt 4 + Django/DRF.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Descriptor-driven**: define fields as `FieldDescriptor[]` — layout, validation, i18n in one object
|
|
8
|
+
- **All field types built-in**: text, select, date/time, FK/M2M autocomplete, WYSIWYG editor, SEO preview, map, repeater
|
|
9
|
+
- **Headless composable**: `useMapoForm()` for fully custom layouts; `<MapoForm>` for the default shell
|
|
10
|
+
- **Registry auto-injection**: `<MapoForm>` and `<MapoDetail>` resolve `$mapoFormRegistry` automatically — no boilerplate
|
|
11
|
+
- **Typed extension API**: `defineFormField(type, entry, opts)` to register custom fields with collision detection
|
|
12
|
+
- **JSON Schema bridge**: `useFormFromSchema()` generates descriptors from Pydantic v2 / DRF-spectacular schemas
|
|
13
|
+
- **Multilingual fields**: `translatable: true` writes to `model.translations[lang].field` automatically
|
|
14
|
+
- **Client validation**: `validate(value, ctx)` per descriptor + `required` shorthand
|
|
15
|
+
- **Tab/group layout**: declare `tab` and `group` per field — tabs render automatically (incl. nested tabs), groups are collapsible. Tab panels expose `tab.<name>.before/after`, full override, `.label` and `tab-bar-end` slots
|
|
16
|
+
- **Consistent repeater items**: `MapoRepeater` drives each item through `useMapoForm`, so nested-path keys, `translatable`, accessors, `descriptor.is` overrides and **client validation** behave identically inside a repeater
|
|
17
|
+
- **Draft persistence**: `draftKey` option on `useMapoForm()` auto-saves to localStorage every 2 s; `checkDraft()` / `draftBanner` offer restore/discard; clears on successful `submit()`
|
|
18
|
+
- **Fail-soft**: unknown field types render a non-destructive placeholder, never crash the form
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
`@mapomodule/form` is included automatically when you install `mapomodule` (recommended). No additional module entry is needed.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// nuxt.config.ts
|
|
26
|
+
export default defineNuxtConfig({
|
|
27
|
+
modules: ["@nuxt/ui", "mapomodule"],
|
|
28
|
+
mapo: {
|
|
29
|
+
form: {
|
|
30
|
+
// optional — global field registry defaults
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import type { FieldDescriptor } from "@mapomodule/form";
|
|
41
|
+
|
|
42
|
+
interface Article {
|
|
43
|
+
title: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
is_draft: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const article = ref<Article>({ title: "", slug: "", is_draft: true });
|
|
49
|
+
const errors = ref<Record<string, string[]>>({});
|
|
50
|
+
|
|
51
|
+
const fields: FieldDescriptor<Article>[] = [
|
|
52
|
+
{ key: "title", type: "text", label: "Title", required: true },
|
|
53
|
+
{ key: "slug", type: "text", label: "Slug" },
|
|
54
|
+
{ key: "is_draft", type: "switch", label: "Draft" },
|
|
55
|
+
];
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<MapoForm v-model="article" :fields="fields" :errors="errors" />
|
|
60
|
+
</template>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `registry` prop is optional — `<MapoForm>` falls back to the global `$mapoFormRegistry` provided by this module's plugin. Pass it explicitly only when you want to swap the registry for a single form.
|
|
64
|
+
|
|
65
|
+
## Field types
|
|
66
|
+
|
|
67
|
+
| Type | Component | Notes |
|
|
68
|
+
| ---------------------------- | --------------------- | ----------------------------------------------------------- |
|
|
69
|
+
| `text` | `UInput` | |
|
|
70
|
+
| `textarea` | `UTextarea` | |
|
|
71
|
+
| `number` | `UInput[type=number]` | |
|
|
72
|
+
| `boolean` | `UCheckbox` | |
|
|
73
|
+
| `switch` | `USwitch` | |
|
|
74
|
+
| `color` | `UInput[type=color]` | |
|
|
75
|
+
| `slider` | `USlider` | |
|
|
76
|
+
| `file` | `MapoFileField` | Current-file preview, image thumbnail, remove button |
|
|
77
|
+
| `select` | `USelectMenu` | requires `attrs.options` |
|
|
78
|
+
| `date` / `datetime` / `time` | `MapoDateField` etc. | ISO string in model; `tz: 'naive' \| 'utc'` for `datetime` |
|
|
79
|
+
| `fks` | `MapoFksField` | FK autocomplete; `attrs.endpoint` required |
|
|
80
|
+
| `m2m` | `MapoFksField` | Multi-select autocomplete, removable chips |
|
|
81
|
+
| `editor` | `MapoWygEditor` | Tiptap v2; sanitized output, safe `Link` validator |
|
|
82
|
+
| `seo` | `MapoSeoPreview` | Title / description with live SERP preview |
|
|
83
|
+
| `map` | `MapoMapField` | Leaflet, `{ lat, lng }` in model, SSR-safe via `ClientOnly` |
|
|
84
|
+
| `repeater` | `MapoRepeater` | Nested fields, drag-and-drop, undo stack, templates |
|
|
85
|
+
|
|
86
|
+
## JSON Schema → Form
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const schema = await $fetch("/api/schema/");
|
|
90
|
+
const fields = useFormFromSchema(schema, {
|
|
91
|
+
exclude: ["id", "created_at"],
|
|
92
|
+
overrides: {
|
|
93
|
+
body: { type: "editor", translatable: true },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Headless
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const form = useMapoForm({ model, fields, registry: $mapoFormRegistry });
|
|
102
|
+
|
|
103
|
+
// form.isDirty, form.isLoading, form.getPatch(), form.submit(handler)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The headless composable still requires the registry explicitly — `useNuxtApp()` is not available in every context where you might want to drive a form (tests, storybook, server-side preview).
|
|
107
|
+
|
|
108
|
+
## Draft persistence (headless)
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const form = useMapoForm({
|
|
112
|
+
model,
|
|
113
|
+
fields,
|
|
114
|
+
registry: $mapoFormRegistry,
|
|
115
|
+
draftKey: `article:${route.params.id}`,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// After fetch:
|
|
119
|
+
form.checkDraft(); // populates form.draftBanner if a draft is found
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`form.draftBanner.value` exposes `{ savedAt, restore(), discard() }`. On `submit()` success the draft is cleared automatically. When using `<MapoDetail :draft="true">`, all of this is handled for you.
|
|
123
|
+
|
|
124
|
+
## Custom fields
|
|
125
|
+
|
|
126
|
+
The recommended way to register a custom field is the typed `defineFormField()` helper, called from a Nuxt plugin:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// app/plugins/my-fields.ts
|
|
130
|
+
export default defineNuxtPlugin(() => {
|
|
131
|
+
defineFormField("rich-text", () => import("~/components/RichText.vue"), {
|
|
132
|
+
attrs: { rows: 6 },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Plugins in `app/plugins/` run after module plugins, so `$mapoFormRegistry` is always available. Re-registering an existing type prints a development warning unless you pass `{ override: true }` explicitly.
|
|
138
|
+
|
|
139
|
+
Any component with `modelValue` + `descriptor` + `errors` + `readonly` props works as a field.
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
Full docs: [Form Engine — Quickstart](../../../docs/uikit/form/index.md)
|
|
144
|
+
|
|
145
|
+
- [All field types](../../../docs/uikit/form/add-fields.md)
|
|
146
|
+
- [Validation](../../../docs/uikit/form/validation.md)
|
|
147
|
+
- [i18n / multilingual fields](../../../docs/uikit/form/i18n.md)
|
|
148
|
+
- [`useMapoForm()` headless](../../../docs/uikit/form/composable.md)
|
|
149
|
+
- [Custom fields & registry](../../../docs/uikit/form/custom-fields.md)
|
|
150
|
+
- [Slot system](../../../docs/uikit/form/slots.md)
|
|
151
|
+
- [JSON Schema bridge](../../../docs/uikit/form/schema-to-form.md)
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.js";
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.js";
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createJiti } from "file:///Volumes/Development/Lotrek/Projects/mapo-new/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/jiti.mjs";
|
|
2
|
+
|
|
3
|
+
const jiti = createJiti(import.meta.url, {
|
|
4
|
+
"interopDefault": true,
|
|
5
|
+
"alias": {
|
|
6
|
+
"@mapomodule/form": "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form"
|
|
7
|
+
},
|
|
8
|
+
"transformOptions": {
|
|
9
|
+
"babel": {
|
|
10
|
+
"plugins": []
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @type {import("/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.js")} */
|
|
16
|
+
const _module = await jiti.import("/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.ts");
|
|
17
|
+
|
|
18
|
+
export const useFormFromSchema = _module.useFormFromSchema;
|
|
19
|
+
export const useFormDraft = _module.useFormDraft;
|
|
20
|
+
export const useFocusMode = _module.useFocusMode;
|
|
21
|
+
export const useUnsavedChangesGuard = _module.useUnsavedChangesGuard;
|
|
22
|
+
export const when = _module.when;
|
|
23
|
+
export const whenAny = _module.whenAny;
|
|
24
|
+
export const whenNot = _module.whenNot;
|
|
25
|
+
export const matchesField = _module.matchesField;
|
|
26
|
+
export const isOneOf = _module.isOneOf;
|
|
27
|
+
export const isNoneOf = _module.isNoneOf;
|
|
28
|
+
export const isNotEmpty = _module.isNotEmpty;
|
|
29
|
+
export const isEmpty = _module.isEmpty;
|
|
30
|
+
export const greaterThan = _module.greaterThan;
|
|
31
|
+
export const lessThan = _module.lessThan;
|
|
32
|
+
export const matches = _module.matches;
|
|
33
|
+
export const flattenFieldGroups = _module.flattenFieldGroups;
|
|
34
|
+
export const KnownFieldType = _module.KnownFieldType;
|
package/dist/module.d.ts
ADDED
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createJiti } from "file:///Volumes/Development/Lotrek/Projects/mapo-new/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/jiti.mjs";
|
|
2
|
+
|
|
3
|
+
const jiti = createJiti(import.meta.url, {
|
|
4
|
+
"interopDefault": true,
|
|
5
|
+
"alias": {
|
|
6
|
+
"@mapomodule/form": "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form"
|
|
7
|
+
},
|
|
8
|
+
"transformOptions": {
|
|
9
|
+
"babel": {
|
|
10
|
+
"plugins": []
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @type {import("/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/module.js")} */
|
|
16
|
+
const _module = await jiti.import("/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/module.ts");
|
|
17
|
+
|
|
18
|
+
export default _module?.default ?? _module;
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ModuleHooks, ModuleRuntimeHooks, ModuleRuntimeConfig, ModulePublicRuntimeConfig } from './module.mjs'
|
|
2
|
+
|
|
3
|
+
declare module '#app' {
|
|
4
|
+
interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module '@nuxt/schema' {
|
|
8
|
+
interface NuxtHooks extends ModuleHooks {}
|
|
9
|
+
interface RuntimeConfig extends ModuleRuntimeConfig {}
|
|
10
|
+
interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export * from "./module.mjs"
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mapomodule/form",
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"description": "Mapo declarative typed form engine and field components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Lotrek Agency (https://lotrek.it)",
|
|
8
|
+
"homepage": "https://lotrekagency.github.io/mapo-new/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/lotrekagency/mapo-new.git",
|
|
12
|
+
"directory": "packages/@mapomodule/form"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/lotrekagency/mapo-new/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"tag": "next"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/module.d.mts",
|
|
24
|
+
"import": "./dist/module.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./types": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"import": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./runtime/*": {
|
|
31
|
+
"types": "./src/runtime/*.ts",
|
|
32
|
+
"import": "./dist/runtime/*.js",
|
|
33
|
+
"default": "./src/runtime/*"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "nuxt-module-build build && vue-tsc -p tsconfig.build-types.json && node ../../../scripts/clean-empty-vue-dts.mjs dist",
|
|
42
|
+
"dev": "nuxt-module-build build --stub --watch",
|
|
43
|
+
"prepare": "nuxt-module-build build --stub",
|
|
44
|
+
"dev:prepare": "nuxt-module-build build --stub",
|
|
45
|
+
"typecheck": "vue-tsc --noEmit",
|
|
46
|
+
"lint": "eslint src",
|
|
47
|
+
"test": "vitest run --passWithNoTests"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@internationalized/date": "^3.7.0",
|
|
51
|
+
"@mapomodule/store": "workspace:*",
|
|
52
|
+
"@mapomodule/utils": "workspace:*",
|
|
53
|
+
"@tiptap/extension-link": "^2.11.5",
|
|
54
|
+
"@tiptap/extension-underline": "^2.11.5",
|
|
55
|
+
"@tiptap/starter-kit": "^2.11.5",
|
|
56
|
+
"@tiptap/vue-3": "^2.11.5",
|
|
57
|
+
"leaflet": "^1.9.4",
|
|
58
|
+
"vue-draggable-plus": "^0.6.0"
|
|
59
|
+
},
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"@types/leaflet": "^1.9.18"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"nuxt": ">=4.0.0",
|
|
65
|
+
"vue": ">=3.5.0"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@nuxt/kit": "4.4.2",
|
|
69
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
70
|
+
"@nuxt/schema": "4.4.2",
|
|
71
|
+
"nuxt": "^4.4.2",
|
|
72
|
+
"vitest": "^3.2.0",
|
|
73
|
+
"vue": "^3.5.32",
|
|
74
|
+
"vue-router": "^5.0.4",
|
|
75
|
+
"vue-tsc": "^3.3.4",
|
|
76
|
+
"zod": "^3.23.8"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveFieldComponent,
|
|
4
|
+
resolveFieldAttrs,
|
|
5
|
+
resolveFieldAccessor,
|
|
6
|
+
} from "../runtime/types/registry.js";
|
|
7
|
+
import { defaultRegistry } from "../runtime/registry/defaults.js";
|
|
8
|
+
import type {
|
|
9
|
+
AnyFieldDescriptor,
|
|
10
|
+
FieldDescriptor,
|
|
11
|
+
} from "../runtime/types/descriptor.js";
|
|
12
|
+
|
|
13
|
+
describe("resolveFieldComponent", () => {
|
|
14
|
+
it("returns a lazy function for text type", () => {
|
|
15
|
+
const d = { key: "title", type: "text" } as FieldDescriptor;
|
|
16
|
+
expect(typeof resolveFieldComponent(d, defaultRegistry)).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns a lazy function for select type", () => {
|
|
20
|
+
const d = {
|
|
21
|
+
key: "cat",
|
|
22
|
+
type: "select",
|
|
23
|
+
attrs: { options: [] },
|
|
24
|
+
} as FieldDescriptor;
|
|
25
|
+
expect(typeof resolveFieldComponent(d, defaultRegistry)).toBe("function");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns a lazy function for date type", () => {
|
|
29
|
+
const d = { key: "pub", type: "date" } as FieldDescriptor;
|
|
30
|
+
const entry = resolveFieldComponent(d, defaultRegistry);
|
|
31
|
+
expect(typeof entry).toBe("function");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("descriptor.is overrides registry", () => {
|
|
35
|
+
const CustomComp = {} as any;
|
|
36
|
+
const d = { key: "x", type: "text", is: CustomComp } as FieldDescriptor;
|
|
37
|
+
expect(resolveFieldComponent(d, defaultRegistry)).toBe(CustomComp);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns null for unknown type", () => {
|
|
41
|
+
const d = { key: "x", type: "unknown-xyz" } as AnyFieldDescriptor;
|
|
42
|
+
expect(resolveFieldComponent(d, defaultRegistry)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("user registry override wins over default", () => {
|
|
46
|
+
const CustomFn = () => Promise.resolve({ default: {} as any });
|
|
47
|
+
const registry = {
|
|
48
|
+
...defaultRegistry,
|
|
49
|
+
mapping: { ...defaultRegistry.mapping, text: CustomFn },
|
|
50
|
+
};
|
|
51
|
+
const d = { key: "title", type: "text" } as FieldDescriptor;
|
|
52
|
+
expect(resolveFieldComponent(d, registry)).toBe(CustomFn);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("resolveFieldAttrs", () => {
|
|
57
|
+
it("applies All attrs to every field", () => {
|
|
58
|
+
const d = { key: "x", type: "text" } as FieldDescriptor;
|
|
59
|
+
const attrs = resolveFieldAttrs(d, defaultRegistry);
|
|
60
|
+
expect(attrs.class).toBe("w-full");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("merges type-specific attrs", () => {
|
|
64
|
+
const d = { key: "n", type: "number" } as FieldDescriptor;
|
|
65
|
+
const attrs = resolveFieldAttrs(d, defaultRegistry);
|
|
66
|
+
expect(attrs.type).toBe("number");
|
|
67
|
+
expect(attrs.class).toBe("w-full");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("descriptor.attrs wins over registry", () => {
|
|
71
|
+
const d = {
|
|
72
|
+
key: "n",
|
|
73
|
+
type: "text",
|
|
74
|
+
attrs: { placeholder: "Hello" },
|
|
75
|
+
} as FieldDescriptor;
|
|
76
|
+
const attrs = resolveFieldAttrs(d, defaultRegistry);
|
|
77
|
+
expect(attrs.placeholder).toBe("Hello");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("resolveFieldAccessor", () => {
|
|
82
|
+
it("returns empty accessor for types without registry accessor", () => {
|
|
83
|
+
const d = { key: "title", type: "text" } as FieldDescriptor;
|
|
84
|
+
const acc = resolveFieldAccessor(d, defaultRegistry);
|
|
85
|
+
expect(acc.get).toBeUndefined();
|
|
86
|
+
expect(acc.set).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns accessor for seo type", () => {
|
|
90
|
+
const d = { key: "seo", type: "seo" } as FieldDescriptor;
|
|
91
|
+
const acc = resolveFieldAccessor(d, defaultRegistry);
|
|
92
|
+
expect(typeof acc.get).toBe("function");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("descriptor.accessor.get overrides registry", () => {
|
|
96
|
+
const customGet = () => "custom";
|
|
97
|
+
const d = {
|
|
98
|
+
key: "x",
|
|
99
|
+
type: "seo",
|
|
100
|
+
accessor: { get: customGet },
|
|
101
|
+
} as FieldDescriptor;
|
|
102
|
+
const acc = resolveFieldAccessor(d, defaultRegistry);
|
|
103
|
+
expect(acc.get).toBe(customGet);
|
|
104
|
+
});
|
|
105
|
+
});
|