@sealcode/sealgen 0.2.2 → 0.4.0

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 (128) hide show
  1. package/@types/forms/controls/checkboxed-list-input.d.ts +5 -6
  2. package/@types/forms/controls/dropdown.d.ts +5 -6
  3. package/@types/forms/controls/editable-collection-subset.d.ts +6 -6
  4. package/@types/forms/controls/form-control.d.ts +3 -4
  5. package/@types/forms/controls/form-field-control.d.ts +9 -8
  6. package/@types/forms/controls/frame.d.ts +2 -2
  7. package/@types/forms/controls/simple-input.d.ts +5 -5
  8. package/@types/forms/field.d.ts +47 -16
  9. package/@types/forms/fields/checkboxed-list.d.ts +16 -0
  10. package/@types/forms/fields/collection-field.d.ts +11 -0
  11. package/@types/forms/fields/field.d.ts +38 -0
  12. package/@types/forms/fields/number.d.ts +13 -0
  13. package/@types/forms/fields/pick-from-list.d.ts +16 -0
  14. package/@types/forms/fields/simple-form-field.d.ts +7 -0
  15. package/@types/forms/form-fields-list.d.ts +1 -1
  16. package/@types/forms/form.d.ts +23 -28
  17. package/@types/forms/simple-form-field.d.ts +6 -0
  18. package/@types/forms/validator.d.ts +0 -3
  19. package/@types/index.d.ts +6 -4
  20. package/@types/make-hidden-inputs.d.ts +4 -0
  21. package/@types/page/list.d.ts +23 -42
  22. package/@types/page/mountable.d.ts +36 -0
  23. package/@types/page/page.d.ts +9 -16
  24. package/@types/page/props-parser.d.ts +5 -10
  25. package/@types/templates/page.d.ts +1 -1
  26. package/lib/build.js +11 -3
  27. package/lib/build.js.map +1 -1
  28. package/lib/forms/controls/checkboxed-list-input.js +10 -17
  29. package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
  30. package/lib/forms/controls/dropdown.js +16 -20
  31. package/lib/forms/controls/dropdown.js.map +1 -1
  32. package/lib/forms/controls/editable-collection-subset.js +33 -35
  33. package/lib/forms/controls/editable-collection-subset.js.map +1 -1
  34. package/lib/forms/controls/form-control.js.map +1 -1
  35. package/lib/forms/controls/form-field-control.js +2 -11
  36. package/lib/forms/controls/form-field-control.js.map +1 -1
  37. package/lib/forms/controls/simple-input.js +16 -22
  38. package/lib/forms/controls/simple-input.js.map +1 -1
  39. package/lib/forms/field.js +96 -20
  40. package/lib/forms/field.js.map +1 -1
  41. package/lib/forms/fields/checkboxed-list.js +43 -0
  42. package/lib/forms/fields/checkboxed-list.js.map +1 -0
  43. package/lib/forms/fields/collection-field.js +28 -0
  44. package/lib/forms/fields/collection-field.js.map +1 -0
  45. package/lib/forms/fields/field.js +66 -0
  46. package/lib/forms/fields/field.js.map +1 -0
  47. package/lib/forms/fields/number.js +45 -0
  48. package/lib/forms/fields/number.js.map +1 -0
  49. package/lib/forms/fields/pick-from-list.js +34 -0
  50. package/lib/forms/fields/pick-from-list.js.map +1 -0
  51. package/lib/forms/fields/simple-form-field.js +18 -0
  52. package/lib/forms/fields/simple-form-field.js.map +1 -0
  53. package/lib/forms/form.js +47 -96
  54. package/lib/forms/form.js.map +1 -1
  55. package/lib/forms/simple-form-field.js +18 -0
  56. package/lib/forms/simple-form-field.js.map +1 -0
  57. package/lib/forms/validator.js +0 -10
  58. package/lib/forms/validator.js.map +1 -1
  59. package/lib/generate-routes.js +11 -3
  60. package/lib/generate-routes.js.map +1 -1
  61. package/lib/index.js +4 -3
  62. package/lib/index.js.map +1 -1
  63. package/lib/make-hidden-inputs.js +17 -0
  64. package/lib/make-hidden-inputs.js.map +1 -0
  65. package/lib/page/list.js +64 -79
  66. package/lib/page/list.js.map +1 -1
  67. package/lib/page/mountable.js +56 -0
  68. package/lib/page/mountable.js.map +1 -0
  69. package/lib/page/page.js +11 -1
  70. package/lib/page/page.js.map +1 -1
  71. package/lib/page/props-parser.js +4 -47
  72. package/lib/page/props-parser.js.map +1 -1
  73. package/lib/templates/form.js +38 -18
  74. package/lib/templates/form.js.map +1 -1
  75. package/lib/templates/list.js +30 -20
  76. package/lib/templates/list.js.map +1 -1
  77. package/lib/templates/page.js +5 -4
  78. package/lib/templates/page.js.map +1 -1
  79. package/lib/utils/escape-url-params.js +1 -0
  80. package/lib/utils/escape-url-params.js.map +1 -1
  81. package/package.json +3 -3
  82. package/src/build.ts +13 -3
  83. package/src/forms/controls/checkboxed-list-input.ts +12 -23
  84. package/src/forms/controls/dropdown.ts +27 -32
  85. package/src/forms/controls/editable-collection-subset.ts +57 -54
  86. package/src/forms/controls/form-control.ts +5 -6
  87. package/src/forms/controls/form-field-control.ts +13 -29
  88. package/src/forms/controls/frame.ts +2 -2
  89. package/src/forms/controls/simple-input.ts +26 -23
  90. package/src/forms/fields/checkboxed-list.ts +61 -0
  91. package/src/forms/fields/collection-field.ts +54 -0
  92. package/src/forms/fields/field.ts +106 -0
  93. package/src/forms/fields/number.ts +53 -0
  94. package/src/forms/fields/pick-from-list.ts +49 -0
  95. package/src/forms/fields/simple-form-field.ts +20 -0
  96. package/src/forms/form.ts +94 -162
  97. package/src/generate-routes.ts +11 -7
  98. package/src/index.ts +6 -4
  99. package/src/make-hidden-inputs.ts +22 -0
  100. package/src/page/list.ts +113 -126
  101. package/src/page/mountable.ts +105 -0
  102. package/src/page/page.ts +20 -11
  103. package/src/page/props-parser.ts +9 -67
  104. package/src/templates/form.ts +38 -18
  105. package/src/templates/list.ts +30 -20
  106. package/src/templates/page.ts +6 -6
  107. package/src/utils/escape-url-params.ts +1 -0
  108. package/.log/ti-101379.log +0 -524
  109. package/.log/ti-131883.log +0 -505
  110. package/.log/ti-13490.log +0 -652
  111. package/.log/ti-138140.log +0 -505
  112. package/.log/ti-138288.log +0 -526
  113. package/.log/ti-292609.log +0 -508
  114. package/.log/ti-304933.log +0 -524
  115. package/.log/ti-312312.log +0 -508
  116. package/.log/ti-314240.log +0 -1617
  117. package/.log/ti-46589.log +0 -675
  118. package/.log/ti-62627.log +0 -508
  119. package/.log/ti-64785.log +0 -508
  120. package/.log/ti-65911.log +0 -533
  121. package/.log/ti-69885.log +0 -548
  122. package/.log/ti-72106.log +0 -551
  123. package/.log/ti-74858.log +0 -508
  124. package/.log/tsserver.log +0 -1651
  125. package/src/forms/controls.ts +0 -0
  126. package/src/forms/field.ts +0 -119
  127. package/src/forms/form-fields-list.ts +0 -7
  128. package/src/forms/validator.ts +0 -10
package/src/page/list.ts CHANGED
@@ -1,142 +1,110 @@
1
1
  import { BaseContext } from "koa";
2
- import { Collection, CollectionItem } from "sealious";
3
- import { Templatable, tempstream } from "tempstream";
2
+ import { Collection, CollectionItem, ItemFields, ItemList } from "sealious";
3
+ import { Templatable, tempstream, FlatTemplatable } from "tempstream";
4
4
  import { peopleWhoCan } from "./access-control";
5
5
  import { Page } from "./page";
6
- import { predicates, ShapeToType } from "@sealcode/ts-predicates";
7
- import { PagePropsParser } from "./props-parser";
6
+ import { ShapeToType } from "@sealcode/ts-predicates";
8
7
  import { FormFieldControl } from "../forms/controls/controls";
9
- import { FormField } from "../forms/field";
10
- import { FormData } from "../forms/form";
11
- import { FormFieldsList } from "../forms/form-fields-list";
8
+ import { FormField, NumberField } from "../forms/fields/field";
12
9
  import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
10
+ import { FormDataValue } from "..";
11
+ import { makeHiddenInputs } from "../make-hidden-inputs";
13
12
 
14
13
  export const BasePagePropsShape = <const>{};
15
14
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
16
15
 
17
- export const BaseListPagePropsShape = <const>{
18
- page: predicates.number,
19
- itemsPerPage: predicates.number,
16
+ export const BaseListPageFields = <const>{
17
+ page: new NumberField(false, 1),
18
+ itemsPerPage: new NumberField(false, 12),
20
19
  };
21
- export type BaseListPageProps = ShapeToType<typeof BaseListPagePropsShape>;
22
- export const BaseListPageDefaultProps = { page: 1, itemsPerPage: 25 };
23
-
24
- export type PropsErrors<PropsType> = Partial<Record<keyof PropsType, string>>;
25
20
 
26
21
  export abstract class ListPage<
27
22
  ItemType,
28
- PropsType extends BaseListPageProps = BaseListPageProps
29
- > extends Page {
23
+ F extends typeof BaseListPageFields
24
+ > extends Page<F> {
30
25
  abstract getItems(
31
26
  ctx: BaseContext,
32
- props: PropsType
33
- ): Promise<{ items: ItemType[] }>;
34
- abstract getTotalPages(ctx: BaseContext, props: PropsType): Promise<number>;
35
- abstract renderItem(ctx: BaseContext, item: ItemType): Promise<Templatable>;
36
- abstract propsParser: PagePropsParser<PropsType>;
37
-
38
- filterFields: FormField<keyof PropsType>[] = [];
39
- filterControls: FormFieldControl[] = [];
27
+ page: number,
28
+ itemsPerPage: number,
29
+ values: Record<string, FormDataValue>
30
+ ): Promise<ItemType[]>;
40
31
 
41
- renderListContainer(_: BaseContext, content: Templatable): Templatable {
42
- return tempstream`<div>${content}</div>`;
43
- }
32
+ abstract getTotalPages(
33
+ ctx: BaseContext,
34
+ itemsPerPage: number,
35
+ values: Record<string, FormDataValue>
36
+ ): Promise<number>;
44
37
 
45
- async validateProps(
38
+ abstract renderItem(
46
39
  ctx: BaseContext,
47
- props: PropsType
48
- ): Promise<{ valid: boolean; errors: PropsErrors<PropsType> }> {
49
- const errors: PropsErrors<PropsType> = {};
50
- let has_errors = false;
51
- const promises = [];
52
- for (const [key, value] of Object.entries(props)) {
53
- const field = FormFieldsList.getField(this.filterFields, key);
54
- if (field) {
55
- promises.push(
56
- field._validate(ctx, value).then(({ valid, message }) => {
57
- if (!valid) {
58
- has_errors = true;
59
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
60
- errors[key as keyof PropsType] = message;
61
- }
62
- })
63
- );
64
- }
40
+ item: ItemType
41
+ ): Promise<FlatTemplatable>;
42
+
43
+ filterFields: Record<string, FormField> = {};
44
+ filterControls: FormFieldControl[] = [];
45
+
46
+ init(): void {
47
+ super.init();
48
+ for (const [fieldname, field] of Object.entries(this.filterFields)) {
49
+ field.init(fieldname);
65
50
  }
66
- await Promise.all(promises);
67
- return { valid: has_errors, errors };
68
51
  }
69
52
 
70
- async getProps(ctx: BaseContext): Promise<{
71
- parsed_props: PropsType;
72
- errors: PropsErrors<PropsType>;
73
- raw_props: PropsType;
74
- }> {
75
- const raw_props = this.propsParser.decode(ctx);
76
- const parsed_props = { ...raw_props };
77
- const { errors } = await this.validateProps(ctx, parsed_props);
78
- for (const prop_name in errors) {
79
- const default_value = this.propsParser.getDefaultValue(prop_name);
80
- if (default_value !== undefined) {
81
- parsed_props[prop_name] = default_value;
82
- } else {
83
- delete parsed_props[prop_name];
84
- }
85
- }
86
- return { parsed_props, errors, raw_props };
53
+ renderListContainer(_: BaseContext, content: Templatable): FlatTemplatable {
54
+ return tempstream`<div>${content}</div>`;
87
55
  }
88
56
 
89
- async render(ctx: BaseContext) {
90
- const { parsed_props, errors, raw_props } = await this.getProps(ctx);
91
-
92
- return tempstream`${this.renderPagination(ctx, parsed_props)}
93
- ${this.renderFilters(ctx, parsed_props, raw_props, errors)}
94
- ${this.getItems(ctx, parsed_props).then(({ items }) =>
95
- this.renderListContainer(
96
- ctx,
97
- items.map((item) => this.renderItem(ctx, item))
98
- )
99
- )}`;
57
+ async render(ctx: BaseContext): Promise<FlatTemplatable> {
58
+ const values = this.extractRawValues(ctx);
59
+ const { parsed: page } = await this.fields.page.getValue(ctx, values);
60
+ const { parsed: itemsPerPage } =
61
+ await this.fields.itemsPerPage.getValue(ctx, values);
62
+
63
+ return tempstream`${this.renderPagination(ctx, values)}
64
+ ${this.renderFilters(ctx)}
65
+ ${this.getItems(ctx, page, itemsPerPage, values).then((items) =>
66
+ this.renderListContainer(
67
+ ctx,
68
+ items.map((item) => this.renderItem(ctx, item))
69
+ )
70
+ )}`;
100
71
  }
101
72
 
102
- async renderPagination(ctx: BaseContext, props: PropsType) {
103
- const totalIems = await this.getTotalPages(ctx, props);
104
- const currentPage = props.page;
73
+ async renderPagination(
74
+ ctx: BaseContext,
75
+ values: Record<string, FormDataValue>
76
+ ): Promise<FlatTemplatable> {
77
+ const { parsed: page } = await this.fields.page.getValue(ctx, values);
78
+ const { parsed: itemsPerPage } =
79
+ await this.fields.itemsPerPage.getValue(ctx, values);
80
+ const totalIems = await this.getTotalPages(ctx, itemsPerPage, values);
105
81
 
106
82
  return tempstream/* HTML */ `<center>
107
- ${currentPage > 1
108
- ? this.renderPageButton(ctx, 1, "Pierwsza strona")
109
- : ""}
110
- ${currentPage > 1
111
- ? this.renderPageButton(
112
- ctx,
113
- currentPage - 1,
114
- "Poprzednia strona"
115
- )
83
+ ${page > 1 ? this.renderPageButton(ctx, 1, "Pierwsza strona") : ""}
84
+ ${page > 1
85
+ ? this.renderPageButton(ctx, page - 1, "Poprzednia strona")
116
86
  : ""}
117
87
 
118
88
  <select onchange="if (this.value) Turbo.visit(this.value)">
119
- ${Array.from(
120
- naturalNumbers(1, await this.getTotalPages(ctx, props))
121
- ).map(
89
+ ${Array.from(naturalNumbers(1, totalIems)).map(
122
90
  (n) => /* HTML */ `<option
123
91
  value="${UrlWithNewParams(
124
92
  ctx,
125
93
  //eslint-disable-next-line @typescript-eslint/consistent-type-assertions
126
94
  this.propsParser.overwriteProp(ctx, {
127
95
  page: n,
128
- } as Partial<PropsType>)
96
+ } as Partial<Record<string, unknown>>)
129
97
  )}"
130
- ${currentPage === n ? "selected" : ""}
98
+ ${page === n ? "selected" : ""}
131
99
  >
132
100
  ${n}
133
101
  </option>`
134
102
  )}
135
103
  </select>
136
- ${currentPage < totalIems
137
- ? this.renderPageButton(ctx, currentPage + 1, "Następna strona")
104
+ ${page < totalIems
105
+ ? this.renderPageButton(ctx, page + 1, "Następna strona")
138
106
  : ""}
139
- ${currentPage < totalIems
107
+ ${page < totalIems
140
108
  ? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
141
109
  : ""}
142
110
  </center>`;
@@ -149,31 +117,33 @@ ${this.getItems(ctx, parsed_props).then(({ items }) =>
149
117
  //eslint-disable-next-line @typescript-eslint/consistent-type-assertions
150
118
  this.propsParser.overwriteProp(ctx, {
151
119
  page,
152
- } as Partial<PropsType>)
120
+ } as Partial<Record<string, unknown>>)
153
121
  )}"
154
122
  >${text}</a
155
123
  >`;
156
124
  }
157
125
 
158
- renderFilters(
159
- ctx: BaseContext,
160
- parsed_props: PropsType, // parsed props don't include wrong values
161
- raw_props: PropsType,
162
- errors: PropsErrors<PropsType>
163
- ) {
126
+ async getFilterValues(ctx: BaseContext): Promise<Record<string, unknown>> {
127
+ const filter = {} as Record<string, unknown>;
128
+ const raw_values = this.extractRawValues(ctx);
129
+ for (const [fieldname, field] of Object.entries(this.filterFields)) {
130
+ // eslint-disable-next-line no-await-in-loop
131
+ const { parsed } = await field.getValue(ctx, raw_values);
132
+ filter[fieldname] = parsed;
133
+ }
134
+ return filter;
135
+ }
136
+
137
+ renderFilters(ctx: BaseContext): FlatTemplatable {
138
+ const values = this.extractRawValues(ctx);
164
139
  return tempstream/* HTML */ `<form method="GET">
165
- ${this.propsParser.makeHiddenInputs(parsed_props, [
140
+ ${makeHiddenInputs(ctx, this.fields, values, [
166
141
  "page",
167
- ...this.filterFields.map((f) => f.name),
142
+ ...Object.values(this.filterFields).map((f) => f.name),
168
143
  ])}
169
144
  ${this.filterControls.map((control) =>
170
145
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
171
- control.render(ctx, this.filterFields, {
172
- values: parsed_props,
173
- raw_values: raw_props,
174
- errors,
175
- messages: [],
176
- } as FormData)
146
+ control.render(ctx, values)
177
147
  )}
178
148
  </form>`;
179
149
  }
@@ -181,32 +151,49 @@ ${this.getItems(ctx, parsed_props).then(({ items }) =>
181
151
 
182
152
  export abstract class SealiousItemListPage<
183
153
  C extends Collection,
184
- PageProps extends BaseListPageProps = BaseListPageProps
185
- > extends ListPage<CollectionItem<C>, PageProps> {
154
+ F extends typeof BaseListPageFields
155
+ > extends ListPage<CollectionItem<C>, F> {
186
156
  constructor(public collection: C) {
187
157
  super();
188
158
  }
189
159
 
190
- async getTotalPages(ctx: BaseContext, props: PageProps) {
191
- const { items } = await this.collection.list(ctx.$context).fetch();
192
- return Math.ceil(items.length / props.itemsPerPage);
160
+ async getTotalPages(
161
+ ctx: BaseContext,
162
+ itemsPerPage: number
163
+ ): Promise<number> {
164
+ const query = this.collection.list(ctx.$context);
165
+ await this.addFilter(ctx, query);
166
+ const { items } = await query.fetch();
167
+ return Math.ceil(items.length / itemsPerPage);
168
+ }
169
+
170
+ protected async addFilter(
171
+ ctx: BaseContext,
172
+ list: ItemList<C>
173
+ ): Promise<void> {
174
+ const filters = await this.getFilterValues(ctx);
175
+ if (Object.keys(filters).length) {
176
+ list.filter(filters as Partial<ItemFields<C>>);
177
+ }
193
178
  }
194
179
 
195
- async getItems(ctx: BaseContext, props: PageProps) {
196
- return {
197
- items: (
198
- await this.collection
199
- .list(ctx.$context)
200
- .paginate({ items: props.itemsPerPage, page: props.page })
201
- .fetch()
202
- ).items,
203
- };
180
+ async getItems(
181
+ ctx: BaseContext,
182
+ page: number,
183
+ itemsPerPage: number
184
+ ): Promise<CollectionItem<C>[]> {
185
+ const query = this.collection.list(ctx.$context);
186
+ await this.addFilter(ctx, query);
187
+ const { items } = await query
188
+ .paginate({ items: itemsPerPage, page })
189
+ .fetch();
190
+ return items;
204
191
  }
205
192
 
206
193
  async renderItem(
207
194
  _: BaseContext,
208
195
  item: CollectionItem<C>
209
- ): Promise<Templatable> {
196
+ ): Promise<FlatTemplatable> {
210
197
  return `<div>${item.id}</div>`;
211
198
  }
212
199
 
@@ -0,0 +1,105 @@
1
+ import Router from "@koa/router";
2
+ import { FormControl } from "../forms/controls/form-control";
3
+ import { BaseContext } from "koa";
4
+ import { FlatTemplatable, Templatable, tempstream } from "tempstream";
5
+ import { FormField } from "../forms/fields/field";
6
+ import { FormData, FormDataValue, FormMessage } from "../forms/form";
7
+
8
+ export type PageErrorMessage = { type: "access" | "internal"; message: string };
9
+
10
+ export type Fields = Record<string, FormField<unknown>>;
11
+
12
+ export abstract class Mountable<F extends Fields = Fields> {
13
+ fields: F;
14
+
15
+ constructor() {
16
+ if (!this.fields) this.fields = {} as F;
17
+ }
18
+
19
+ abstract controls: FormControl[];
20
+ abstract mount(router: Router, path: string): void;
21
+
22
+ abstract canAccess(
23
+ ctx: BaseContext
24
+ ): Promise<{ canAccess: boolean; message: string }>;
25
+
26
+ abstract renderError(
27
+ ctx: BaseContext,
28
+ error: PageErrorMessage
29
+ ): Promise<Templatable>;
30
+
31
+ abstract extractRawValues(ctx: BaseContext): Record<string, FormDataValue>;
32
+
33
+ // this one is meant to be overwritten
34
+ async validateValues(
35
+ ctx: BaseContext,
36
+ data: Record<string, FormDataValue>
37
+ ): Promise<{ valid: boolean; error: string }> {
38
+ return {
39
+ valid: true,
40
+ error: "",
41
+ };
42
+ }
43
+
44
+ protected async validate(
45
+ ctx: BaseContext,
46
+ values: Record<string, FormDataValue>
47
+ ): Promise<{
48
+ valid: boolean;
49
+ field_errors: Partial<Record<keyof Fields, string>>;
50
+ form_messages: FormMessage[];
51
+ }> {
52
+ const field_errors = {} as Record<keyof Fields, string>;
53
+ let valid = true;
54
+ const form_messages = [] as FormMessage[];
55
+ await Promise.all(
56
+ Object.entries(values)
57
+ .filter(([fieldname]) => fieldname in this.fields)
58
+ .map(async ([key]: [keyof F, FormDataValue]) => {
59
+ const field = this.fields[key];
60
+ const { valid: fieldvalid, message: fieldmessage } =
61
+ await field.getValue(ctx, values);
62
+ if (!fieldvalid) {
63
+ valid = false;
64
+ field_errors[field.name] = fieldmessage;
65
+ }
66
+ })
67
+ );
68
+ const formValidationResult = await this.validateValues(ctx, values);
69
+ if (!formValidationResult.valid) {
70
+ form_messages.push({
71
+ type: "error",
72
+ text: formValidationResult.error,
73
+ });
74
+ valid = false;
75
+ }
76
+ return { valid, field_errors, form_messages };
77
+ }
78
+
79
+ init(): void {
80
+ for (const [name, field] of Object.entries(this.fields)) {
81
+ field.init(name);
82
+ }
83
+ }
84
+
85
+ public renderControls(
86
+ ctx: BaseContext,
87
+ data: FormData<keyof Fields>
88
+ ): FlatTemplatable {
89
+ return tempstream/* HTML */ `${this.controls.map((control) =>
90
+ control.render(ctx, data.raw_values, data.messages)
91
+ )}`;
92
+ }
93
+
94
+ public renderMessages(
95
+ _: BaseContext,
96
+ data: FormData<keyof Fields>
97
+ ): Templatable {
98
+ return tempstream/* HTML */ `<div class="form-messages">
99
+ ${data.messages.map(
100
+ (message) =>
101
+ `<div class="form-message form-message--${message.type}">${message.text}</div>`
102
+ )}
103
+ </div>`;
104
+ }
105
+ }
package/src/page/page.ts CHANGED
@@ -1,18 +1,20 @@
1
1
  import Router from "@koa/router";
2
2
  import { BaseContext } from "koa";
3
3
 
4
- import { Templatable, tempstream } from "tempstream";
4
+ import { FlatTemplatable, Templatable, tempstream } from "tempstream";
5
+ import { AllQueryParams } from "..";
6
+ import { FormDataValue } from "../forms/form";
7
+ import { Fields, Mountable, PageErrorMessage } from "./mountable";
8
+ import { PagePropsParser } from "./props-parser";
5
9
 
6
- export type PageErrorMessage = { type: "access" | "internal"; message: string };
10
+ export abstract class Page<
11
+ F extends Fields = Record<string, never>
12
+ > extends Mountable<F> {
13
+ propsParser: PagePropsParser<Record<keyof Fields, unknown>> =
14
+ new AllQueryParams();
15
+ controls = [];
7
16
 
8
- export interface Mountable {
9
- mount: (router: Router, path: string) => void;
10
- canAccess: (ctx: BaseContext) => Promise<{ canAccess: boolean; message: string }>;
11
- renderError(ctx: BaseContext, error: PageErrorMessage): Promise<Templatable>;
12
- }
13
-
14
- export abstract class Page implements Mountable {
15
- mount(router: Router, path: string) {
17
+ mount(router: Router, path: string): void {
16
18
  router.get(path, async (ctx) => {
17
19
  ctx.body = await this.render(ctx);
18
20
  });
@@ -23,9 +25,16 @@ export abstract class Page implements Mountable {
23
25
  return { canAccess: true, message: "" };
24
26
  }
25
27
 
26
- async renderError(_: BaseContext, error: PageErrorMessage) {
28
+ async renderError(
29
+ _: BaseContext,
30
+ error: PageErrorMessage
31
+ ): Promise<FlatTemplatable> {
27
32
  return tempstream/* HTML */ `<div>${error.message}</div>`;
28
33
  }
29
34
 
30
35
  public abstract render(ctx: BaseContext): Promise<Templatable>;
36
+
37
+ extractRawValues(ctx: BaseContext): Record<string, FormDataValue> {
38
+ return this.propsParser.decode(ctx);
39
+ }
31
40
  }
@@ -1,48 +1,19 @@
1
- import {
2
- hasShape,
3
- predicates,
4
- Shape,
5
- ShapeToType,
6
- } from "@sealcode/ts-predicates";
7
-
8
1
  import { BaseContext } from "koa";
9
2
  import merge from "merge";
3
+ import { FormData, FormDataValue } from "..";
10
4
  import { BasePageProps } from "./list";
11
5
 
12
6
  export type EncodedProps = Record<string, unknown>;
13
7
 
14
- // the intention here is to sometime in the future be able to store multiple frames on one document, so props for each frame will be in a different namespace, and parsers are going to help with that
8
+ // page props parser is only tasked with getting the raw props from *somewhere*
9
+ // and being able to put new values back to that somewhere. Parsing to numbers
10
+ // or any other exotic types is done by fields logic.
15
11
 
16
- function parseStringValues<TheShape extends Shape>(
17
- shape: TheShape,
18
- values: Record<string, string>
19
- ): ShapeToType<TheShape> {
20
- const result: Record<string, unknown> = {};
21
- for (const [key, value] of Object.entries(values)) {
22
- if (!(key in shape)) {
23
- continue;
24
- }
25
- const predicate = shape[key];
26
- if (predicate == predicates.number) {
27
- result[key] = parseFloat(value);
28
- } else {
29
- result[key] = value;
30
- }
31
- }
32
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
33
- return result as ShapeToType<TheShape>;
34
- }
35
-
36
- export abstract class PagePropsParser<PropsType extends BasePageProps> {
37
- abstract decode(ctx: BaseContext): PropsType;
38
- abstract encode(props: PropsType): EncodedProps;
12
+ export abstract class PagePropsParser<PropsType> {
13
+ abstract decode(ctx: BaseContext): Record<string, FormDataValue>;
14
+ abstract encode(props: Record<string, FormData>): EncodedProps;
39
15
  abstract getHTMLInputName(prop_name: string): string;
40
16
 
41
- constructor(
42
- public propsShape: Shape,
43
- public defaultValues: Partial<PropsType>
44
- ) {}
45
-
46
17
  overwriteProp(
47
18
  ctx: BaseContext,
48
19
  new_props: Partial<PropsType>
@@ -51,42 +22,13 @@ export abstract class PagePropsParser<PropsType extends BasePageProps> {
51
22
  merge.recursive(result, this.decode(ctx), new_props);
52
23
  return result;
53
24
  }
54
-
55
- makeHiddenInputs(values: PropsType, fields_to_skip: string[]): string {
56
- return Object.entries(values)
57
- .filter(([key]) => !fields_to_skip.includes(key))
58
- .map(
59
- ([key, value]: [string, string | number]) =>
60
- /* HTML */ `<input
61
- type="hidden"
62
- name="${key}"
63
- value="${value}"
64
- />`
65
- )
66
- .join(" ");
67
- }
68
-
69
- getDefaultValue<Key extends keyof PropsType>(
70
- key: Key
71
- ): PropsType[Key] | undefined {
72
- return this.defaultValues[key];
73
- }
74
25
  }
75
26
 
76
27
  export class AllQueryParams<
77
28
  PropsType extends BasePageProps
78
29
  > extends PagePropsParser<PropsType> {
79
- decode(ctx: BaseContext): PropsType {
80
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
81
- const query = parseStringValues(this.propsShape, {
82
- ...this.defaultValues,
83
- ...ctx.query,
84
- } as unknown as Record<string, string>);
85
- if (!hasShape(this.propsShape, query)) {
86
- throw new Error("Wrong props shape");
87
- }
88
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
89
- return query as PropsType;
30
+ decode(ctx: BaseContext): Record<string, FormDataValue> {
31
+ return ctx.query;
90
32
  }
91
33
 
92
34
  encode(props: PropsType): Record<string, unknown> {
@@ -6,39 +6,59 @@ export async function formTemplate(
6
6
  ): Promise<string> {
7
7
  const rel = curryImportPath(newfilefullpath);
8
8
  return `import { BaseContext } from "koa";
9
- import {Users} from "${rel("src/back/collections/collections")}";
10
- import { FormHeader, SimpleInput } from "${rel("src/back/forms/controls")}";
11
- import { FormField } from "${rel("src/back/forms/field")}";
12
- import Form, { FormData } from "${rel("src/back/forms/form")}";
13
- import { collectionFieldValidator } from "${rel("src/back/forms/validator")}";
14
- import html from "${rel("src/back/html")}";
9
+ import {
10
+ CollectionField,
11
+ Form,
12
+ FormData,
13
+ FormDataValue,
14
+ SimpleFormField,
15
+ SimpleInput,
16
+ } from "@sealcode/sealgen";
17
+ import html from "../html";
18
+ import { Users } from "${rel("src/back/collections/collections")}";
15
19
 
16
20
  export const actionName = "${action_name}";
17
21
 
18
- export default new(class ${action_name}Form extends Form {
19
- defaultSuccessMessage = "Pomyślnie utworzono użytkownika";
22
+ const fields = {
23
+ email: new CollectionField(true, Users.fields.email),
24
+ password: new SimpleFormField(true),
25
+ };
20
26
 
21
- fields = [new FormField("email", true, collectionFieldValidator(Users.fields.email))];
27
+ export default new (class ${action_name}Form extends Form<typeof fields> {
28
+ defaultSuccessMessage = "Formularz wypełniony poprawnie";
29
+ fields = fields;
22
30
 
23
31
  controls = [
24
- new FormHeader("Enter your email address"),
25
- new SimpleInput("email", { label: "Your email", type: "email" }),
32
+ new SimpleInput(fields.email, { label: "Email:", type: "email" }),
33
+ new SimpleInput(fields.password, { label: "Password:", type: "password" }),
26
34
  ];
27
35
 
36
+ async validateValues(
37
+ ctx: BaseContext,
38
+ data: Record<string, FormDataValue>
39
+ ): Promise<{ valid: boolean; error: string }> {
40
+ // just an example
41
+ const { parsed: email } = await this.fields.email.getValue(ctx, data);
42
+ const { parsed: password } = await this.fields.password.getValue(ctx, data);
43
+ if ((password || "").length > (email || "").length) {
44
+ return { valid: false, error: "Password cannot be longer than email" };
45
+ } else {
46
+ return { valid: true, error: "" };
47
+ }
48
+ }
49
+
28
50
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
51
  async canAccess(_: BaseContext) {
30
52
  return { canAccess: true, message: "" };
31
53
  }
32
54
 
33
- async onSubmit(ctx: BaseContext, values: Record<"email", string>) {
34
- await ctx.$app.collections.users.create(ctx.$context, {
35
- username: values.email,
36
- ...values,
37
- });
55
+ async onSubmit() {
56
+ //noop
57
+ return;
38
58
  }
39
59
 
40
- async render(ctx: BaseContext, data: FormData, path: string) {
41
- return html(ctx, "${action_name}", await super.render(ctx, data, path));
60
+ async render(ctx: BaseContext, data: FormData) {
61
+ return html(ctx, "${action_name}", await super.render(ctx, data));
42
62
  }
43
63
  })();
44
64
  `;