@mapomodule/form 2.0.0-beta.8 → 2.0.0-beta.9

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=camomillaRoundTrip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camomillaRoundTrip.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/camomillaRoundTrip.test.ts"],"names":[],"mappings":""}
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapomodule/form",
3
3
  "configKey": "mapoForm",
4
- "version": "2.0.0-beta.8",
4
+ "version": "2.0.0-beta.9",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1 +1 @@
1
- {"version":3,"file":"useFormFromSchema.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useFormFromSchema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAa,MAAM,mBAAmB,CAAC;AAsMvE,4EAA4E;AAC5E,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,wBAA6B,GACrC,kBAAkB,EAAE,CA+BtB"}
1
+ {"version":3,"file":"useFormFromSchema.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useFormFromSchema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAa,MAAM,mBAAmB,CAAC;AAwMvE,4EAA4E;AAC5E,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,wBAA6B,GACrC,kBAAkB,EAAE,CA+BtB"}
@@ -4,6 +4,7 @@ function mapType(schema, key) {
4
4
  const t = Array.isArray(schema.type) ? schema.type.find((v) => v !== "null") : schema.type;
5
5
  const fmt = schema.format;
6
6
  if (t === "string") {
7
+ if (fmt === "textarea") return "textarea";
7
8
  if (fmt === "date") return "date";
8
9
  if (fmt === "date-time") return "datetime";
9
10
  if (fmt === "time") return "time";
@@ -104,6 +105,7 @@ function propertyToDescriptor(key, rawSchema, defs, requiredKeys, rules) {
104
105
  type,
105
106
  label: schema.title ?? void 0,
106
107
  required: requiredKeys.has(key),
108
+ ...schema.readOnly ? { readonly: true } : {},
107
109
  ...visible ? { visible } : {},
108
110
  ...Object.keys(attrs).length ? { attrs } : {}
109
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapomodule/form",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.0.0-beta.9",
4
4
  "description": "Mapo declarative typed form engine and field components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  "@internationalized/date": "^3.7.0",
50
50
  "@mapomodule/i18n": "2.0.0-beta.2",
51
51
  "@mapomodule/store": "2.0.0-beta.5",
52
- "@mapomodule/utils": "2.0.0-beta.3",
52
+ "@mapomodule/utils": "2.0.0-beta.4",
53
53
  "@tiptap/extension-image": "^2.11.5",
54
54
  "@tiptap/extension-link": "^2.11.5",
55
55
  "@tiptap/extension-underline": "^2.11.5",
@@ -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
+ });
@@ -11,9 +11,10 @@ function mapType(schema: JSONSchema, key: string): FieldType {
11
11
  const t = Array.isArray(schema.type)
12
12
  ? schema.type.find((v) => v !== "null")
13
13
  : schema.type;
14
- const fmt = schema.format as string | undefined;
14
+ const fmt = schema.format;
15
15
 
16
16
  if (t === "string") {
17
+ if (fmt === "textarea") return "textarea";
17
18
  if (fmt === "date") return "date";
18
19
  if (fmt === "date-time") return "datetime";
19
20
  if (fmt === "time") return "time";
@@ -179,6 +180,7 @@ function propertyToDescriptor(
179
180
  type,
180
181
  label: schema.title ?? undefined,
181
182
  required: requiredKeys.has(key),
183
+ ...(schema.readOnly ? { readonly: true } : {}),
182
184
  ...(visible ? { visible } : {}),
183
185
  ...(Object.keys(attrs).length ? { attrs } : {}),
184
186
  };