@mapomodule/form 2.0.0-beta.1 → 2.0.0-beta.10

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 (194) hide show
  1. package/README.md +2 -1
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  3. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  4. package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
  5. package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
  6. package/dist/__tests__/registry.test.d.ts +2 -0
  7. package/dist/__tests__/registry.test.d.ts.map +1 -0
  8. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  9. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  10. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  11. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  12. package/dist/index.d.mts +9 -1
  13. package/dist/index.d.ts +15 -1
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.mjs +7 -34
  16. package/dist/module.d.mts +20 -2
  17. package/dist/module.d.ts +12 -2
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.json +1 -1
  20. package/dist/module.mjs +187 -13
  21. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  22. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  23. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoForm.vue +168 -0
  25. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  26. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormField.vue +269 -0
  28. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  29. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  31. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  34. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  35. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoFormTabs.vue +110 -0
  37. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  38. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  40. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  41. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  43. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  46. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  49. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  52. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  55. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  58. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  61. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
  64. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
  67. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  70. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  73. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  76. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  79. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  82. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/MapoWygEditor.vue +252 -0
  85. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  91. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  93. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  94. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  96. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  97. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  99. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  100. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  102. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  103. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  105. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  106. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  108. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  109. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  110. package/dist/runtime/composables/defineFormField.js +27 -0
  111. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  112. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  113. package/dist/runtime/composables/useCurrentLang.js +9 -0
  114. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  115. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  116. package/dist/runtime/composables/useFocusMode.js +31 -0
  117. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  118. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  119. package/dist/runtime/composables/useFormDraft.js +52 -0
  120. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  121. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  122. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  123. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  124. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  125. package/dist/runtime/composables/useMapoForm.js +325 -0
  126. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  127. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  128. package/dist/runtime/composables/useMediaManager.js +6 -0
  129. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  130. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  132. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  133. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  135. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  136. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  138. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  139. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.js +19 -0
  141. package/dist/runtime/registry/defaults.d.ts +3 -0
  142. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  143. package/dist/runtime/registry/defaults.js +93 -0
  144. package/dist/runtime/types/descriptor.d.ts +373 -0
  145. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  146. package/dist/runtime/types/descriptor.js +26 -0
  147. package/dist/runtime/types/index.d.ts +12 -0
  148. package/dist/runtime/types/index.d.ts.map +1 -0
  149. package/dist/runtime/types/index.js +6 -0
  150. package/dist/runtime/types/layout.d.ts +21 -0
  151. package/dist/runtime/types/layout.d.ts.map +1 -0
  152. package/dist/runtime/types/layout.js +0 -0
  153. package/dist/runtime/types/media.d.ts +43 -0
  154. package/dist/runtime/types/media.d.ts.map +1 -0
  155. package/dist/runtime/types/media.js +0 -0
  156. package/dist/runtime/types/registry.d.ts +62 -0
  157. package/dist/runtime/types/registry.d.ts.map +1 -0
  158. package/dist/runtime/types/registry.js +19 -0
  159. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  160. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  161. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  162. package/dist/types.d.mts +6 -10
  163. package/package.json +10 -8
  164. package/src/__tests__/article.schema.json +193 -0
  165. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  166. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  167. package/src/__tests__/useMapoForm.test.ts +5 -0
  168. package/src/index.ts +5 -0
  169. package/src/module.ts +34 -0
  170. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  171. package/src/runtime/components/MapoFormTabs.vue +4 -1
  172. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  173. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  174. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  175. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  176. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  177. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  178. package/src/runtime/components/fields/MapoMediaField.vue +104 -0
  179. package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
  180. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  181. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  182. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  183. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  184. package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
  185. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  186. package/src/runtime/composables/useMapoForm.ts +4 -2
  187. package/src/runtime/composables/useMediaManager.ts +21 -0
  188. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  189. package/src/runtime/registry/defaults.ts +9 -1
  190. package/src/runtime/types/descriptor.ts +22 -7
  191. package/src/runtime/types/index.ts +4 -0
  192. package/src/runtime/types/media.ts +44 -0
  193. package/dist/index.d.cts +0 -1
  194. package/dist/module.d.cts +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flattenFieldGroups.d.ts","sourceRoot":"","sources":["../../../src/runtime/utils/flattenFieldGroups.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC/D,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,yFAAyF;IACzF,MAAM,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;CAChE;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC,EAC7D,eAAe,CAAC,EAAE,MAAM,EACxB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAChC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAsBzB"}
@@ -0,0 +1,24 @@
1
+ function isGroupDescriptor(item) {
2
+ return "fields" in item && Array.isArray(item.fields);
3
+ }
4
+ export function flattenFieldGroups(items, _inheritedGroup, _inheritedTab) {
5
+ const result = [];
6
+ for (const item of items) {
7
+ if (isGroupDescriptor(item)) {
8
+ const group = item.group ?? _inheritedGroup;
9
+ const tab = item.tab ?? _inheritedTab;
10
+ result.push(...flattenFieldGroups(item.fields, group, tab));
11
+ } else {
12
+ const field = item;
13
+ const merged = { ...field };
14
+ if (_inheritedGroup && !merged.group) {
15
+ Object.assign(merged, { group: _inheritedGroup });
16
+ }
17
+ if (_inheritedTab && !merged.tab) {
18
+ Object.assign(merged, { tab: _inheritedTab });
19
+ }
20
+ result.push(merged);
21
+ }
22
+ }
23
+ return result;
24
+ }
package/dist/types.d.mts CHANGED
@@ -1,13 +1,9 @@
1
- import type { ModuleHooks, ModuleRuntimeHooks, ModuleRuntimeConfig, ModulePublicRuntimeConfig } from './module.mjs'
1
+ import type { NuxtModule } from '@nuxt/schema'
2
2
 
3
- declare module '#app' {
4
- interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
5
- }
3
+ import type { default as Module } from './module.mjs'
6
4
 
7
- declare module '@nuxt/schema' {
8
- interface NuxtHooks extends ModuleHooks {}
9
- interface RuntimeConfig extends ModuleRuntimeConfig {}
10
- interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
11
- }
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
12
6
 
13
- export * from "./module.mjs"
7
+ export { default } from './module.mjs'
8
+
9
+ export { type MapoFormOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapomodule/form",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.10",
4
4
  "description": "Mapo declarative typed form engine and field components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,6 @@
40
40
  "scripts": {
41
41
  "build": "nuxt-module-build build && vue-tsc -p tsconfig.build-types.json && node ../../../scripts/clean-empty-vue-dts.mjs dist",
42
42
  "dev": "nuxt-module-build build --stub --watch",
43
- "prepare": "nuxt-module-build build --stub",
44
43
  "dev:prepare": "nuxt-module-build build --stub",
45
44
  "typecheck": "vue-tsc --noEmit",
46
45
  "lint": "eslint src",
@@ -48,14 +47,17 @@
48
47
  },
49
48
  "dependencies": {
50
49
  "@internationalized/date": "^3.7.0",
51
- "@mapomodule/store": "2.0.0-beta.1",
52
- "@mapomodule/utils": "2.0.0-beta.1",
50
+ "@mapomodule/i18n": "2.0.0-beta.2",
51
+ "@mapomodule/store": "2.0.0-beta.6",
52
+ "@mapomodule/utils": "2.0.0-beta.5",
53
+ "@tiptap/extension-image": "^2.11.5",
53
54
  "@tiptap/extension-link": "^2.11.5",
54
55
  "@tiptap/extension-underline": "^2.11.5",
55
56
  "@tiptap/starter-kit": "^2.11.5",
56
57
  "@tiptap/vue-3": "^2.11.5",
57
58
  "leaflet": "^1.9.4",
58
- "vue-draggable-plus": "^0.6.0"
59
+ "vue-draggable-plus": "^0.6.0",
60
+ "vue-i18n": "^11.4.0"
59
61
  },
60
62
  "optionalDependencies": {
61
63
  "@types/leaflet": "^1.9.18"
@@ -68,10 +70,10 @@
68
70
  "@nuxt/kit": "4.4.2",
69
71
  "@nuxt/module-builder": "^1.0.2",
70
72
  "@nuxt/schema": "4.4.2",
71
- "nuxt": "^4.4.2",
73
+ "nuxt": "^4.5.1",
72
74
  "vitest": "^3.2.0",
73
- "vue": "^3.5.32",
74
- "vue-router": "^5.0.4",
75
+ "vue": "^3.5.40",
76
+ "vue-router": "^5.2.0",
75
77
  "vue-tsc": "^3.3.4",
76
78
  "zod": "^3.23.8"
77
79
  }
@@ -0,0 +1,193 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "indexable": {
5
+ "type": "boolean",
6
+ "translatable": true,
7
+ "title": "Indexable"
8
+ },
9
+ "ordering": {
10
+ "type": "integer",
11
+ "maximum": 9223372036854775807,
12
+ "format": "int64",
13
+ "minimum": 0,
14
+ "title": "Ordering"
15
+ },
16
+ "is_public": { "type": "boolean", "readOnly": true, "title": "Is public" },
17
+ "meta": { "type": "object", "title": "Meta" },
18
+ "routerlink": { "type": "string", "readOnly": true, "title": "Routerlink" },
19
+ "og_url": {
20
+ "type": "string",
21
+ "nullable": true,
22
+ "maxLength": 200,
23
+ "translatable": true,
24
+ "title": "Og url"
25
+ },
26
+ "tags": {
27
+ "type": "relation",
28
+ "model": "camomilla.Tag",
29
+ "multiple": true,
30
+ "endpoint": "/api/camomilla/tags/",
31
+ "title": "Tags"
32
+ },
33
+ "published_at": {
34
+ "type": "string",
35
+ "format": "date-time",
36
+ "nullable": true,
37
+ "translatable": true,
38
+ "title": "Published at"
39
+ },
40
+ "keywords": {
41
+ "type": "string",
42
+ "nullable": true,
43
+ "maxLength": 200,
44
+ "translatable": true,
45
+ "title": "Keywords"
46
+ },
47
+ "breadcrumbs_title": {
48
+ "type": "string",
49
+ "nullable": true,
50
+ "maxLength": 128,
51
+ "translatable": true,
52
+ "title": "Breadcrumbs title"
53
+ },
54
+ "content": {
55
+ "type": "string",
56
+ "translatable": true,
57
+ "title": "Content",
58
+ "format": "textarea"
59
+ },
60
+ "identifier": {
61
+ "type": "string",
62
+ "nullable": true,
63
+ "maxLength": 200,
64
+ "title": "Identifier"
65
+ },
66
+ "template": {
67
+ "type": "string",
68
+ "nullable": true,
69
+ "maxLength": 500,
70
+ "title": "Template"
71
+ },
72
+ "description": {
73
+ "type": "string",
74
+ "nullable": true,
75
+ "translatable": true,
76
+ "title": "Description",
77
+ "format": "textarea"
78
+ },
79
+ "id": { "type": "integer", "readOnly": true, "title": "ID" },
80
+ "og_title": {
81
+ "type": "string",
82
+ "nullable": true,
83
+ "maxLength": 200,
84
+ "translatable": true,
85
+ "title": "Og title"
86
+ },
87
+ "canonical": {
88
+ "type": "string",
89
+ "nullable": true,
90
+ "maxLength": 200,
91
+ "translatable": true,
92
+ "title": "Canonical"
93
+ },
94
+ "author": {
95
+ "type": "relation",
96
+ "model": "auth.User",
97
+ "multiple": false,
98
+ "endpoint": "/api/camomilla/users/",
99
+ "nullable": true,
100
+ "title": "Author"
101
+ },
102
+ "og_image": {
103
+ "type": "relation",
104
+ "model": "camomilla.Media",
105
+ "multiple": false,
106
+ "endpoint": "/api/camomilla/media/",
107
+ "nullable": true,
108
+ "title": "Og image"
109
+ },
110
+ "parent_page": {
111
+ "type": "relation",
112
+ "model": "camomilla.Article",
113
+ "multiple": false,
114
+ "endpoint": "/api/camomilla/articles/",
115
+ "nullable": true,
116
+ "title": "Parent page"
117
+ },
118
+ "highlight_image": {
119
+ "type": "relation",
120
+ "model": "camomilla.Media",
121
+ "multiple": false,
122
+ "endpoint": "/api/camomilla/media/",
123
+ "nullable": true,
124
+ "title": "Highlight image"
125
+ },
126
+ "template_file": {
127
+ "type": "string",
128
+ "readOnly": true,
129
+ "title": "Template file"
130
+ },
131
+ "date_updated_at": {
132
+ "type": "string",
133
+ "format": "date-time",
134
+ "readOnly": true,
135
+ "title": "Date updated at"
136
+ },
137
+ "breadcrumbs": {
138
+ "type": "string",
139
+ "readOnly": true,
140
+ "title": "Breadcrumbs"
141
+ },
142
+ "og_description": {
143
+ "type": "string",
144
+ "nullable": true,
145
+ "translatable": true,
146
+ "title": "Og description",
147
+ "format": "textarea"
148
+ },
149
+ "template_data": {
150
+ "type": "object",
151
+ "translatable": true,
152
+ "title": "Template data"
153
+ },
154
+ "date_created": {
155
+ "type": "string",
156
+ "format": "date-time",
157
+ "readOnly": true,
158
+ "title": "Date created"
159
+ },
160
+ "autopermalink": {
161
+ "type": "boolean",
162
+ "translatable": true,
163
+ "title": "Autopermalink"
164
+ },
165
+ "title": {
166
+ "type": "string",
167
+ "nullable": true,
168
+ "maxLength": 200,
169
+ "translatable": true,
170
+ "title": "Title"
171
+ },
172
+ "permalink": {
173
+ "type": "string",
174
+ "translatable": true,
175
+ "title": "Permalink"
176
+ },
177
+ "og_type": {
178
+ "type": "string",
179
+ "nullable": true,
180
+ "maxLength": 200,
181
+ "translatable": true,
182
+ "title": "Og type"
183
+ },
184
+ "status": { "type": "string", "readOnly": true, "title": "Status" },
185
+ "deleted_at": {
186
+ "type": "string",
187
+ "format": "date-time",
188
+ "readOnly": true,
189
+ "nullable": true,
190
+ "title": "Deleted at"
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { AnyFieldDescriptor } from "../runtime/types/index.js";
4
+ import { useFormFromSchema } from "../runtime/composables/useFormFromSchema";
5
+ import articleSchema from "./article.schema.json";
6
+
7
+ // Captured from camomilla: FormAutoSchema().map_serializer(ArticleSerializer()).
8
+ // This is the contract between the two repos — if camomilla changes the shape,
9
+ // this fails here rather than as an empty form in someone's browser.
10
+ describe("camomilla OPTIONS schema -> mapo descriptors", () => {
11
+ const fields = useFormFromSchema(articleSchema as never);
12
+ const byKey = Object.fromEntries(fields.map((f) => [f.key, f]));
13
+ // `attrs` is not on every member of the AnyFieldDescriptor union.
14
+ const attrsOf = (f: AnyFieldDescriptor | undefined) =>
15
+ (f as { attrs?: Record<string, unknown> } | undefined)?.attrs;
16
+
17
+ it("produces a descriptor per field, not an empty array", () => {
18
+ expect(fields.length).toBeGreaterThan(20);
19
+ });
20
+
21
+ it("maps m2m relations with a usable endpoint", () => {
22
+ expect(byKey.tags.type).toBe("m2m");
23
+ expect(attrsOf(byKey.tags)?.endpoint).toBe("/api/camomilla/tags/");
24
+ expect(attrsOf(byKey.tags)?.multiple).toBe(true);
25
+ });
26
+
27
+ it("maps fk relations with a usable endpoint", () => {
28
+ expect(byKey.author.type).toBe("fks");
29
+ expect(attrsOf(byKey.author)?.endpoint).toBe("/api/camomilla/users/");
30
+ });
31
+
32
+ it("does not leave any relation with an empty endpoint", () => {
33
+ // An empty endpoint is the silent failure: MapoFksField rewrites "" to "/",
34
+ // fetches the API root, and swallows the error as an empty picker.
35
+ const relations = fields.filter(
36
+ (f) => f.type === "fks" || f.type === "m2m",
37
+ );
38
+ expect(relations.length).toBeGreaterThan(0);
39
+ for (const rel of relations) expect(attrsOf(rel)?.endpoint).toBeTruthy();
40
+ });
41
+
42
+ it("marks server-computed fields readonly rather than dropping them", () => {
43
+ // Without this they rendered as ordinary inputs and the backend discarded
44
+ // whatever was typed, with nothing to notice.
45
+ expect(byKey.id.readonly).toBe(true);
46
+ expect(byKey.status.readonly).toBe(true);
47
+ expect(byKey.date_created.readonly).toBe(true);
48
+ // An editable field must NOT be flagged.
49
+ expect(byKey.title.readonly).toBeUndefined();
50
+ });
51
+
52
+ it("respects the multiline hint for unbounded text", () => {
53
+ // A Django TextField has no maxLength, so the >255 heuristic never fires.
54
+ expect(byKey.content.type).toBe("textarea");
55
+ expect(byKey.description.type).toBe("textarea");
56
+ });
57
+
58
+ it("carries labels", () => {
59
+ expect(byKey.title.label).toBe("Title");
60
+ });
61
+
62
+ it("types scalars correctly", () => {
63
+ expect(byKey.title.type).toBe("text");
64
+ expect(byKey.published_at.type).toBe("datetime");
65
+ });
66
+
67
+ it("has no translations envelope and no localized twins", () => {
68
+ expect(byKey.translations).toBeUndefined();
69
+ expect(fields.filter((f) => /_(it|en)$/.test(String(f.key)))).toHaveLength(
70
+ 0,
71
+ );
72
+ });
73
+ });
@@ -10,6 +10,8 @@ type DescriptorWithAttrs = {
10
10
  };
11
11
  fields?: unknown[];
12
12
  visible?: (ctx: { model: Record<string, unknown> }) => boolean;
13
+ readonly?: boolean;
14
+ translatable?: boolean;
13
15
  };
14
16
 
15
17
  // Pydantic v2 / DRF Spectacular style schema.
@@ -413,3 +415,25 @@ describe("useFormFromSchema — number attrs", () => {
413
415
  expect(typed?.attrs?.max).toBe(5);
414
416
  });
415
417
  });
418
+
419
+ describe("useFormFromSchema — schema flags", () => {
420
+ // Camomilla's OPTIONS schema marks per-language properties. Dropping the flag
421
+ // renders them as plain fields under language tabs that no longer bind them.
422
+ const flagged: JSONSchema = {
423
+ type: "object",
424
+ properties: {
425
+ title: { type: "string", title: "Titolo", translatable: true },
426
+ identifier: { type: "string", title: "Identifier" },
427
+ created_at: { type: "string", format: "date-time", readOnly: true },
428
+ },
429
+ };
430
+
431
+ it("carries translatable and readOnly onto the descriptor", () => {
432
+ const fields = useFormFromSchema(flagged);
433
+ const by = (k: string) =>
434
+ fields.find((f) => f.key === k) as DescriptorWithAttrs | undefined;
435
+ expect(by("title")?.translatable).toBe(true);
436
+ expect(by("identifier")?.translatable).toBeUndefined();
437
+ expect(by("created_at")?.readonly).toBe(true);
438
+ });
439
+ });
@@ -10,6 +10,11 @@ vi.mock("@mapomodule/store/runtime/stores/auth", () => ({
10
10
  vi.mock("@mapomodule/store/runtime/stores/snack", () => ({
11
11
  useSnackStore: () => ({ show: vi.fn() }),
12
12
  }));
13
+ // Pulls in `nuxt/app`, which cannot resolve outside a Nuxt build. Assertions
14
+ // below match on message keys, so echoing the key back is the useful stub.
15
+ vi.mock("@mapomodule/i18n/runtime/composables/useMapoT", () => ({
16
+ useMapoT: () => (key: string) => key,
17
+ }));
13
18
 
14
19
  // provide/inject called outside setup() emits a warning; silence it in tests.
15
20
  beforeEach(() => {
package/src/index.ts CHANGED
@@ -42,7 +42,12 @@ export type {
42
42
  MapDescriptor,
43
43
  RepeaterDescriptor,
44
44
  MediaDescriptor,
45
+ MediaM2mDescriptor,
46
+ EnhancedMediaDescriptor,
45
47
  CustomDescriptor,
48
+ MediaLink,
49
+ MediaItem,
50
+ EnhancedMediaValue,
46
51
  FieldComponentEntry,
47
52
  FieldRegistry,
48
53
  PartialFieldRegistry,
package/src/module.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  createResolver,
7
7
  addImports,
8
8
  extendPages,
9
+ hasNuxtModule,
10
+ installModule,
9
11
  } from "@nuxt/kit";
10
12
  import type { NuxtModule } from "@nuxt/schema";
11
13
  import type { PartialFieldRegistry } from "./runtime/types/index.js";
@@ -30,6 +32,23 @@ export default defineNuxtModule<MapoFormOptions>({
30
32
  async setup(options, nuxt) {
31
33
  const resolver = createResolver(import.meta.url);
32
34
 
35
+ // In consuming apps this runtime lives inside node_modules, where Nuxt
36
+ // skips auto-import injection by default (workspace apps are unaffected:
37
+ // pnpm symlinks resolve outside node_modules). Opt back in and transpile
38
+ // the runtime for the SSR build.
39
+ nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
40
+ nuxt.options.imports.transform ??= {};
41
+ (nuxt.options.imports.transform.include ??= []).push(
42
+ /@mapomodule[\\/]form[\\/]/,
43
+ );
44
+
45
+ // Form components render user-facing strings through vue-i18n
46
+ // (`useI18n().t("mapo...")`). @mapomodule/i18n provides the catalogs and
47
+ // installs @nuxtjs/i18n unless the app manages it itself.
48
+ if (!hasNuxtModule("@mapomodule/i18n")) {
49
+ await installModule(await resolver.resolvePath("@mapomodule/i18n"));
50
+ }
51
+
33
52
  // Only serializable values can go into runtimeConfig.public.
34
53
  // `mapping` and `accessor` contain functions → not SSR-serializable.
35
54
  // To extend mapping/accessor, use an app plugin that accesses $mapoFormRegistry:
@@ -111,6 +130,21 @@ export {};
111
130
  });
112
131
  }
113
132
 
133
+ // Media field components — registered so consumers can use them directly,
134
+ // and so MapoMediaUnavailable resolves inside them.
135
+ const mediaFieldComponents = [
136
+ "MapoMediaField",
137
+ "MapoMediaM2mField",
138
+ "MapoEnhancedMediaField",
139
+ "MapoMediaUnavailable",
140
+ ];
141
+ for (const name of mediaFieldComponents) {
142
+ addComponent({
143
+ name,
144
+ filePath: resolver.resolve(`./runtime/components/fields/${name}.vue`),
145
+ });
146
+ }
147
+
114
148
  // Public auto-imported composables
115
149
  addImports([
116
150
  {
@@ -11,9 +11,11 @@
11
11
  */
12
12
  import { computed, ref, onMounted, onUnmounted } from "vue";
13
13
  import { watch } from "vue";
14
+ import { useI18n } from "vue-i18n";
14
15
  import { useFocusMode } from "../composables/useFocusMode.js";
15
16
  import MapoForm from "./MapoForm.vue";
16
17
 
18
+ const { t } = useI18n();
17
19
  const { focusTarget, exit } = useFocusMode();
18
20
 
19
21
  const isOpen = computed(() => focusTarget.value !== null);
@@ -88,7 +90,7 @@ function onUpdate(val: Record<string, unknown>) {
88
90
  variant="ghost"
89
91
  color="neutral"
90
92
  icon="i-lucide-x"
91
- label="Close"
93
+ :label="t('mapo.close')"
92
94
  size="sm"
93
95
  @click="exit"
94
96
  />
@@ -1,10 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import type { TabEntry } from "../types/layout.js";
4
5
  import { injectMapoForm } from "../composables/useMapoForm.js";
5
6
  import MapoFormGroup from "./MapoFormGroup.vue";
6
7
  import MapoFormFlatSection from "./MapoFormFlatSection.vue";
7
8
 
9
+ const { t } = useI18n();
10
+
8
11
  const props = defineProps<{ tabs: TabEntry[] }>();
9
12
 
10
13
  defineSlots<Record<string, unknown>>();
@@ -54,7 +57,7 @@ const tabsWithErrors = computed(() => {
54
57
  <span
55
58
  v-if="tabsWithErrors.has(tab.name)"
56
59
  class="inline-block h-1.5 w-1.5 rounded-full bg-red-500"
57
- aria-label="This tab contains errors"
60
+ :aria-label="t('mapo.form.tabErrors')"
58
61
  />
59
62
  </button>
60
63
  <!-- Extra content appended to the tab bar (actions, badges, …). -->
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { parseDate, CalendarDate } from "@internationalized/date";
4
5
  import type { DateDescriptor } from "../../types/index.js";
5
6
 
@@ -14,6 +15,7 @@ const props = defineProps<{
14
15
 
15
16
  const emit = defineEmits<{ "update:modelValue": [value: string | null] }>();
16
17
 
18
+ const { t } = useI18n();
17
19
  const open = ref(false);
18
20
 
19
21
  /** Parsed calendar value derived from the current ISO-like model string. */
@@ -30,7 +32,7 @@ const calendarValue = computed<CalendarDate | undefined>(() => {
30
32
 
31
33
  const displayLabel = computed(() => {
32
34
  const v = calendarValue.value;
33
- if (!v) return "Select date";
35
+ if (!v) return t("mapo.dateField.selectDate");
34
36
  return `${String(v.day).padStart(2, "0")}/${String(v.month).padStart(2, "0")}/${v.year}`;
35
37
  });
36
38
 
@@ -6,6 +6,7 @@ import {
6
6
  today,
7
7
  getLocalTimeZone,
8
8
  } from "@internationalized/date";
9
+ import { useI18n } from "vue-i18n";
9
10
  import type { DateDescriptor } from "../../types/index.js";
10
11
 
11
12
  /** Datetime field with configurable naive/UTC serialisation semantics. */
@@ -19,6 +20,7 @@ const props = defineProps<{
19
20
 
20
21
  const emit = defineEmits<{ "update:modelValue": [value: string | null] }>();
21
22
 
23
+ const { t } = useI18n();
22
24
  const open = ref(false);
23
25
 
24
26
  // Timezone strategy configurable through `descriptor.attrs.tz`:
@@ -72,7 +74,7 @@ function emitFromCalendar(v: CalendarDateTime) {
72
74
  // Displayed label in the trigger button
73
75
  const displayLabel = computed(() => {
74
76
  const v = calendarValue.value;
75
- if (!v) return "Select date and time";
77
+ if (!v) return t("mapo.dateField.selectDateTime");
76
78
  const date = `${String(v.day).padStart(2, "0")}/${String(v.month).padStart(2, "0")}/${v.year}`;
77
79
  const time = `${String(v.hour).padStart(2, "0")}:${String(v.minute).padStart(2, "0")}`;
78
80
  return `${date} ${time}`;