@sealcode/sealgen 0.3.1 → 0.4.1
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 +23 -28
- package/@types/forms/simple-form-field.d.ts +6 -0
- package/@types/forms/validator.d.ts +0 -3
- package/@types/index.d.ts +6 -4
- 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 +9 -16
- package/@types/page/props-parser.d.ts +5 -10
- package/@types/templates/page.d.ts +1 -1
- package/lib/build.js +12 -3
- 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 +16 -22
- 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 +66 -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 +18 -3
- 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 +64 -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 +38 -18
- 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/templates/page.js +5 -4
- package/lib/templates/page.js.map +1 -1
- package/lib/utils/escape-url-params.js +1 -0
- package/lib/utils/escape-url-params.js.map +1 -1
- package/package.json +4 -3
- package/src/build.ts +14 -3
- 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 +26 -23
- package/src/forms/fields/checkboxed-list.ts +61 -0
- package/src/forms/fields/collection-field.ts +54 -0
- package/src/forms/fields/field.ts +106 -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 +97 -162
- package/src/generate-routes.ts +18 -7
- package/src/index.ts +6 -4
- package/src/make-hidden-inputs.ts +22 -0
- package/src/page/list.ts +113 -126
- package/src/page/mountable.ts +105 -0
- package/src/page/page.ts +20 -11
- package/src/page/props-parser.ts +9 -67
- package/src/templates/form.ts +38 -18
- package/src/templates/list.ts +30 -20
- package/src/templates/page.ts +6 -6
- package/src/utils/escape-url-params.ts +1 -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,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 {
|
|
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
|
|
18
|
-
page:
|
|
19
|
-
itemsPerPage:
|
|
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
|
-
|
|
29
|
-
> extends Page {
|
|
23
|
+
F extends typeof BaseListPageFields
|
|
24
|
+
> extends Page<F> {
|
|
30
25
|
abstract getItems(
|
|
31
26
|
ctx: BaseContext,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
abstract getTotalPages(
|
|
33
|
+
ctx: BaseContext,
|
|
34
|
+
itemsPerPage: number,
|
|
35
|
+
values: Record<string, FormDataValue>
|
|
36
|
+
): Promise<number>;
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
abstract renderItem(
|
|
46
39
|
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
|
-
}
|
|
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
|
-
|
|
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 };
|
|
53
|
+
renderListContainer(_: BaseContext, content: Templatable): FlatTemplatable {
|
|
54
|
+
return tempstream`<div>${content}</div>`;
|
|
87
55
|
}
|
|
88
56
|
|
|
89
|
-
async render(ctx: BaseContext) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
${
|
|
108
|
-
|
|
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<
|
|
96
|
+
} as Partial<Record<string, unknown>>)
|
|
129
97
|
)}"
|
|
130
|
-
${
|
|
98
|
+
${page === n ? "selected" : ""}
|
|
131
99
|
>
|
|
132
100
|
${n}
|
|
133
101
|
</option>`
|
|
134
102
|
)}
|
|
135
103
|
</select>
|
|
136
|
-
${
|
|
137
|
-
? this.renderPageButton(ctx,
|
|
104
|
+
${page < totalIems
|
|
105
|
+
? this.renderPageButton(ctx, page + 1, "Następna strona")
|
|
138
106
|
: ""}
|
|
139
|
-
${
|
|
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<
|
|
120
|
+
} as Partial<Record<string, unknown>>)
|
|
153
121
|
)}"
|
|
154
122
|
>${text}</a
|
|
155
123
|
>`;
|
|
156
124
|
}
|
|
157
125
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
${
|
|
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,
|
|
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
|
-
|
|
185
|
-
> extends ListPage<CollectionItem<C>,
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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<
|
|
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
|
|
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
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
|
}
|
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
|
@@ -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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
22
|
+
const fields = {
|
|
23
|
+
email: new CollectionField(true, Users.fields.email),
|
|
24
|
+
password: new SimpleFormField(true),
|
|
25
|
+
};
|
|
20
26
|
|
|
21
|
-
|
|
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
|
|
25
|
-
new SimpleInput(
|
|
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(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
...values,
|
|
37
|
-
});
|
|
55
|
+
async onSubmit() {
|
|
56
|
+
//noop
|
|
57
|
+
return;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
async render(ctx: BaseContext, data: FormData
|
|
41
|
-
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));
|
|
42
62
|
}
|
|
43
63
|
})();
|
|
44
64
|
`;
|