@sealcode/sealgen 0.19.4 → 0.19.6

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 (67) hide show
  1. package/@types/forms/fields/field.d.ts +1 -0
  2. package/@types/forms/fields/fields.d.ts +2 -1
  3. package/@types/forms/fields/get-field-for-sealious.d.ts +3 -1
  4. package/@types/forms/fields/hybrid.d.ts +11 -0
  5. package/@types/forms/fields/item-draft.d.ts +20 -0
  6. package/@types/forms/form.d.ts +1 -0
  7. package/@types/forms/multiform.d.ts +2 -2
  8. package/@types/page/list.d.ts +5 -4
  9. package/@types/page/mountable-with-fields.d.ts +9 -2
  10. package/@types/page/mountable.d.ts +1 -1
  11. package/@types/page/page.d.ts +1 -1
  12. package/@types/templates/form.d.ts +2 -2
  13. package/lib/forms/controls/checkboxed-list-input.test.js +16 -12
  14. package/lib/forms/controls/file.test.js +2 -2
  15. package/lib/forms/controls/image.test.js +4 -4
  16. package/lib/forms/controls/single-reference.test.js +2 -2
  17. package/lib/forms/fields/field.js +1 -1
  18. package/lib/forms/fields/field.test.js +9 -6
  19. package/lib/forms/fields/fields.js +2 -1
  20. package/lib/forms/fields/file.test.js +2 -2
  21. package/lib/forms/fields/get-field-for-sealious.js +14 -0
  22. package/lib/forms/fields/hybrid.js +37 -0
  23. package/lib/forms/fields/item-draft.js +34 -0
  24. package/lib/forms/form.js +11 -5
  25. package/lib/forms/form.test.js +9 -9
  26. package/lib/forms/multiform.js +7 -4
  27. package/lib/forms/multiform.test.js +7 -7
  28. package/lib/mount.js +4 -1
  29. package/lib/page/list.js +9 -6
  30. package/lib/page/mountable-with-fields.js +16 -1
  31. package/lib/page/mountable-with-fields.test.js +2 -2
  32. package/lib/page/mountable.js +1 -1
  33. package/lib/page/page.js +1 -1
  34. package/lib/templates/form.js +9 -18
  35. package/lib/templates/shared/collection-create-form.js +8 -8
  36. package/lib/templates/shared/collection-edit-form.js +9 -9
  37. package/lib/templates/shared/collection-list.js +2 -1
  38. package/lib/templates/shared/shared-crud-form-fields.js +4 -4
  39. package/lib/utils/extract-fields-from-collection.js +3 -0
  40. package/package.json +1 -1
  41. package/src/forms/controls/checkboxed-list-input.test.ts +16 -12
  42. package/src/forms/controls/file.test.ts +2 -2
  43. package/src/forms/controls/image.test.ts +4 -4
  44. package/src/forms/controls/single-reference.test.ts +2 -2
  45. package/src/forms/fields/field.test.ts +9 -6
  46. package/src/forms/fields/field.ts +6 -7
  47. package/src/forms/fields/fields.ts +2 -1
  48. package/src/forms/fields/file.test.ts +2 -2
  49. package/src/forms/fields/get-field-for-sealious.ts +66 -100
  50. package/src/forms/fields/hybrid.ts +47 -0
  51. package/src/forms/fields/item-draft.ts +44 -0
  52. package/src/forms/form.test.ts +15 -15
  53. package/src/forms/form.ts +34 -26
  54. package/src/forms/multiform.test.ts +7 -7
  55. package/src/forms/multiform.ts +10 -7
  56. package/src/mount.ts +11 -7
  57. package/src/page/list.ts +33 -28
  58. package/src/page/mountable-with-fields.test.ts +2 -2
  59. package/src/page/mountable-with-fields.ts +26 -5
  60. package/src/page/mountable.ts +1 -1
  61. package/src/page/page.ts +1 -1
  62. package/src/templates/form.ts +9 -18
  63. package/src/templates/shared/collection-create-form.ts +8 -8
  64. package/src/templates/shared/collection-edit-form.ts +9 -9
  65. package/src/templates/shared/collection-list.ts +5 -4
  66. package/src/templates/shared/shared-crud-form-fields.ts +5 -5
  67. package/src/utils/extract-fields-from-collection.ts +3 -0
package/src/page/list.ts CHANGED
@@ -1,15 +1,16 @@
1
- import { Context } from "koa";
2
- import { Templatable, tempstream, FlatTemplatable } from "tempstream";
3
- import { Page } from "./page.js";
4
1
  import { ShapeToType } from "@sealcode/ts-predicates";
2
+ import { Context } from "koa";
3
+ import qs from "qs";
4
+ import { FlatTemplatable, Templatable, tempstream } from "tempstream";
5
5
  import { FormControl } from "../forms/controls/controls.js";
6
6
  import { FormField } from "../forms/fields/field.js";
7
- import { naturalNumbers, UrlWithNewParams } from "../utils/utils.js";
8
- import { makeHiddenInputs } from "../make-hidden-inputs.js";
9
- import qs from "qs";
10
7
  import { NumberField } from "../forms/fields/number.js";
11
8
  import { TextBasedSimpleField } from "../forms/fields/simple-form-field.js";
12
9
  import { FormDataValue } from "../forms/form-types.js";
10
+ import { makeHiddenInputs } from "../make-hidden-inputs.js";
11
+ import { naturalNumbers, UrlWithNewParams } from "../utils/utils.js";
12
+ import { Page } from "./page.js";
13
+ import Router from "@koa/router";
13
14
 
14
15
  export const BasePagePropsShape = <const>{};
15
16
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
@@ -72,8 +73,11 @@ export abstract class ListPage<
72
73
  filterFields: Record<string, FormField> = {};
73
74
  filterControls: FormControl[] = [];
74
75
 
75
- init(): void {
76
- super.init();
76
+ init(path: string, router: Router): void {
77
+ if (this.initialized) {
78
+ return;
79
+ }
80
+ super.init(path, router);
77
81
  for (const [, field] of Object.entries(this.filterFields)) {
78
82
  void field.init();
79
83
  }
@@ -107,9 +111,9 @@ export abstract class ListPage<
107
111
  }
108
112
  const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
109
113
  const items_promise = this.getItems(ctx, page, itemsPerPage, values);
110
- return tempstream`${(items
111
- ? Promise.resolve(items)
112
- : items_promise
114
+ return tempstream`${(items ?
115
+ Promise.resolve(items)
116
+ : items_promise
113
117
  ).then((items) =>
114
118
  items.map((item, index) => this.renderItem(ctx, item, index))
115
119
  )}`;
@@ -128,12 +132,12 @@ export abstract class ListPage<
128
132
 
129
133
  return tempstream /* HTML */ `<div class="list-pagination">
130
134
  <div class="list-pagination__left">
131
- ${page > 1
132
- ? this.renderPageButton(ctx, 1, "Pierwsza strona")
133
- : ""}
134
- ${page > 1
135
- ? this.renderPageButton(ctx, page - 1, "Poprzednia strona")
136
- : ""}
135
+ ${page > 1 ?
136
+ this.renderPageButton(ctx, 1, "Pierwsza strona")
137
+ : ""}
138
+ ${page > 1 ?
139
+ this.renderPageButton(ctx, page - 1, "Poprzednia strona")
140
+ : ""}
137
141
  </div>
138
142
  <div class="list-pagination__center">
139
143
  <select
@@ -157,12 +161,12 @@ export abstract class ListPage<
157
161
  </select>
158
162
  </div>
159
163
  <div class="list-pagination__right">
160
- ${page < totalIems
161
- ? this.renderPageButton(ctx, page + 1, "Następna strona")
162
- : ""}
163
- ${page < totalIems
164
- ? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
165
- : ""}
164
+ ${page < totalIems ?
165
+ this.renderPageButton(ctx, page + 1, "Następna strona")
166
+ : ""}
167
+ ${page < totalIems ?
168
+ this.renderPageButton(ctx, totalIems, "Ostatnia strona")
169
+ : ""}
166
170
  </div>
167
171
  </div>`;
168
172
  }
@@ -248,11 +252,12 @@ export abstract class ListPage<
248
252
  const current_order =
249
253
  current_sort?.field == field ? current_sort.order : null;
250
254
  const order =
251
- current_order === "asc" // cycles the sort order: null → "asc" → "desc" → null → ...
252
- ? "desc"
253
- : current_order === null
254
- ? "asc"
255
- : null;
255
+ (
256
+ current_order === "asc" // cycles the sort order: null → "asc" → "desc" → null → ...
257
+ ) ?
258
+ "desc"
259
+ : current_order === null ? "asc"
260
+ : null;
256
261
  return /* HTML */ `<th data-field="${field}">
257
262
  <a href="${this.makeSortLink(ctx, field, order)}"
258
263
  >${label}
@@ -11,8 +11,8 @@ describe("mountable-with-fields", () => {
11
11
  required: new CollectionField(true, new FieldTypes.Text()),
12
12
  };
13
13
  new (class extends Form<typeof fields, void> {
14
- fields = fields;
15
- controls = [];
14
+ getFields = () => fields;
15
+ getControls = () => [];
16
16
  async onSubmit(ctx: Context, form_values: FormData) {
17
17
  const data = await this.getParsedValues(ctx);
18
18
  // this should not throw a Typescript error
@@ -13,6 +13,7 @@ import type {
13
13
  FormData,
14
14
  } from "../forms/form-types.js";
15
15
  import { Mountable } from "./mountable.js";
16
+ import Router from "@koa/router";
16
17
 
17
18
  export type PageErrorMessage = { type: "access" | "internal"; message: string };
18
19
 
@@ -27,21 +28,41 @@ type ParsedValue<T extends FormField> = Resolved<
27
28
  export abstract class MountableWithFields<
28
29
  F extends Fields = Fields,
29
30
  > extends Mountable {
31
+ getFields: () => F;
30
32
  fields: F;
33
+ initialized = false;
31
34
  field_names_prefix = ""; // useful for multiform, where many forms are merged into one and field assignment is made using the prefix
32
35
  form_id = ""; // all fields within this mountable will be tied to form of this id
33
- abstract controls: FormControl[];
36
+ abstract getControls: (fields: F) => FormControl[];
37
+ controls: FormControl[];
34
38
 
35
39
  constructor() {
36
40
  super();
37
41
  if (!this.fields) this.fields = {} as F;
38
42
  }
39
43
 
40
- init(): void {
44
+ /** most routes should be setup using .mount(), but mount runs before
45
+ initialization so we don't yet have controls, app, etc ready for dynamic
46
+ routes **/
47
+ init(path: string, router: Router): void {
48
+ // we setup fields not on construction, but sometime after the app is
49
+ // initialized, as some field constructors require app data to fulle
50
+ // construct themselves
51
+ super.init(path, router);
52
+ if (this.initialized) {
53
+ return;
54
+ }
55
+ if (this.getFields) {
56
+ this.fields = this.getFields();
57
+ } else if (!this.fields) {
58
+ throw new Error("Fields not defined and getter not provided");
59
+ }
60
+ this.controls = this.getControls(this.fields);
41
61
  for (const [field_name, field] of Object.entries(this.fields)) {
42
62
  void field.init();
43
63
  field.setName(field_name);
44
64
  }
65
+ this.initialized = true;
45
66
  }
46
67
 
47
68
  makeFormControlContext(
@@ -145,9 +166,9 @@ export abstract class MountableWithFields<
145
166
  ): Promise<Record<string, FormDataValue>>;
146
167
 
147
168
  async getParsedValues(ctx: Context): Promise<{
148
- [field in keyof F]: F[field] extends FormField<true>
149
- ? Exclude<ParsedValue<F[field]>, null>
150
- : ParsedValue<F[field]>;
169
+ [field in keyof F]: F[field] extends FormField<true> ?
170
+ Exclude<ParsedValue<F[field]>, null>
171
+ : ParsedValue<F[field]>;
151
172
  }> {
152
173
  const raw_values = await this.extractRawValues(ctx);
153
174
  const result: Record<string, unknown> = {};
@@ -3,7 +3,7 @@ import Router from "@koa/router";
3
3
  import { Context } from "koa";
4
4
 
5
5
  export abstract class Mountable {
6
- init(): void {}
6
+ init(_path: string, _router: Router): void {}
7
7
  abstract mount(router: Router, path: string): void;
8
8
 
9
9
  abstract canAccess(
package/src/page/page.ts CHANGED
@@ -15,7 +15,7 @@ export abstract class Page<
15
15
  > extends MountableWithFields<F> {
16
16
  propsParser: PagePropsParser<Record<keyof Fields, unknown>> =
17
17
  new AllQueryParams();
18
- controls = [];
18
+ getControls = () => [];
19
19
 
20
20
  mount(router: Router, path: string): void {
21
21
  router.get(path, async (ctx) => {
@@ -4,13 +4,12 @@ const defaults = {
4
4
  postimport: `
5
5
  import { Fields } from "@sealcode/sealgen";
6
6
  import type {FormDataValue} from "@sealcode/sealgen";`,
7
- fields: `
8
- password: new Fields.Password(true),`,
9
- controls: `
10
- new Controls.SimpleInput(fields.password, {
7
+ get_fields: `()=>(<const>{ password: new Fields.Password(true) })`,
8
+ get_controls: `
9
+ ()=><const>[new Controls.SimpleInput(fields.password, {
11
10
  label: "Password:",
12
11
  type: "password",
13
- }),`,
12
+ })],`,
14
13
  validate_method: `async validateValues(
15
14
  ctx: Context,
16
15
  data: Record<string, FormDataValue>
@@ -48,28 +47,20 @@ export async function formTemplate(
48
47
  const params = { ...defaults, ..._params };
49
48
  const content = `import type { Context } from "koa";
50
49
  import type { FormData } from "@sealcode/sealgen";
51
- import { Form, Controls, fieldsToShape } from "@sealcode/sealgen";
50
+ import { Form, Controls } from "@sealcode/sealgen";
52
51
  import html from "src/back/html.js";
53
52
 
54
53
  ${params.postimport}
55
54
 
56
55
  export const actionName = "${action_name}";
57
56
 
58
- const fields = {
59
- ${params.fields}
60
- };
61
-
62
- Form.initFieldNames(fields);
63
-
64
- export const ${action_name}Shape = fieldsToShape(fields);
57
+ const getFields = ${params.get_fields};
65
58
 
66
- export default new (class ${action_name}Form extends Form<typeof fields, void> {
59
+ export default new (class ${action_name}Form extends Form<ReturnType<typeof getFields>, void> {
67
60
  defaultSuccessMessage = "Formularz wypełniony poprawnie";
68
- fields = fields;
61
+ getFields = getFields;
69
62
 
70
- controls = [
71
- ${params.controls}
72
- ];
63
+ getControls = ${params.get_controls};
73
64
 
74
65
  ${params.other_methods}
75
66
 
@@ -12,11 +12,11 @@ export async function createItemFormTemplate(
12
12
  ): Promise<string> {
13
13
  const collection_fields =
14
14
  await extract_fields_from_collection(collection_name);
15
- const formFieldsVar = `${toPascalCase(collection_name)}FormFields`;
15
+ const formFieldsGetterVar = `get${toPascalCase(collection_name)}FormFields`;
16
16
 
17
17
  return formTemplate(create_action_name, {
18
18
  success_message: "Created!",
19
- postimport: `import {${formFieldsVar}, get${toPascalCase(
19
+ postimport: `import {${formFieldsGetterVar}, get${toPascalCase(
20
20
  collection_name
21
21
  )}FormControls }from "./shared.js";
22
22
 
@@ -28,12 +28,12 @@ export async function createItemFormTemplate(
28
28
  import {tempstream} from "tempstream";
29
29
  `,
30
30
 
31
- fields: `...${toPascalCase(collection_name)}FormFields`,
32
- controls: `
31
+ get_fields: `get${toPascalCase(collection_name)}FormFields`,
32
+ get_controls: `(fields: ReturnType<typeof getFields>)=>[
33
33
  new Controls.FormHeader("Create ${toPascalCase(
34
34
  collection_name
35
35
  )}"),
36
- ...get${toPascalCase(collection_name)}FormControls()`,
36
+ ...get${toPascalCase(collection_name)}FormControls(this.fields)]`,
37
37
 
38
38
  can_access: `canAccess = async (ctx: Context) => {
39
39
  const policy = ${toPascalCase(collection_name)}.getPolicy("create");
@@ -55,15 +55,15 @@ export async function createItemFormTemplate(
55
55
  ${collection_fields
56
56
  .map((field_info) =>
57
57
  field_info.form_field.generateCreateValueGetter(field_info, {
58
- form_field_types: formFieldsVar,
58
+ form_field_types: "FormFieldTypes",
59
59
  sealious_field: `fields${wrapAttribute(field_info.name)}`,
60
- form_fields: "fields",
60
+ form_fields: "this.fields",
61
61
  })
62
62
  )
63
63
  .join(",\n")}
64
64
  });
65
65
 
66
- for(const field of Object.values(${formFieldsVar})){
66
+ for(const field of Object.values(this.fields)){
67
67
  // eslint-disable-next-line no-await-in-loop
68
68
  await field.postSealiousCreate(ctx, created, formData)
69
69
  }
@@ -12,12 +12,12 @@ export async function editItemFormTemplate(
12
12
  ): Promise<string> {
13
13
  const collection_fields =
14
14
  await extract_fields_from_collection(collection_name);
15
- const formFieldsVar = `${toPascalCase(collection_name)}FormFields`;
15
+ const formFieldsGetterVar = `get${toPascalCase(collection_name)}FormFields`;
16
16
  const collectionVar = `${toPascalCase(collection_name)}`;
17
17
 
18
18
  return formTemplate(edit_action_name, {
19
19
  success_message: "Changes saved!",
20
- postimport: `import {${formFieldsVar}, get${toPascalCase(
20
+ postimport: `import {${formFieldsGetterVar}, get${toPascalCase(
21
21
  collection_name
22
22
  )}FormControls }from "../shared.js";
23
23
  import { ${collectionVar} } from "src/back/collections/collections.js";
@@ -27,12 +27,12 @@ export async function editItemFormTemplate(
27
27
  import type { CollectionInputWithAllKeys } from "sealious";
28
28
  `,
29
29
 
30
- fields: `...${toPascalCase(collection_name)}FormFields`,
31
- controls: `
30
+ get_fields: formFieldsGetterVar,
31
+ get_controls: `(fields: ReturnType<typeof getFields>)=>[
32
32
  new Controls.FormHeader("Edit ${toPascalCase(
33
33
  collection_name
34
34
  )}"),
35
- ...get${toPascalCase(collection_name)}FormControls()`,
35
+ ...get${toPascalCase(collection_name)}FormControls(this.fields)]`,
36
36
 
37
37
  can_access: `canAccess = async (ctx: Context) => {
38
38
  const policy = ${toPascalCase(collection_name)}.getPolicy("edit");
@@ -67,7 +67,7 @@ export async function editItemFormTemplate(
67
67
  sealious_field: `${collectionVar}.fields${wrapAttribute(
68
68
  field_info.name
69
69
  )}`,
70
- form_fields: "fields",
70
+ form_fields: "this.fields",
71
71
  }
72
72
  )
73
73
  )
@@ -77,7 +77,7 @@ export async function editItemFormTemplate(
77
77
 
78
78
  await item.save(ctx.$context);
79
79
 
80
- for(const field of Object.values(${formFieldsVar})){
80
+ for(const field of Object.values(this.fields)){
81
81
  // eslint-disable-next-line no-await-in-loop
82
82
  await field.postSealiousEdit(ctx, item, formData)
83
83
  }
@@ -95,7 +95,7 @@ export async function editItemFormTemplate(
95
95
  }
96
96
  `,
97
97
 
98
- get_initial_values: `async getInitialValues(ctx: Context): Promise<{ [key in keyof typeof fields]: FormDataValue | undefined }> {
98
+ get_initial_values: `async getInitialValues(ctx: Context): Promise<{ [key in keyof typeof this.fields]: FormDataValue | undefined }> {
99
99
  const id = await this.getID(ctx);
100
100
 
101
101
  const {
@@ -116,7 +116,7 @@ export async function editItemFormTemplate(
116
116
  .map((field_info) =>
117
117
  field_info.form_field.generateInitialValue(field_info, {
118
118
  form_field_types: "FieldTypes",
119
- form_fields: "fields",
119
+ form_fields: "this.fields",
120
120
  })
121
121
  )
122
122
  .join(",\n")}
@@ -25,9 +25,9 @@ export function makeDefaultFilterFields(
25
25
  .map((field) => {
26
26
  const type = field.type;
27
27
  return `{ field: "${field.name}", ...${
28
- type in DefaultListFilters
29
- ? `DefaultListFilters["${field.type}"]`
30
- : `DefaultListFilters.fallback`
28
+ type in DefaultListFilters ?
29
+ `DefaultListFilters["${field.type}"]`
30
+ : `DefaultListFilters.fallback`
31
31
  } }`;
32
32
  })
33
33
  .join(",\n");
@@ -148,7 +148,8 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
148
148
  typeof ${uppercase_collection},
149
149
  typeof BaseListPageFields
150
150
  > {
151
- fields = BaseListPageFields;
151
+ getFields = ()=>BaseListPageFields;
152
+ getControls = ()=>[];
152
153
 
153
154
  async renderFilters(ctx: Context): Promise<FlatTemplatable> {
154
155
  const query_params = qs.parse(ctx.search.slice(1));
@@ -19,13 +19,13 @@ ${Array.from(
19
19
  )
20
20
  .flat()
21
21
  .map(
22
- ({ what, from, as }) =>
23
- `import {${what}${as ? ` as ${as}` : ""} } from "${from}"`
22
+ ({ what, from, as, type }) =>
23
+ `import ${type ? "type " : ""}{${what}${as ? ` as ${as}` : ""} } from "${from}"`
24
24
  )
25
25
  ).values()
26
26
  ).join(";\n")}
27
27
 
28
- export const ${toPascalCase(collection_name)}FormFields = <const>{
28
+ export const get${toPascalCase(collection_name)}FormFields = () => <const>{
29
29
  ${collection_fields
30
30
  .map((field_info) => {
31
31
  return `${
@@ -40,11 +40,11 @@ export const ${toPascalCase(collection_name)}FormFields = <const>{
40
40
  .join(",\n")}
41
41
  };
42
42
 
43
- const fields = ${toPascalCase(collection_name)}FormFields
43
+ const getFields = get${toPascalCase(collection_name)}FormFields
44
44
 
45
45
  export const get${toPascalCase(
46
46
  collection_name
47
- )}FormControls: () => Controls.FormControl[] = () => [
47
+ )}FormControls: (fields: ReturnType<typeof getFields>) => Controls.FormControl[] = (fields) => [
48
48
  ${collection_fields
49
49
  .map(({ name }) => `fields${wrapAttribute(name)}.getControl()`)
50
50
  .join(",\n")}
@@ -61,6 +61,9 @@ for (const field_name in c.fields){
61
61
  }
62
62
  if(type == "single-reference"){
63
63
  target = field.target_collection;
64
+ }
65
+ if(type == "item-draft"){
66
+ target = field.target_collection_name;
64
67
  }
65
68
  if(type == "deep-reverse-single-reference" || type =="reverse-single-reference"){
66
69
  target = field.target_collection;