@sealcode/sealgen 0.3.2 → 0.4.2
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/@types/forms/controls/checkboxed-list-input.d.ts +5 -6
- package/@types/forms/controls/dropdown.d.ts +5 -6
- package/@types/forms/controls/editable-collection-subset.d.ts +6 -6
- package/@types/forms/controls/form-control.d.ts +3 -4
- package/@types/forms/controls/form-field-control.d.ts +9 -8
- package/@types/forms/controls/frame.d.ts +2 -2
- package/@types/forms/controls/simple-input.d.ts +5 -5
- package/@types/forms/field.d.ts +47 -16
- package/@types/forms/fields/checkboxed-list.d.ts +16 -0
- package/@types/forms/fields/collection-field.d.ts +11 -0
- package/@types/forms/fields/field.d.ts +38 -0
- package/@types/forms/fields/number.d.ts +13 -0
- package/@types/forms/fields/pick-from-list.d.ts +16 -0
- package/@types/forms/fields/simple-form-field.d.ts +7 -0
- package/@types/forms/form-fields-list.d.ts +1 -1
- package/@types/forms/form.d.ts +24 -29
- package/@types/forms/simple-form-field.d.ts +6 -0
- package/@types/forms/validator.d.ts +0 -3
- package/@types/index.d.ts +8 -5
- package/@types/make-hidden-inputs.d.ts +4 -0
- package/@types/page/list.d.ts +23 -42
- package/@types/page/mountable.d.ts +36 -0
- package/@types/page/page.d.ts +10 -17
- package/@types/page/props-parser.d.ts +5 -10
- package/@types/utils/translate.d.ts +3 -0
- package/lib/build.js +2 -1
- package/lib/build.js.map +1 -1
- package/lib/forms/controls/checkboxed-list-input.js +10 -17
- package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
- package/lib/forms/controls/dropdown.js +16 -20
- package/lib/forms/controls/dropdown.js.map +1 -1
- package/lib/forms/controls/editable-collection-subset.js +33 -35
- package/lib/forms/controls/editable-collection-subset.js.map +1 -1
- package/lib/forms/controls/form-control.js.map +1 -1
- package/lib/forms/controls/form-field-control.js +2 -11
- package/lib/forms/controls/form-field-control.js.map +1 -1
- package/lib/forms/controls/simple-input.js +17 -23
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/field.js +96 -20
- package/lib/forms/field.js.map +1 -1
- package/lib/forms/fields/checkboxed-list.js +43 -0
- package/lib/forms/fields/checkboxed-list.js.map +1 -0
- package/lib/forms/fields/collection-field.js +28 -0
- package/lib/forms/fields/collection-field.js.map +1 -0
- package/lib/forms/fields/field.js +70 -0
- package/lib/forms/fields/field.js.map +1 -0
- package/lib/forms/fields/number.js +45 -0
- package/lib/forms/fields/number.js.map +1 -0
- package/lib/forms/fields/pick-from-list.js +34 -0
- package/lib/forms/fields/pick-from-list.js.map +1 -0
- package/lib/forms/fields/simple-form-field.js +18 -0
- package/lib/forms/fields/simple-form-field.js.map +1 -0
- package/lib/forms/form.js +50 -96
- package/lib/forms/form.js.map +1 -1
- package/lib/forms/simple-form-field.js +18 -0
- package/lib/forms/simple-form-field.js.map +1 -0
- package/lib/forms/validator.js +0 -10
- package/lib/forms/validator.js.map +1 -1
- package/lib/generate-routes.js +19 -2
- package/lib/generate-routes.js.map +1 -1
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/lib/make-hidden-inputs.js +17 -0
- package/lib/make-hidden-inputs.js.map +1 -0
- package/lib/page/list.js +65 -79
- package/lib/page/list.js.map +1 -1
- package/lib/page/mountable.js +56 -0
- package/lib/page/mountable.js.map +1 -0
- package/lib/page/page.js +11 -1
- package/lib/page/page.js.map +1 -1
- package/lib/page/props-parser.js +4 -47
- package/lib/page/props-parser.js.map +1 -1
- package/lib/templates/form.js +32 -17
- package/lib/templates/form.js.map +1 -1
- package/lib/templates/list.js +30 -20
- package/lib/templates/list.js.map +1 -1
- package/lib/utils/translate.js +10 -0
- package/lib/utils/translate.js.map +1 -0
- package/package.json +4 -3
- package/src/build.ts +2 -1
- package/src/forms/controls/checkboxed-list-input.ts +12 -23
- package/src/forms/controls/dropdown.ts +27 -32
- package/src/forms/controls/editable-collection-subset.ts +57 -54
- package/src/forms/controls/form-control.ts +5 -6
- package/src/forms/controls/form-field-control.ts +13 -29
- package/src/forms/controls/frame.ts +2 -2
- package/src/forms/controls/simple-input.ts +27 -24
- package/src/forms/fields/checkboxed-list.ts +61 -0
- package/src/forms/fields/collection-field.ts +54 -0
- package/src/forms/fields/field.ts +116 -0
- package/src/forms/fields/number.ts +53 -0
- package/src/forms/fields/pick-from-list.ts +49 -0
- package/src/forms/fields/simple-form-field.ts +20 -0
- package/src/forms/form.ts +98 -163
- package/src/generate-routes.ts +19 -2
- package/src/index.ts +8 -5
- package/src/make-hidden-inputs.ts +22 -0
- package/src/page/list.ts +115 -126
- package/src/page/mountable.ts +105 -0
- package/src/page/page.ts +21 -12
- package/src/page/props-parser.ts +9 -67
- package/src/templates/form.ts +32 -17
- package/src/templates/list.ts +30 -20
- package/src/utils/translate.ts +13 -0
- package/src/forms/controls.ts +0 -0
- package/src/forms/field.ts +0 -119
- package/src/forms/form-fields-list.ts +0 -7
- package/src/forms/validator.ts +0 -10
package/src/page/list.ts
CHANGED
|
@@ -1,142 +1,112 @@
|
|
|
1
1
|
import { BaseContext } from "koa";
|
|
2
|
-
import { Collection, CollectionItem } from "sealious";
|
|
3
|
-
|
|
2
|
+
import { Collection, CollectionItem, ItemFields, ItemList } from "sealious";
|
|
3
|
+
|
|
4
|
+
import { Templatable, tempstream, FlatTemplatable } from "tempstream";
|
|
4
5
|
import { peopleWhoCan } from "./access-control";
|
|
5
6
|
import { Page } from "./page";
|
|
6
|
-
import {
|
|
7
|
-
import { PagePropsParser } from "./props-parser";
|
|
7
|
+
import { ShapeToType } from "@sealcode/ts-predicates";
|
|
8
8
|
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";
|
|
9
|
+
import { FormField, NumberField } from "../forms/fields/field";
|
|
12
10
|
import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
|
|
11
|
+
import { FormDataValue } from "..";
|
|
12
|
+
import { makeHiddenInputs } from "../make-hidden-inputs";
|
|
13
13
|
|
|
14
14
|
export const BasePagePropsShape = <const>{};
|
|
15
15
|
export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
|
|
16
16
|
|
|
17
|
-
export const
|
|
18
|
-
page:
|
|
19
|
-
itemsPerPage:
|
|
17
|
+
export const BaseListPageFields = <const>{
|
|
18
|
+
page: new NumberField(false, 1),
|
|
19
|
+
itemsPerPage: new NumberField(false, 12),
|
|
20
20
|
};
|
|
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
21
|
|
|
26
22
|
export abstract class ListPage<
|
|
27
23
|
ItemType,
|
|
28
|
-
|
|
29
|
-
> extends Page {
|
|
24
|
+
F extends typeof BaseListPageFields
|
|
25
|
+
> extends Page<F> {
|
|
30
26
|
abstract getItems(
|
|
31
27
|
ctx: BaseContext,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
abstract propsParser: PagePropsParser<PropsType>;
|
|
37
|
-
|
|
38
|
-
filterFields: FormField<keyof PropsType>[] = [];
|
|
39
|
-
filterControls: FormFieldControl[] = [];
|
|
28
|
+
page: number,
|
|
29
|
+
itemsPerPage: number,
|
|
30
|
+
values: Record<string, FormDataValue>
|
|
31
|
+
): Promise<ItemType[]>;
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
abstract getTotalPages(
|
|
34
|
+
ctx: BaseContext,
|
|
35
|
+
itemsPerPage: number,
|
|
36
|
+
values: Record<string, FormDataValue>
|
|
37
|
+
): Promise<number>;
|
|
44
38
|
|
|
45
|
-
|
|
39
|
+
abstract renderItem(
|
|
46
40
|
ctx: BaseContext,
|
|
47
|
-
|
|
48
|
-
): Promise<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|
|
41
|
+
item: ItemType
|
|
42
|
+
): Promise<FlatTemplatable>;
|
|
43
|
+
|
|
44
|
+
filterFields: Record<string, FormField> = {};
|
|
45
|
+
filterControls: FormFieldControl[] = [];
|
|
46
|
+
|
|
47
|
+
init(): void {
|
|
48
|
+
super.init();
|
|
49
|
+
for (const [fieldname, field] of Object.entries(this.filterFields)) {
|
|
50
|
+
field.init(fieldname);
|
|
65
51
|
}
|
|
66
|
-
await Promise.all(promises);
|
|
67
|
-
return { valid: has_errors, errors };
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
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 };
|
|
54
|
+
renderListContainer(_: BaseContext, content: Templatable): FlatTemplatable {
|
|
55
|
+
return tempstream`<div>${content}</div>`;
|
|
87
56
|
}
|
|
88
57
|
|
|
89
|
-
async render(ctx: BaseContext) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ctx,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
58
|
+
async render(ctx: BaseContext): Promise<FlatTemplatable> {
|
|
59
|
+
const values = this.extractRawValues(ctx);
|
|
60
|
+
const { parsed: page } = await this.fields.page.getValue(ctx, values);
|
|
61
|
+
const { parsed: itemsPerPage } =
|
|
62
|
+
await this.fields.itemsPerPage.getValue(ctx, values);
|
|
63
|
+
|
|
64
|
+
const items_promise = this.getItems(ctx, page, itemsPerPage, values);
|
|
65
|
+
return tempstream`${this.renderPagination(ctx, values)}
|
|
66
|
+
${this.renderFilters(ctx)}
|
|
67
|
+
${items_promise.then((items) =>
|
|
68
|
+
this.renderListContainer(
|
|
69
|
+
ctx,
|
|
70
|
+
items.map((item) => this.renderItem(ctx, item))
|
|
71
|
+
)
|
|
72
|
+
)}`;
|
|
100
73
|
}
|
|
101
74
|
|
|
102
|
-
async renderPagination(
|
|
103
|
-
|
|
104
|
-
|
|
75
|
+
async renderPagination(
|
|
76
|
+
ctx: BaseContext,
|
|
77
|
+
values: Record<string, FormDataValue>
|
|
78
|
+
): Promise<FlatTemplatable> {
|
|
79
|
+
const { parsed: page } = await this.fields.page.getValue(ctx, values);
|
|
80
|
+
const { parsed: itemsPerPage } =
|
|
81
|
+
await this.fields.itemsPerPage.getValue(ctx, values);
|
|
82
|
+
const totalIems = await this.getTotalPages(ctx, itemsPerPage, values);
|
|
105
83
|
|
|
106
84
|
return tempstream/* HTML */ `<center>
|
|
107
|
-
${
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
${currentPage > 1
|
|
111
|
-
? this.renderPageButton(
|
|
112
|
-
ctx,
|
|
113
|
-
currentPage - 1,
|
|
114
|
-
"Poprzednia strona"
|
|
115
|
-
)
|
|
85
|
+
${page > 1 ? this.renderPageButton(ctx, 1, "Pierwsza strona") : ""}
|
|
86
|
+
${page > 1
|
|
87
|
+
? this.renderPageButton(ctx, page - 1, "Poprzednia strona")
|
|
116
88
|
: ""}
|
|
117
89
|
|
|
118
90
|
<select onchange="if (this.value) Turbo.visit(this.value)">
|
|
119
|
-
${Array.from(
|
|
120
|
-
naturalNumbers(1, await this.getTotalPages(ctx, props))
|
|
121
|
-
).map(
|
|
91
|
+
${Array.from(naturalNumbers(1, totalIems)).map(
|
|
122
92
|
(n) => /* HTML */ `<option
|
|
123
93
|
value="${UrlWithNewParams(
|
|
124
94
|
ctx,
|
|
125
95
|
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
126
96
|
this.propsParser.overwriteProp(ctx, {
|
|
127
97
|
page: n,
|
|
128
|
-
} as Partial<
|
|
98
|
+
} as Partial<Record<string, unknown>>)
|
|
129
99
|
)}"
|
|
130
|
-
${
|
|
100
|
+
${page === n ? "selected" : ""}
|
|
131
101
|
>
|
|
132
102
|
${n}
|
|
133
103
|
</option>`
|
|
134
104
|
)}
|
|
135
105
|
</select>
|
|
136
|
-
${
|
|
137
|
-
? this.renderPageButton(ctx,
|
|
106
|
+
${page < totalIems
|
|
107
|
+
? this.renderPageButton(ctx, page + 1, "Następna strona")
|
|
138
108
|
: ""}
|
|
139
|
-
${
|
|
109
|
+
${page < totalIems
|
|
140
110
|
? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
|
|
141
111
|
: ""}
|
|
142
112
|
</center>`;
|
|
@@ -149,31 +119,33 @@ ${this.getItems(ctx, parsed_props).then(({ items }) =>
|
|
|
149
119
|
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
150
120
|
this.propsParser.overwriteProp(ctx, {
|
|
151
121
|
page,
|
|
152
|
-
} as Partial<
|
|
122
|
+
} as Partial<Record<string, unknown>>)
|
|
153
123
|
)}"
|
|
154
124
|
>${text}</a
|
|
155
125
|
>`;
|
|
156
126
|
}
|
|
157
127
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
128
|
+
async getFilterValues(ctx: BaseContext): Promise<Record<string, unknown>> {
|
|
129
|
+
const filter = {} as Record<string, unknown>;
|
|
130
|
+
const raw_values = this.extractRawValues(ctx);
|
|
131
|
+
for (const [fieldname, field] of Object.entries(this.filterFields)) {
|
|
132
|
+
// eslint-disable-next-line no-await-in-loop
|
|
133
|
+
const { parsed } = await field.getValue(ctx, raw_values);
|
|
134
|
+
filter[fieldname] = parsed;
|
|
135
|
+
}
|
|
136
|
+
return filter;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
renderFilters(ctx: BaseContext): FlatTemplatable {
|
|
140
|
+
const values = this.extractRawValues(ctx);
|
|
164
141
|
return tempstream/* HTML */ `<form method="GET">
|
|
165
|
-
${
|
|
142
|
+
${makeHiddenInputs(ctx, this.fields, values, [
|
|
166
143
|
"page",
|
|
167
|
-
...this.filterFields.map((f) => f.name),
|
|
144
|
+
...Object.values(this.filterFields).map((f) => f.name),
|
|
168
145
|
])}
|
|
169
146
|
${this.filterControls.map((control) =>
|
|
170
147
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
171
|
-
control.render(ctx,
|
|
172
|
-
values: parsed_props,
|
|
173
|
-
raw_values: raw_props,
|
|
174
|
-
errors,
|
|
175
|
-
messages: [],
|
|
176
|
-
} as FormData)
|
|
148
|
+
control.render(ctx, values)
|
|
177
149
|
)}
|
|
178
150
|
</form>`;
|
|
179
151
|
}
|
|
@@ -181,32 +153,49 @@ ${this.getItems(ctx, parsed_props).then(({ items }) =>
|
|
|
181
153
|
|
|
182
154
|
export abstract class SealiousItemListPage<
|
|
183
155
|
C extends Collection,
|
|
184
|
-
|
|
185
|
-
> extends ListPage<CollectionItem<C>,
|
|
156
|
+
F extends typeof BaseListPageFields
|
|
157
|
+
> extends ListPage<CollectionItem<C>, F> {
|
|
186
158
|
constructor(public collection: C) {
|
|
187
159
|
super();
|
|
188
160
|
}
|
|
189
161
|
|
|
190
|
-
async getTotalPages(
|
|
191
|
-
|
|
192
|
-
|
|
162
|
+
async getTotalPages(
|
|
163
|
+
ctx: BaseContext,
|
|
164
|
+
itemsPerPage: number
|
|
165
|
+
): Promise<number> {
|
|
166
|
+
const query = this.collection.list(ctx.$context);
|
|
167
|
+
await this.addFilter(ctx, query);
|
|
168
|
+
const { items } = await query.fetch();
|
|
169
|
+
return Math.ceil(items.length / itemsPerPage);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected async addFilter(
|
|
173
|
+
ctx: BaseContext,
|
|
174
|
+
list: ItemList<C>
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
const filters = await this.getFilterValues(ctx);
|
|
177
|
+
if (Object.keys(filters).length) {
|
|
178
|
+
list.filter(filters as Partial<ItemFields<C>>);
|
|
179
|
+
}
|
|
193
180
|
}
|
|
194
181
|
|
|
195
|
-
async getItems(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
async getItems(
|
|
183
|
+
ctx: BaseContext,
|
|
184
|
+
page: number,
|
|
185
|
+
itemsPerPage: number
|
|
186
|
+
): Promise<CollectionItem<C>[]> {
|
|
187
|
+
const query = this.collection.list(ctx.$context);
|
|
188
|
+
await this.addFilter(ctx, query);
|
|
189
|
+
const { items } = await query
|
|
190
|
+
.paginate({ items: itemsPerPage, page })
|
|
191
|
+
.fetch();
|
|
192
|
+
return items;
|
|
204
193
|
}
|
|
205
194
|
|
|
206
195
|
async renderItem(
|
|
207
196
|
_: BaseContext,
|
|
208
197
|
item: CollectionItem<C>
|
|
209
|
-
): Promise<
|
|
198
|
+
): Promise<FlatTemplatable> {
|
|
210
199
|
return `<div>${item.id}</div>`;
|
|
211
200
|
}
|
|
212
201
|
|
|
@@ -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<FlatTemplatable>;
|
|
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
|
+
): FlatTemplatable {
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
public abstract render(ctx: BaseContext): Promise<
|
|
35
|
+
public abstract render(ctx: BaseContext): Promise<FlatTemplatable>;
|
|
36
|
+
|
|
37
|
+
extractRawValues(ctx: BaseContext): Record<string, FormDataValue> {
|
|
38
|
+
return this.propsParser.decode(ctx);
|
|
39
|
+
}
|
|
31
40
|
}
|
package/src/page/props-parser.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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):
|
|
80
|
-
|
|
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> {
|
package/src/templates/form.ts
CHANGED
|
@@ -7,43 +7,58 @@ export async function formTemplate(
|
|
|
7
7
|
const rel = curryImportPath(newfilefullpath);
|
|
8
8
|
return `import { BaseContext } from "koa";
|
|
9
9
|
import {
|
|
10
|
+
CollectionField,
|
|
10
11
|
Form,
|
|
11
12
|
FormData,
|
|
12
|
-
|
|
13
|
+
FormDataValue,
|
|
14
|
+
SimpleFormField,
|
|
13
15
|
SimpleInput,
|
|
14
|
-
FormField,
|
|
15
|
-
collectionFieldValidator,
|
|
16
16
|
} from "@sealcode/sealgen";
|
|
17
|
-
|
|
17
|
+
import html from "../html";
|
|
18
18
|
import { Users } from "${rel("src/back/collections/collections")}";
|
|
19
|
-
import html from "${rel("src/back/html")}";
|
|
20
19
|
|
|
21
20
|
export const actionName = "${action_name}";
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const fields = {
|
|
23
|
+
email: new CollectionField(true, Users.fields.email),
|
|
24
|
+
password: new SimpleFormField(true),
|
|
25
|
+
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
export default new (class ${action_name}Form extends Form<typeof fields> {
|
|
28
|
+
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
|
29
|
+
fields = fields;
|
|
27
30
|
|
|
28
31
|
controls = [
|
|
29
|
-
new
|
|
30
|
-
new SimpleInput(
|
|
32
|
+
new SimpleInput(fields.email, { label: "Email:", type: "email" }),
|
|
33
|
+
new SimpleInput(fields.password, { label: "Password:", type: "password" }),
|
|
31
34
|
];
|
|
32
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
|
+
|
|
33
50
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
34
51
|
async canAccess(_: BaseContext) {
|
|
35
52
|
return { canAccess: true, message: "" };
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
async onSubmit(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
...values,
|
|
42
|
-
});
|
|
55
|
+
async onSubmit() {
|
|
56
|
+
//noop
|
|
57
|
+
return;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
async render(ctx: BaseContext, data: FormData
|
|
46
|
-
return html(ctx, "${action_name}", await super.render(ctx, data
|
|
60
|
+
async render(ctx: BaseContext, data: FormData) {
|
|
61
|
+
return html(ctx, "${action_name}", await super.render(ctx, data));
|
|
47
62
|
}
|
|
48
63
|
})();
|
|
49
64
|
`;
|