@sealcode/sealgen 0.15.35 → 0.15.36

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 (85) hide show
  1. package/.xunit +59 -57
  2. package/@types/src/forms/controls/form-control.d.ts +2 -1
  3. package/@types/src/forms/controls/table.d.ts +31 -4
  4. package/@types/src/forms/fields/table.d.ts +4 -2
  5. package/@types/src/templates/form.d.ts +5 -0
  6. package/@types/src/templates/shared/collection-create-form.d.ts +0 -2
  7. package/@types/src/templates/shared/handlers/boolean.d.ts +2 -0
  8. package/@types/src/templates/shared/handlers/date.d.ts +2 -0
  9. package/@types/src/templates/shared/handlers/deep-reverse-single-reference.d.ts +2 -0
  10. package/@types/src/templates/shared/handlers/default.d.ts +2 -0
  11. package/@types/src/templates/shared/handlers/enum.d.ts +2 -0
  12. package/@types/src/templates/shared/handlers/file.d.ts +2 -0
  13. package/@types/src/templates/shared/handlers/image.d.ts +2 -0
  14. package/@types/src/templates/shared/handlers/int.d.ts +2 -0
  15. package/@types/src/templates/shared/handlers/jdd.d.ts +2 -0
  16. package/@types/src/templates/shared/handlers/single-reference.d.ts +2 -0
  17. package/@types/src/templates/shared/handlers/structured-array.d.ts +2 -0
  18. package/@types/src/templates/shared/shared-crud-form-fields.d.ts +29 -6
  19. package/@types/src/utils/extract-fields-from-collection.d.ts +12 -2
  20. package/@types/src/utils/with-fallback.d.ts +1 -1
  21. package/lib/add-crud.js +5 -5
  22. package/lib/cli.js +2 -0
  23. package/lib/forms/controls/form-control.js +2 -2
  24. package/lib/forms/controls/multiple-photos.js +2 -1
  25. package/lib/forms/controls/single-reference.test.js +1 -1
  26. package/lib/forms/controls/table.js +114 -12
  27. package/lib/forms/fields/boolean.js +1 -1
  28. package/lib/forms/fields/field.js +4 -1
  29. package/lib/forms/fields/table.js +8 -3
  30. package/lib/forms/form.js +7 -6
  31. package/lib/page/default-list-filters.js +1 -0
  32. package/lib/templates/form.js +25 -16
  33. package/lib/templates/list.js +6 -167
  34. package/lib/templates/shared/collection-create-form.js +23 -83
  35. package/lib/templates/shared/collection-edit-form.js +53 -96
  36. package/lib/templates/shared/collection-list.js +21 -6
  37. package/lib/templates/shared/confirm-item-delete.js +7 -5
  38. package/lib/templates/shared/handlers/boolean.js +14 -0
  39. package/lib/templates/shared/handlers/date.js +8 -0
  40. package/lib/templates/shared/handlers/deep-reverse-single-reference.js +209 -0
  41. package/lib/templates/shared/handlers/default.js +50 -0
  42. package/lib/templates/shared/handlers/enum.js +19 -0
  43. package/lib/templates/shared/handlers/file.js +18 -0
  44. package/lib/templates/shared/handlers/image.js +32 -0
  45. package/lib/templates/shared/handlers/int.js +11 -0
  46. package/lib/templates/shared/handlers/jdd.js +10 -0
  47. package/lib/templates/shared/handlers/single-reference.js +12 -0
  48. package/lib/templates/shared/handlers/structured-array.js +33 -0
  49. package/lib/templates/shared/item-delete.js +5 -3
  50. package/lib/templates/shared/shared-crud-form-fields.js +72 -82
  51. package/lib/utils/extract-fields-from-collection.js +31 -5
  52. package/package.json +2 -1
  53. package/src/add-crud.ts +153 -0
  54. package/src/cli.ts +2 -0
  55. package/src/forms/controls/form-control.ts +3 -2
  56. package/src/forms/controls/multiple-photos.ts +1 -0
  57. package/src/forms/controls/single-reference.test.ts +1 -1
  58. package/src/forms/controls/table.ts +160 -30
  59. package/src/forms/fields/boolean.ts +1 -1
  60. package/src/forms/fields/field.ts +4 -1
  61. package/src/forms/fields/table.ts +25 -6
  62. package/src/forms/form.ts +13 -7
  63. package/src/page/default-list-filters.ts +1 -0
  64. package/src/templates/form.ts +24 -15
  65. package/src/templates/list.ts +6 -174
  66. package/src/templates/shared/collection-create-form.ts +121 -0
  67. package/src/templates/shared/collection-edit-form.ts +196 -0
  68. package/src/templates/shared/collection-list.ts +252 -0
  69. package/src/templates/shared/confirm-item-delete.ts +78 -0
  70. package/src/templates/shared/handlers/boolean.ts +18 -0
  71. package/src/templates/shared/handlers/date.ts +12 -0
  72. package/src/templates/shared/handlers/deep-reverse-single-reference.ts +244 -0
  73. package/src/templates/shared/handlers/default.ts +58 -0
  74. package/src/templates/shared/handlers/enum.ts +23 -0
  75. package/src/templates/shared/handlers/file.ts +22 -0
  76. package/src/templates/shared/handlers/image.ts +45 -0
  77. package/src/templates/shared/handlers/int.ts +15 -0
  78. package/src/templates/shared/handlers/jdd.ts +14 -0
  79. package/src/templates/shared/handlers/single-reference.ts +16 -0
  80. package/src/templates/shared/handlers/structured-array.ts +42 -0
  81. package/src/templates/shared/item-delete.ts +45 -0
  82. package/src/templates/shared/shared-crud-form-fields.ts +179 -0
  83. package/src/utils/extract-fields-from-collection.ts +45 -6
  84. package/src/utils/with-fallback.ts +1 -1
  85. package/src/utils/write-file.ts +8 -0
@@ -0,0 +1,11 @@
1
+ const int_handler = async (_, { name }, __, { is_required_expr, fields_var }) => ({
2
+ field: `${name}: new Fields.NumberField(${is_required_expr}, 0)`,
3
+ controls: `new Controls.Number(${fields_var}.${name}, { label: "${name}" })`,
4
+ initial_value_edit: {
5
+ parsed_value: `String(item.get("${name}"))`
6
+ }
7
+ });
8
+ export {
9
+ int_handler
10
+ };
11
+ //# sourceMappingURL=int.js.map
@@ -0,0 +1,10 @@
1
+ const jdd_handler = async (_, { name }) => ({
2
+ field: ``,
3
+ initial_value_edit: "",
4
+ controls: "",
5
+ sealious_value: () => `${name}: []`
6
+ });
7
+ export {
8
+ jdd_handler
9
+ };
10
+ //# sourceMappingURL=jdd.js.map
@@ -0,0 +1,12 @@
1
+ const single_reference_handler = async (_, { name }, __, { fields_var }) => {
2
+ return {
3
+ controls: `new Controls.SingleReferenceDropdown(${fields_var}.${name}, {
4
+ label: "${name}",
5
+ getLabel: (item)=>item.id
6
+ })`
7
+ };
8
+ };
9
+ export {
10
+ single_reference_handler
11
+ };
12
+ //# sourceMappingURL=single-reference.js.map
@@ -0,0 +1,33 @@
1
+ const structured_array_handler = async (_, { name, subfields }, __, { fields_var }) => ({
2
+ field: `${name}: new Fields.Table({
3
+ ${subfields.map(
4
+ (subfield) => `${subfield.name}: new Fields.SimpleFormField(true)`
5
+ ).join(",\n")}
6
+ })`,
7
+ controls: `new Controls.Table(${fields_var}.${name}, {
8
+ label: "${name}",
9
+ render_fields: {
10
+ ${subfields.map(
11
+ ({
12
+ name: name2
13
+ }) => `"${name2}": (fctx, name, value) => /* HTML */ \`<input
14
+ name="\${name}"
15
+ value="\${value}"
16
+ form="\${fctx.form_id}"
17
+ autocomplete="off"
18
+ />\``
19
+ ).join(",\n")}
20
+
21
+ },
22
+ allow_adding: true,
23
+ allow_removing: true,
24
+ make_new_row: async () => ({
25
+ ${subfields.map(({ name: name2 }) => `"${name2}": ""`).join(",\n")}
26
+ }),
27
+ })`,
28
+ fallback_value_sealious: `[]`
29
+ });
30
+ export {
31
+ structured_array_handler
32
+ };
33
+ //# sourceMappingURL=structured-array.js.map
@@ -1,10 +1,11 @@
1
1
  import { toKebabCase, toPascalCase } from "js-convert-case";
2
2
  import { curryImportPath } from "../../utils/import-path.js";
3
+ import { formatWithPrettier } from "../../utils/prettier.js";
3
4
  function itemDeleteTemplate(delete_action_name, collection_name, newfilefullpath, list_action_name) {
4
5
  const importPath = curryImportPath(newfilefullpath);
5
- return `import { Context } from "koa";
6
+ const result = `import type { Context } from "koa";
6
7
  import { Mountable } from "@sealcode/sealgen";
7
- import Router from "@koa/router";
8
+ import type Router from "@koa/router";
8
9
 
9
10
  import { ${toPascalCase(collection_name)} } from "${importPath(
10
11
  "src/back/collections/collections.js"
@@ -25,7 +26,7 @@ export default new (class ${delete_action_name}Redirect extends Mountable {
25
26
 
26
27
  mount(router: Router, path: string) {
27
28
  router.get(path, async (ctx) => {
28
- ctx.$app.collections["${toKebabCase(
29
+ await ctx.$app.collections["${toKebabCase(
29
30
  collection_name
30
31
  )}"].removeByID(ctx.$context, ctx.params.id);
31
32
  ctx.status = 302;
@@ -34,6 +35,7 @@ export default new (class ${delete_action_name}Redirect extends Mountable {
34
35
  }
35
36
  })();
36
37
  `;
38
+ return formatWithPrettier(result);
37
39
  }
38
40
  export {
39
41
  itemDeleteTemplate
@@ -4,82 +4,53 @@ import extract_fields_from_collection from "../../utils/extract-fields-from-coll
4
4
  import _locreq from "locreq";
5
5
  import { curryImportPath } from "../../utils/import-path.js";
6
6
  import { formatWithPrettier } from "../../utils/prettier.js";
7
+ import { default_handler } from "./handlers/default.js";
8
+ import { enum_handler } from "./handlers/enum.js";
9
+ import { image_handler } from "./handlers/image.js";
10
+ import { file_handler } from "./handlers/file.js";
11
+ import { int_handler } from "./handlers/int.js";
12
+ import { boolean_handler } from "./handlers/boolean.js";
13
+ import { jdd_handler } from "./handlers/jdd.js";
14
+ import { date_handler } from "./handlers/date.js";
15
+ import { deep_reverse_single_reference_handler } from "./handlers/deep-reverse-single-reference.js";
16
+ import { structured_array_handler } from "./handlers/structured-array.js";
7
17
  const target_locreq = _locreq(process.cwd());
8
- function renderFieldImports(field) {
9
- if (field.type == "enum") {
10
- return `import type { FieldTypes } from "sealious";`;
11
- }
12
- }
13
- function renderFormField(collection_name, { name, type, target, is_readonly }) {
14
- const required_s = `${toPascalCase(
15
- collection_name
16
- )}.fields.${name}.required`;
17
- if (type == "image" || type == "file") {
18
- return `${name}: new Fields.File(${required_s}, TheFileManager)`;
19
- }
20
- if (type == "int") {
21
- return `${name}: new Fields.NumberField(${required_s}, 0)`;
22
- }
23
- if (type == "boolean") {
24
- return `${name}: new Fields.Boolean(${required_s})`;
25
- }
26
- if (type == "enum") {
27
- return `${name}: new Fields.PickFromListField(
28
- ${toPascalCase(collection_name)}.fields["${name}"].required,
29
- async (ctx) =>
30
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
- (${toPascalCase(collection_name)}.fields["${name}"] as FieldTypes.Enum<string>)
32
- .getAllowedValues(ctx.$app)
33
- .map((value) => ({ value, label: value }))
34
- )`;
35
- }
36
- if (is_readonly || type == "jdd") {
37
- return "";
38
- }
39
- if (type == "deep-reverse-single-reference" && target) {
40
- return `${name}: new Fields.CheckboxedListField(${required_s}, async (ctx) => {
41
- const { items } = await ctx.$app.collections["${target}"]
42
- .list(ctx.$context)
43
- .fetch();
44
-
45
- return items.map((s) => ({
46
- label: s.get("name"),
47
- value: s.id,
48
- }));
49
- })`;
50
- }
51
- return `${name}: new Fields.CollectionField(${required_s}, ${toPascalCase(
52
- collection_name
53
- )}.fields.${name})`;
54
- }
55
- function renderFormControl(collection_name, { name, type, target, is_readonly }) {
56
- const fields_var = `${toPascalCase(collection_name)}FormFields`;
57
- if (is_readonly || type == "jdd") {
58
- return "";
59
- }
60
- if (type == "int") {
61
- return `new Controls.Number(${fields_var}.${name}, { label: "${name}" })`;
62
- }
63
- if (type == "deep-reverse-single-reference" && target) {
64
- return `new Controls.CheckboxedListInput(${fields_var}.${name}, {
65
- label: "${name}",
66
- })`;
67
- }
68
- if (type == "boolean") {
69
- return `new Controls.Checkbox(${fields_var}.${name}, { label: "${name}" })`;
70
- }
71
- if (type == "date") {
72
- return `new Controls.SimpleInput(${fields_var}.${name}, { label: "${name}", type: "date"})`;
73
- }
74
- if (type == "enum") {
75
- return `new Controls.Dropdown(${fields_var}.${name}, { label: "${name}"})`;
76
- }
77
- if (type == "image") {
78
- return `new Controls.Photo(${fields_var}.${name}, imageRouter, {
79
- label: "${name}",
80
- })`;
81
- }
82
- return `new Controls.SimpleInput(${fields_var}.${name}, { label: "${name}"})`;
18
+ const field_handlers = {
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
20
+ default: default_handler,
21
+ enum: enum_handler,
22
+ image: image_handler,
23
+ file: file_handler,
24
+ int: int_handler,
25
+ boolean: boolean_handler,
26
+ jdd: jdd_handler,
27
+ date: date_handler,
28
+ "deep-reverse-single-reference": deep_reverse_single_reference_handler,
29
+ "structured-array": structured_array_handler
30
+ };
31
+ async function getFieldHandler(collection_name, field, importPath) {
32
+ const defaults = await field_handlers.default(
33
+ collection_name,
34
+ field,
35
+ importPath,
36
+ {}
37
+ );
38
+ const result = {
39
+ ...defaults,
40
+ ...await field_handlers[field.type]?.(
41
+ collection_name,
42
+ field,
43
+ importPath,
44
+ defaults
45
+ ) || {}
46
+ };
47
+ const result_initial_value_edit = result.initial_value_edit;
48
+ result.initial_value_edit = typeof result_initial_value_edit == "string" ? result_initial_value_edit : {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ ...defaults.initial_value_edit,
51
+ ...result_initial_value_edit
52
+ };
53
+ return result;
83
54
  }
84
55
  async function sharedCrudFormFields(collection_name, newfilefullpath) {
85
56
  const [uppercase_collection, collection_fields] = await Promise.all([
@@ -91,28 +62,47 @@ async function sharedCrudFormFields(collection_name, newfilefullpath) {
91
62
  extract_fields_from_collection(collection_name)
92
63
  ]);
93
64
  const importPath = curryImportPath(newfilefullpath);
65
+ const field_handler_results = await Promise.all(
66
+ collection_fields.map(async (field) => {
67
+ return getFieldHandler(collection_name, field, importPath);
68
+ })
69
+ );
94
70
  const result = `import { Controls, Fields } from "@sealcode/sealgen";
95
71
  import { ${uppercase_collection} } from "${importPath(
96
72
  "src/back/collections/collections.js"
97
73
  )}";
98
- import { TheFileManager } from "${importPath("src/back/file-manager.js")}";
99
- import { imageRouter } from "${importPath("src/back/image-router.js")}";
100
74
 
101
75
  ${Array.from(
102
- new Set(collection_fields.map((field) => renderFieldImports(field)))
103
- ).join(";\n")}
76
+ new Set(
77
+ field_handler_results.filter(
78
+ ({ hide_field, hide_control }) => !hide_field && !hide_control
79
+ ).map(({ imports }) => imports.shared || "")
80
+ )
81
+ ).join("\n")}
82
+
104
83
 
105
- export const ${toPascalCase(collection_name)}FormFields = <const>{
106
- ${collection_fields.map((field) => renderFormField(collection_name, field)).filter((e) => e != "").join(",\n")}
84
+
85
+ export const ${toPascalCase(collection_name)}FormFields = <const>
86
+ {
87
+ ${field_handler_results.filter(({ hide_field }) => !hide_field).map(({ field }) => field).filter((e) => e != "").join(",\n")}
107
88
  };
108
89
 
109
90
  export const ${toPascalCase(collection_name)}FormControls = [
110
- ${collection_fields.map((field) => renderFormControl(collection_name, field)).filter((e) => e != "").join(",\n")}
91
+ ${field_handler_results.filter(({ hide_control }) => !hide_control).map(({ controls }) => controls).filter((e) => e != "").join(",\n")}
111
92
  ];
93
+
94
+ ${Array.from(
95
+ new Set(
96
+ field_handler_results.map(({ top_level_shared }) => top_level_shared)
97
+ )
98
+ ).join("\n")}
99
+
112
100
  `;
113
101
  return formatWithPrettier(result);
114
102
  }
115
103
  export {
104
+ field_handlers,
105
+ getFieldHandler,
116
106
  sharedCrudFormFields
117
107
  };
118
108
  //# sourceMappingURL=shared-crud-form-fields.js.map
@@ -3,6 +3,12 @@ import { exec } from "./utils.js";
3
3
  const target_locreq = _locreq(process.cwd());
4
4
  import { promises as fs } from "fs";
5
5
  import { is, predicates } from "@sealcode/ts-predicates";
6
+ const counter = function* () {
7
+ let i = 0;
8
+ while (true) {
9
+ yield i++;
10
+ }
11
+ }();
6
12
  async function extract_fields_from_collection(collection_name) {
7
13
  const extractor_code = `import {default as the_app} from "./app.js";
8
14
  const c = new the_app().collections["${collection_name}"];
@@ -10,15 +16,34 @@ const fields = [];
10
16
  for (const field_name in c.fields){
11
17
  const field = c.fields[field_name];
12
18
  let type = field.typeName;
19
+ let target = null;
20
+ let referencing_collection = null;
21
+ let referencing_field = null;
22
+ let intermediary_field_that_points_there = null;
23
+ let is_readonly = false;
24
+ let is_required = field.required;
25
+ let subfields = [];
13
26
  if(["derived-value", "cached-value"].includes(type)){
14
- type = field.virtual_field.typeName
27
+ type = field.virtual_field.typeName;
28
+ }
29
+ if(["derived-value", "cached-value", "reverse-single-reference"].includes(field.typeName)){
30
+ is_readonly = true;
31
+ }
32
+ if(type == "deep-reverse-single-reference" || type =="reverse-single-reference"){
33
+ target = field.target_collection;
34
+ referencing_collection = field.referencing_collection;
35
+ referencing_field = field.referencing_field;
36
+ intermediary_field_that_points_there = field.intermediary_field_that_points_there
37
+ }
38
+ if( type == "structured-array" ) {
39
+ subfields = Object.keys(field.subfields).map(key=>({name: key}));
15
40
  }
16
- fields.push({name: field_name, type})
41
+ fields.push({ name: field_name, type, target, referencing_collection, referencing_field, intermediary_field_that_points_there, is_readonly, is_required, subfields })
17
42
  }
18
- console.log(JSON.stringify(fields));
43
+ console.log(JSON.stringify(fields));
19
44
  `;
20
45
  const extractor_code_path = target_locreq.resolve(
21
- "dist/back/___extract_fields.js"
46
+ `dist/back/___extract_fields--${counter.next().value || 0}.js`
22
47
  );
23
48
  await fs.writeFile(extractor_code_path, extractor_code);
24
49
  const { stdout } = await exec("node", [extractor_code_path]);
@@ -29,7 +54,8 @@ console.log(JSON.stringify(fields));
29
54
  predicates.array(
30
55
  predicates.shape({
31
56
  name: predicates.string,
32
- type: predicates.string
57
+ type: predicates.string,
58
+ is_readonly: predicates.boolean
33
59
  })
34
60
  )
35
61
  )) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.15.35",
3
+ "version": "0.15.36",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -61,6 +61,7 @@
61
61
  "@koa/router": "^12.0.1",
62
62
  "@sealcode/file-manager": "^1.0.2",
63
63
  "@sealcode/ts-predicates": "^0.4.3",
64
+ "chokidar": "^3.6.0",
64
65
  "deepmerge": "^4.3.1",
65
66
  "esbuild": "^0.20.0",
66
67
  "escape-goat": "^4.0.0",
@@ -0,0 +1,153 @@
1
+ import _locreq from "locreq";
2
+ import { escape_url_params } from "./utils/escape-url-params.js";
3
+ import prompts from "prompts";
4
+ import { toKebabCase, toPascalCase } from "js-convert-case";
5
+ import { listCollections } from "./utils/list-collections.js";
6
+ import { collectionListTemplate } from "./templates/shared/collection-list.js";
7
+ import { writeFile } from "./utils/write-file.js";
8
+ import { importPath } from "./utils/import-path.js";
9
+ import { sharedCrudFormFields } from "./templates/shared/shared-crud-form-fields.js";
10
+ import { generateRoutes } from "./generate-routes.js";
11
+ import { createItemFormTemplate } from "./templates/shared/collection-create-form.js";
12
+ import { editItemFormTemplate } from "./templates/shared/collection-edit-form.js";
13
+ import { askItemDeleteTemplate } from "./templates/shared/confirm-item-delete.js";
14
+ import { itemDeleteTemplate } from "./templates/shared/item-delete.js";
15
+
16
+ export async function addCRUD(
17
+ params: Partial<{ [key in "collection" | "url"]: string }>,
18
+ app_directory: string = process.cwd()
19
+ ) {
20
+ const target_locreq = _locreq(app_directory);
21
+ prompts.override(params);
22
+ const response = await prompts([
23
+ {
24
+ type: "autocomplete",
25
+ name: "collection",
26
+ message:
27
+ "Which sealious collection do you like to add CRUD forms for?",
28
+ choices: (
29
+ await listCollections()
30
+ ).map((collection) => ({
31
+ title: collection,
32
+ value: collection,
33
+ })),
34
+ },
35
+ {
36
+ type: "text",
37
+ name: "url",
38
+ message:
39
+ "Enter a full absolute path for the new route (for example: /admin/users/:id/edit): ",
40
+ validate: (s: string) =>
41
+ s.trim()[0] == "/" ? true : "Should start with a '/'",
42
+ initial: function (_, { collection }: { collection: string }) {
43
+ return "/" + toKebabCase(collection);
44
+ },
45
+ },
46
+ ]);
47
+
48
+ const collection_name = response.collection as string;
49
+ const url = response.url as string;
50
+ const list_action_name = toPascalCase(collection_name) + "CRUDList";
51
+ const create_action_name = toPascalCase(collection_name) + "CRUDCreate";
52
+ const edit_action_name = toPascalCase(collection_name) + "CRUDEdit";
53
+ const delete_ask_action_name =
54
+ toPascalCase(collection_name) + "CRUDDeleteAsk";
55
+ const delete_action_name = toPascalCase(collection_name) + "CRUDDelete";
56
+
57
+ /* create the list endpoint */
58
+ const list_path = target_locreq.resolve(
59
+ `src/back/routes/${escape_url_params(url)}.list.tsx`
60
+ );
61
+ const list_content = await collectionListTemplate(
62
+ collection_name,
63
+ list_action_name,
64
+ list_path,
65
+ {
66
+ post_import_js: `import { ${create_action_name}URL, ${edit_action_name}URL, ${delete_ask_action_name}URL } from "${importPath(
67
+ list_path,
68
+ "src/back/routes/urls.ts"
69
+ )}";`,
70
+ post_header_html: `<a href={${create_action_name}URL}> Create </a>`,
71
+ render_item: (
72
+ collection_name: string
73
+ ) => ` async renderItem(ctx: Context, item: CollectionItem<typeof ${toPascalCase(
74
+ collection_name
75
+ )}>) {
76
+ return <tr>
77
+ {displayFields.map(({ field, format }) => {
78
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
79
+ const value = item.get(field as any);
80
+ return <td>{format ? format(value, item) : value}</td>;
81
+ })}
82
+ <td><div class="sealious-list__actions">
83
+ <a href={${edit_action_name}URL(item.id)}>Edit</a>
84
+ <a href={${delete_ask_action_name}URL(item.id)}>Delete</a>
85
+ </div>
86
+ </td>
87
+ </tr>;
88
+ }`,
89
+ }
90
+ );
91
+ await writeFile(list_path, list_content);
92
+
93
+ /* create the shared fields file */
94
+ const fields_path = target_locreq.resolve(
95
+ `src/back/routes/${escape_url_params(url)}/shared.ts`
96
+ );
97
+ const fields_content = await sharedCrudFormFields(
98
+ collection_name,
99
+ fields_path
100
+ );
101
+ await writeFile(fields_path, fields_content);
102
+
103
+ /* create the create endpoint */
104
+ const create_path = target_locreq.resolve(
105
+ `src/back/routes/${escape_url_params(url)}/create.form.ts`
106
+ );
107
+ const create_content = await createItemFormTemplate(
108
+ create_action_name,
109
+ create_path,
110
+ collection_name,
111
+ list_action_name
112
+ );
113
+ await writeFile(create_path, create_content);
114
+
115
+ /* create the edit endpoint */
116
+ const edit_path = target_locreq.resolve(
117
+ `src/back/routes/${escape_url_params(url)}/[id]/edit.form.ts`
118
+ );
119
+ const edit_content = await editItemFormTemplate(
120
+ edit_action_name,
121
+ edit_path,
122
+ collection_name,
123
+ list_action_name
124
+ );
125
+ await writeFile(edit_path, edit_content);
126
+
127
+ /* create the confirm delete endpoint */
128
+ const delete_ask_path = target_locreq.resolve(
129
+ `src/back/routes/${escape_url_params(url)}/[id]/delete-ask.page.tsx`
130
+ );
131
+ const delete_ask_content = await askItemDeleteTemplate(
132
+ delete_ask_action_name,
133
+ collection_name,
134
+ delete_ask_path,
135
+ list_action_name,
136
+ delete_action_name
137
+ );
138
+ await writeFile(delete_ask_path, delete_ask_content);
139
+
140
+ /* create the actual delete endpoint */
141
+ const delete_path = target_locreq.resolve(
142
+ `src/back/routes/${escape_url_params(url)}/[id]/delete.page.tsx`
143
+ );
144
+ const delete_content = await itemDeleteTemplate(
145
+ delete_action_name,
146
+ collection_name,
147
+ delete_ask_path,
148
+ list_action_name
149
+ );
150
+ await writeFile(delete_path, delete_content);
151
+
152
+ await generateRoutes();
153
+ }
package/src/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { makeEnv } from "./make-env.js";
11
11
  import yargs from "yargs/yargs";
12
12
  import { getFonts } from "./get-fonts.js";
13
13
  import { generateStimulusControllers } from "./generate-stimulus.js";
14
+ import { addCRUD } from "./add-crud.js";
14
15
 
15
16
  const actions: Record<
16
17
  string,
@@ -18,6 +19,7 @@ const actions: Record<
18
19
  > = {
19
20
  "add-collection": addCollection,
20
21
  "add-route": addRoute,
22
+ "add-crud": addCRUD,
21
23
  "generate-collections": generateCollections,
22
24
  "generate-components": generateComponents,
23
25
  "generate-routes": generateRoutes,
@@ -2,6 +2,7 @@ import Router from "@koa/router";
2
2
  import { Context } from "koa";
3
3
  import { FlatTemplatable } from "tempstream";
4
4
  import { FormDataValue, FormMessage } from "../form-types.js";
5
+ import type { Form } from "../form.js";
5
6
 
6
7
  export class FormControlContext {
7
8
  constructor(
@@ -20,6 +21,6 @@ export abstract class FormControl {
20
21
  ): FlatTemplatable | Promise<FlatTemplatable>;
21
22
  abstract role: "input" | "decoration" | "messages" | "submit";
22
23
 
23
- // eslint-disable-next-line @typescript-eslint/no-empty-function
24
- mount(_router: Router) {}
24
+ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any
25
+ mount(_router: Router, _form: Form<any, any>) {}
25
26
  }
@@ -38,6 +38,7 @@ export class MultiplePhotos extends MultipleFiles {
38
38
  ${this.imageRouter.image(path, {
39
39
  resolutions: [150, 300],
40
40
  sizesAttr: "150px",
41
+ alt: "",
41
42
  })}
42
43
  </a>
43
44
  <span class="filename">
@@ -40,7 +40,7 @@ describe("single-reference.test", () => {
40
40
  await test_app.addFile(
41
41
  form_file_path,
42
42
  await formTemplate("SomeForm", form_file_path, {
43
- postimport: `import {Comments} from "../collections/collections.js";`,
43
+ postimport: `import {Comments} from "../collections/collections.js"; import {Fields} from "@sealcode/sealgen";`,
44
44
  fields: `article: new Fields.CollectionField(true, Comments.fields.article),`,
45
45
  controls: `new Controls.SingleReferenceDropdown(fields.article, {getLabel: item=>item.get("title")}),`,
46
46
  validate_method: "",