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

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 (202) hide show
  1. package/README.md +37 -118
  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__/tabVisibility.test.d.ts +2 -0
  9. package/dist/__tests__/tabVisibility.test.d.ts.map +1 -0
  10. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  11. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  12. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  13. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  14. package/dist/index.d.mts +9 -1
  15. package/dist/index.d.ts +15 -1
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.mjs +7 -34
  18. package/dist/module.d.mts +20 -2
  19. package/dist/module.d.ts +12 -2
  20. package/dist/module.d.ts.map +1 -0
  21. package/dist/module.json +1 -1
  22. package/dist/module.mjs +187 -13
  23. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  24. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  25. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  26. package/dist/runtime/components/MapoForm.vue +177 -0
  27. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  28. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  29. package/dist/runtime/components/MapoFormField.vue +269 -0
  30. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  31. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  33. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  34. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  35. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  36. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  37. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  38. package/dist/runtime/components/MapoFormTabs.vue +122 -0
  39. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  40. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  41. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  42. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  43. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  45. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  46. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  48. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  49. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  51. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  52. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  54. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  55. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  57. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  58. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  60. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  61. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  63. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  64. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue +93 -0
  66. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  67. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue +111 -0
  69. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  70. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  72. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  73. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  75. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  76. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  78. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  79. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  81. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  82. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  84. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  85. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue +258 -0
  87. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  88. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  90. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  91. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  93. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  94. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  96. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  97. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  99. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  100. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  102. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  103. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  105. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  106. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  108. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  109. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  110. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  111. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  112. package/dist/runtime/composables/defineFormField.js +27 -0
  113. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  114. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  115. package/dist/runtime/composables/useCurrentLang.js +9 -0
  116. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  117. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  118. package/dist/runtime/composables/useFocusMode.js +31 -0
  119. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  120. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  121. package/dist/runtime/composables/useFormDraft.js +52 -0
  122. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  123. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  124. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  125. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  126. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  127. package/dist/runtime/composables/useMapoForm.js +325 -0
  128. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  129. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  130. package/dist/runtime/composables/useMediaManager.js +6 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  132. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  133. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  135. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  136. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  138. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  139. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  141. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  142. package/dist/runtime/plugins/form-registry.js +19 -0
  143. package/dist/runtime/registry/defaults.d.ts +3 -0
  144. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  145. package/dist/runtime/registry/defaults.js +93 -0
  146. package/dist/runtime/types/descriptor.d.ts +373 -0
  147. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  148. package/dist/runtime/types/descriptor.js +26 -0
  149. package/dist/runtime/types/index.d.ts +12 -0
  150. package/dist/runtime/types/index.d.ts.map +1 -0
  151. package/dist/runtime/types/index.js +6 -0
  152. package/dist/runtime/types/layout.d.ts +21 -0
  153. package/dist/runtime/types/layout.d.ts.map +1 -0
  154. package/dist/runtime/types/layout.js +0 -0
  155. package/dist/runtime/types/media.d.ts +43 -0
  156. package/dist/runtime/types/media.d.ts.map +1 -0
  157. package/dist/runtime/types/media.js +0 -0
  158. package/dist/runtime/types/registry.d.ts +62 -0
  159. package/dist/runtime/types/registry.d.ts.map +1 -0
  160. package/dist/runtime/types/registry.js +19 -0
  161. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  162. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  163. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  164. package/dist/runtime/utils/tabVisibility.d.ts +18 -0
  165. package/dist/runtime/utils/tabVisibility.d.ts.map +1 -0
  166. package/dist/runtime/utils/tabVisibility.js +18 -0
  167. package/dist/types.d.mts +6 -10
  168. package/package.json +10 -8
  169. package/src/__tests__/article.schema.json +193 -0
  170. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  171. package/src/__tests__/tabVisibility.test.ts +102 -0
  172. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  173. package/src/__tests__/useMapoForm.test.ts +5 -0
  174. package/src/index.ts +5 -0
  175. package/src/module.ts +34 -0
  176. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  177. package/src/runtime/components/MapoForm.vue +17 -4
  178. package/src/runtime/components/MapoFormTabs.vue +29 -6
  179. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  180. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  181. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  182. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  183. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  184. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  185. package/src/runtime/components/fields/MapoMediaField.vue +106 -0
  186. package/src/runtime/components/fields/MapoMediaM2mField.vue +131 -0
  187. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  188. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  189. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  190. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  191. package/src/runtime/components/fields/MapoWygEditor.vue +55 -7
  192. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  193. package/src/runtime/composables/useMapoForm.ts +4 -2
  194. package/src/runtime/composables/useMediaManager.ts +21 -0
  195. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  196. package/src/runtime/registry/defaults.ts +9 -1
  197. package/src/runtime/types/descriptor.ts +22 -7
  198. package/src/runtime/types/index.ts +4 -0
  199. package/src/runtime/types/media.ts +44 -0
  200. package/src/runtime/utils/tabVisibility.ts +44 -0
  201. package/dist/index.d.cts +0 -1
  202. 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
+ }
@@ -0,0 +1,18 @@
1
+ import type { AnyFieldDescriptor } from "../types/index.js";
2
+ import type { TabEntry } from "../types/layout.js";
3
+ /**
4
+ * Whether a field shows for this model: neither its `visible` predicate, which
5
+ * removes it, nor its `show` predicate, which hides it, keeps it out.
6
+ */
7
+ export declare function isFieldShown(field: AnyFieldDescriptor, model: Record<string, unknown>): boolean;
8
+ /**
9
+ * Whether a tab has a field to show for this model, in one of its groups (sub-tabs
10
+ * included) or in one of its child tabs.
11
+ */
12
+ export declare function tabHasShownFields(tab: TabEntry, model: Record<string, unknown>): boolean;
13
+ /**
14
+ * The tab to open among the ones shown: the tab the user picked, while it is
15
+ * shown; otherwise the first one. Empty when no tab is shown.
16
+ */
17
+ export declare function pickActiveTab(shownTabs: TabEntry[], picked: string | null): string;
18
+ //# sourceMappingURL=tabVisibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabVisibility.d.ts","sourceRoot":"","sources":["../../../src/runtime/utils/tabVisibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAQT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,QAAQ,EAAE,EACrB,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,MAAM,CAGR"}
@@ -0,0 +1,18 @@
1
+ export function isFieldShown(field, model) {
2
+ const removed = field.visible ? !field.visible({ model }) : false;
3
+ const hidden = field.show ? !field.show({ model }) : false;
4
+ return !removed && !hidden;
5
+ }
6
+ export function tabHasShownFields(tab, model) {
7
+ for (const group of tab.groups.values()) {
8
+ const fields = [...group.fields, ...[...group.subtabs.values()].flat()];
9
+ if (fields.some((field) => isFieldShown(field, model))) return true;
10
+ }
11
+ return [...tab.children.values()].some(
12
+ (child) => tabHasShownFields(child, model)
13
+ );
14
+ }
15
+ export function pickActiveTab(shownTabs, picked) {
16
+ const pickedTab = shownTabs.find((tab) => tab.name === picked);
17
+ return (pickedTab ?? shownTabs[0])?.name ?? "";
18
+ }
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.11",
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.3",
51
+ "@mapomodule/store": "2.0.0-beta.6",
52
+ "@mapomodule/utils": "2.0.0-beta.6",
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.8"
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
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AnyFieldDescriptor } from "../runtime/types/index.js";
3
+ import type { TabEntry } from "../runtime/types/layout.js";
4
+ import {
5
+ isFieldShown,
6
+ pickActiveTab,
7
+ tabHasShownFields,
8
+ } from "../runtime/utils/tabVisibility.js";
9
+
10
+ type Predicates = Pick<AnyFieldDescriptor, "visible" | "show">;
11
+
12
+ const field = (key: string, predicates: Predicates = {}) =>
13
+ ({ key, type: "text", ...predicates }) as AnyFieldDescriptor;
14
+
15
+ /** A tab holding its fields in one flat group, as MapoForm builds it. */
16
+ const tab = (
17
+ name: string,
18
+ fields: AnyFieldDescriptor[],
19
+ children: TabEntry[] = [],
20
+ ): TabEntry => ({
21
+ name,
22
+ groups: new Map([["__flat__", { fields, subtabs: new Map() }]]),
23
+ children: new Map(children.map((child) => [child.name, child])),
24
+ });
25
+
26
+ // Like a page whose content fields depend on the template picked in the sidebar.
27
+ const hasTemplate = ({ model }: { model: Record<string, unknown> }) =>
28
+ Boolean(model.template);
29
+
30
+ describe("isFieldShown", () => {
31
+ it("shows a field without predicates", () => {
32
+ expect(isFieldShown(field("title"), {})).toBe(true);
33
+ });
34
+
35
+ it("hides a field that `visible` removes", () => {
36
+ const body = field("body", { visible: hasTemplate });
37
+ expect(isFieldShown(body, { template: "" })).toBe(false);
38
+ expect(isFieldShown(body, { template: "Home" })).toBe(true);
39
+ });
40
+
41
+ it("hides a field that `show` hides", () => {
42
+ const body = field("body", { show: hasTemplate });
43
+ expect(isFieldShown(body, { template: "" })).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe("tabHasShownFields", () => {
48
+ it("is false while every field is hidden and true once one shows", () => {
49
+ const general = tab("__default__", [
50
+ field("body", { visible: hasTemplate }),
51
+ field("intro", { visible: hasTemplate }),
52
+ ]);
53
+ expect(tabHasShownFields(general, { template: "" })).toBe(false);
54
+ expect(tabHasShownFields(general, { template: "Home" })).toBe(true);
55
+ });
56
+
57
+ it("counts the fields of a sub-tab", () => {
58
+ const seo: TabEntry = {
59
+ name: "SEO",
60
+ groups: new Map([
61
+ [
62
+ "meta",
63
+ { fields: [], subtabs: new Map([["basic", [field("meta_title")]]]) },
64
+ ],
65
+ ]),
66
+ children: new Map(),
67
+ };
68
+ expect(tabHasShownFields(seo, {})).toBe(true);
69
+ });
70
+
71
+ it("counts the fields of a child tab", () => {
72
+ const settings = tab(
73
+ "Settings",
74
+ [field("legacy", { visible: () => false })],
75
+ [tab("SEO", [field("meta_title")])],
76
+ );
77
+ expect(tabHasShownFields(settings, {})).toBe(true);
78
+ });
79
+ });
80
+
81
+ describe("pickActiveTab", () => {
82
+ const general = tab("__default__", []);
83
+ const seo = tab("SEO", []);
84
+
85
+ it("opens the first shown tab until the user picks one", () => {
86
+ expect(pickActiveTab([general, seo], null)).toBe("__default__");
87
+ // The first tab is hidden: the next one opens, not an empty panel.
88
+ expect(pickActiveTab([seo], null)).toBe("SEO");
89
+ });
90
+
91
+ it("keeps the picked tab while it is shown", () => {
92
+ expect(pickActiveTab([general, seo], "SEO")).toBe("SEO");
93
+ });
94
+
95
+ it("falls back to the first shown tab while the picked one is hidden", () => {
96
+ expect(pickActiveTab([seo], "__default__")).toBe("SEO");
97
+ });
98
+
99
+ it("is empty when no tab is shown", () => {
100
+ expect(pickActiveTab([], null)).toBe("");
101
+ });
102
+ });
@@ -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
  />