@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.
- package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -1
- package/dist/runtime/composables/useFormFromSchema.js +2 -0
- package/package.json +2 -2
- package/src/__tests__/article.schema.json +193 -0
- package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
- package/src/runtime/composables/useFormFromSchema.ts +3 -1
|
@@ -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 +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;
|
|
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.
|
|
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.
|
|
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
|
|
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
|
};
|