@sealcode/sealgen 0.19.5 → 0.19.7
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/fields/field.d.ts +1 -0
- package/@types/forms/fields/fields.d.ts +2 -1
- package/@types/forms/fields/get-field-for-sealious.d.ts +2 -1
- package/@types/forms/fields/hybrid.d.ts +11 -0
- package/@types/forms/fields/item-draft.d.ts +20 -0
- package/@types/forms/form.d.ts +1 -0
- package/@types/forms/multiform.d.ts +2 -2
- package/@types/page/list.d.ts +5 -4
- package/@types/page/mountable-with-fields.d.ts +9 -2
- package/@types/page/mountable.d.ts +1 -1
- package/@types/page/page.d.ts +1 -1
- package/@types/templates/form.d.ts +2 -2
- package/lib/forms/controls/checkboxed-list-input.test.js +16 -12
- package/lib/forms/controls/file.test.js +2 -2
- package/lib/forms/controls/image.test.js +4 -4
- package/lib/forms/controls/single-reference.test.js +2 -2
- package/lib/forms/fields/field.test.js +9 -6
- package/lib/forms/fields/fields.js +2 -1
- package/lib/forms/fields/file.test.js +2 -2
- package/lib/forms/fields/get-field-for-sealious.js +6 -0
- package/lib/forms/fields/hybrid.js +37 -0
- package/lib/forms/fields/item-draft.js +34 -0
- package/lib/forms/form.js +11 -5
- package/lib/forms/form.test.js +9 -9
- package/lib/forms/multiform.js +7 -4
- package/lib/forms/multiform.test.js +7 -7
- package/lib/mount.js +4 -1
- package/lib/page/list.js +9 -6
- package/lib/page/mountable-with-fields.js +16 -1
- package/lib/page/mountable-with-fields.test.js +2 -2
- package/lib/page/mountable.js +1 -1
- package/lib/page/page.js +1 -1
- package/lib/templates/form.js +9 -18
- package/lib/templates/shared/collection-create-form.js +8 -8
- package/lib/templates/shared/collection-edit-form.js +9 -9
- package/lib/templates/shared/collection-list.js +2 -1
- package/lib/templates/shared/shared-crud-form-fields.js +4 -4
- package/lib/utils/extract-fields-from-collection.js +3 -0
- package/package.json +1 -1
- package/src/forms/controls/checkboxed-list-input.test.ts +16 -12
- package/src/forms/controls/file.test.ts +2 -2
- package/src/forms/controls/image.test.ts +4 -4
- package/src/forms/controls/single-reference.test.ts +2 -2
- package/src/forms/fields/field.test.ts +9 -6
- package/src/forms/fields/field.ts +5 -6
- package/src/forms/fields/fields.ts +2 -1
- package/src/forms/fields/file.test.ts +2 -2
- package/src/forms/fields/get-field-for-sealious.ts +8 -0
- package/src/forms/fields/hybrid.ts +47 -0
- package/src/forms/fields/item-draft.ts +44 -0
- package/src/forms/form.test.ts +15 -15
- package/src/forms/form.ts +34 -26
- package/src/forms/multiform.test.ts +7 -7
- package/src/forms/multiform.ts +10 -7
- package/src/mount.ts +11 -7
- package/src/page/list.ts +33 -28
- package/src/page/mountable-with-fields.test.ts +2 -2
- package/src/page/mountable-with-fields.ts +26 -5
- package/src/page/mountable.ts +1 -1
- package/src/page/page.ts +1 -1
- package/src/templates/form.ts +9 -18
- package/src/templates/shared/collection-create-form.ts +8 -8
- package/src/templates/shared/collection-edit-form.ts +9 -9
- package/src/templates/shared/collection-list.ts +5 -4
- package/src/templates/shared/shared-crud-form-fields.ts +5 -5
- package/src/utils/extract-fields-from-collection.ts +3 -0
package/src/forms/form.test.ts
CHANGED
|
@@ -18,9 +18,9 @@ const fields = {
|
|
|
18
18
|
|
|
19
19
|
export function form_factory(canAccessFun?: Mountable["canAccess"]): Form<typeof fields, void> {
|
|
20
20
|
const result = new (class extends Form<typeof fields, void> {
|
|
21
|
-
|
|
21
|
+
getFields = () => fields;
|
|
22
22
|
submitButtonText = "Submit";
|
|
23
|
-
|
|
23
|
+
getControls = () => [
|
|
24
24
|
new Controls.SimpleInput(fields.text, {
|
|
25
25
|
label: "This is a test:",
|
|
26
26
|
type: "password",
|
|
@@ -112,7 +112,7 @@ describe("form test", () => {
|
|
|
112
112
|
router,
|
|
113
113
|
"/",
|
|
114
114
|
form_factory(
|
|
115
|
-
async (
|
|
115
|
+
async (_ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => {
|
|
116
116
|
return { canAccess: true, message: "" };
|
|
117
117
|
}
|
|
118
118
|
),
|
|
@@ -184,9 +184,9 @@ describe("form test", () => {
|
|
|
184
184
|
"/",
|
|
185
185
|
form_factory(
|
|
186
186
|
async (ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => {
|
|
187
|
-
return ctx.$context && ctx.$context.user_id
|
|
188
|
-
|
|
189
|
-
:
|
|
187
|
+
return ctx.$context && ctx.$context.user_id ?
|
|
188
|
+
{ canAccess: true, message: "" }
|
|
189
|
+
: { canAccess: false, message: "" };
|
|
190
190
|
}
|
|
191
191
|
),
|
|
192
192
|
true
|
|
@@ -214,9 +214,9 @@ describe("form test", () => {
|
|
|
214
214
|
"/",
|
|
215
215
|
form_factory(
|
|
216
216
|
async (ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => {
|
|
217
|
-
return ctx.$context && ctx.$context.user_id
|
|
218
|
-
|
|
219
|
-
:
|
|
217
|
+
return ctx.$context && ctx.$context.user_id ?
|
|
218
|
+
{ canAccess: true, message: "" }
|
|
219
|
+
: { canAccess: false, message: "" };
|
|
220
220
|
}
|
|
221
221
|
),
|
|
222
222
|
true
|
|
@@ -242,9 +242,9 @@ describe("form test", () => {
|
|
|
242
242
|
router,
|
|
243
243
|
"/",
|
|
244
244
|
new (class extends Form<typeof fields, void> {
|
|
245
|
-
|
|
245
|
+
getFields = () => fields;
|
|
246
246
|
submitButtonText = "Submit";
|
|
247
|
-
|
|
247
|
+
getControls = () => [
|
|
248
248
|
new Controls.SimpleInput(fields.text, {
|
|
249
249
|
label: "This is a test:",
|
|
250
250
|
type: "password",
|
|
@@ -311,9 +311,9 @@ describe("form test", () => {
|
|
|
311
311
|
router,
|
|
312
312
|
"/",
|
|
313
313
|
new (class extends Form<typeof fields, void> {
|
|
314
|
-
|
|
314
|
+
getFields = () => fields;
|
|
315
315
|
submitButtonText = "Submit";
|
|
316
|
-
|
|
316
|
+
getControls = () => [
|
|
317
317
|
new Controls.SimpleInput(fields.text, {
|
|
318
318
|
label: "This is a test:",
|
|
319
319
|
type: "text",
|
|
@@ -375,8 +375,8 @@ describe("form test", () => {
|
|
|
375
375
|
router,
|
|
376
376
|
"/",
|
|
377
377
|
new (class extends Form<typeof fields, null> {
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
getFields = () => fields;
|
|
379
|
+
getControls = () => [new Controls.SimpleInput(fields.title)];
|
|
380
380
|
|
|
381
381
|
async getInitialValues() {
|
|
382
382
|
return { title: "Hello" };
|
package/src/forms/form.ts
CHANGED
|
@@ -66,15 +66,15 @@ export abstract class Form<
|
|
|
66
66
|
): Promise<JSX.Element | Readable> {
|
|
67
67
|
return tempstream /* HTML */ `<div class="form-container">
|
|
68
68
|
${this.makeOpenFormTag(ctx)}
|
|
69
|
-
${!this.controls.some((control) => control.role == "messages")
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
${!this.controls.some((control) => control.role == "messages") ?
|
|
70
|
+
this.renderMessages(ctx, data)
|
|
71
|
+
: ""}
|
|
72
72
|
${this.renderControls(
|
|
73
73
|
this.makeFormControlContext(ctx, data, show_field_errors)
|
|
74
74
|
)}
|
|
75
|
-
${this.controls.some((control) => control.role == "submit")
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
${this.controls.some((control) => control.role == "submit") ?
|
|
76
|
+
""
|
|
77
|
+
: this.makeSubmitButton()}
|
|
78
78
|
${this.makeCloseFormTag()}
|
|
79
79
|
</div>`;
|
|
80
80
|
}
|
|
@@ -102,9 +102,10 @@ export abstract class Form<
|
|
|
102
102
|
form_messages: FormMessage[],
|
|
103
103
|
field_errors: FieldMessages<keyof Fields>
|
|
104
104
|
): Promise<FormReaction> {
|
|
105
|
-
const messages =
|
|
106
|
-
? form_messages
|
|
107
|
-
|
|
105
|
+
const messages =
|
|
106
|
+
form_messages.length ? form_messages : (
|
|
107
|
+
[<const>{ type: "error", text: "Some fields are invalid" }]
|
|
108
|
+
);
|
|
108
109
|
return {
|
|
109
110
|
action: "stay",
|
|
110
111
|
content: await this.render(
|
|
@@ -217,12 +218,13 @@ export abstract class Form<
|
|
|
217
218
|
// eslint-disable-next-line no-console
|
|
218
219
|
console.dir(e, { depth: 5 });
|
|
219
220
|
const message =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
(
|
|
222
|
+
is(e, predicates.object) &&
|
|
223
|
+
hasFieldOfType(e, "message", predicates.string)
|
|
224
|
+
) ?
|
|
225
|
+
e?.message
|
|
226
|
+
: is(e, predicates.string) ? e
|
|
227
|
+
: "Wystąpił błąd";
|
|
226
228
|
|
|
227
229
|
return this.onError(
|
|
228
230
|
ctx,
|
|
@@ -241,6 +243,20 @@ export abstract class Form<
|
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
|
|
246
|
+
init(path: string, router: Router) {
|
|
247
|
+
super.init(path, router);
|
|
248
|
+
if (this.initialized) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// for use with other subroutes or middlewares that individual controls
|
|
252
|
+
// might register
|
|
253
|
+
const subrouter = new Router();
|
|
254
|
+
for (const control of this.controls) {
|
|
255
|
+
control.mount(subrouter, this);
|
|
256
|
+
}
|
|
257
|
+
router.use(path, subrouter.routes(), subrouter.allowedMethods());
|
|
258
|
+
}
|
|
259
|
+
|
|
244
260
|
public mount(router: Router, path: string): void {
|
|
245
261
|
router.use(path, async (ctx, next) => {
|
|
246
262
|
const result = await this.canAccess(ctx);
|
|
@@ -255,14 +271,6 @@ export abstract class Form<
|
|
|
255
271
|
await next();
|
|
256
272
|
});
|
|
257
273
|
|
|
258
|
-
// for use with other subroutes or middlewares that individual controls
|
|
259
|
-
// might register
|
|
260
|
-
const subrouter = new Router();
|
|
261
|
-
for (const control of this.controls) {
|
|
262
|
-
control.mount(subrouter, this);
|
|
263
|
-
}
|
|
264
|
-
router.use(path, subrouter.routes(), subrouter.allowedMethods());
|
|
265
|
-
|
|
266
274
|
router.get(path, async (ctx) => {
|
|
267
275
|
ctx.type = "html";
|
|
268
276
|
ctx.body = await this.render(
|
|
@@ -292,9 +300,9 @@ export abstract class Form<
|
|
|
292
300
|
async extractRawValues(
|
|
293
301
|
ctx: Context
|
|
294
302
|
): Promise<Record<string, FormDataValue>> {
|
|
295
|
-
return Object.keys(ctx.$body).length
|
|
296
|
-
|
|
297
|
-
:
|
|
303
|
+
return Object.keys(ctx.$body).length ?
|
|
304
|
+
ctx.$body
|
|
305
|
+
: this.getInitialValues(ctx);
|
|
298
306
|
}
|
|
299
307
|
|
|
300
308
|
static initFieldNames(fields: Record<string, FormField>): void {
|
|
@@ -27,8 +27,8 @@ const fields1 = {
|
|
|
27
27
|
|
|
28
28
|
const form1 = new (class extends Form<typeof fields1, void> {
|
|
29
29
|
defaultSuccessMessage = "Form1 done!";
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
getControls = () => [new FormHeader("form1"), new SimpleInput(fields1.email)];
|
|
31
|
+
getFields = () => fields1;
|
|
32
32
|
onSubmit() {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -40,8 +40,8 @@ const fields2 = {
|
|
|
40
40
|
|
|
41
41
|
const form2 = new (class extends Form<Record<never, unknown>, void> {
|
|
42
42
|
defaultSuccessMessage = "Form2 done!";
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
getControls = () => [new FormHeader("form2"), new SimpleInput(fields2.name)];
|
|
44
|
+
getFields = () => fields2;
|
|
45
45
|
onSubmit() {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
@@ -427,7 +427,7 @@ describe("multiform test", () => {
|
|
|
427
427
|
form1: new (class extends Form<typeof fields1, void> {
|
|
428
428
|
fields = fields1;
|
|
429
429
|
submitButtonText = "Submit";
|
|
430
|
-
|
|
430
|
+
getControls = () => [
|
|
431
431
|
new SimpleInput(fields1.email, {
|
|
432
432
|
label: "This is a test:",
|
|
433
433
|
type: "text",
|
|
@@ -500,9 +500,9 @@ describe("multiform test", () => {
|
|
|
500
500
|
name = "multiform_test";
|
|
501
501
|
forms = {
|
|
502
502
|
form1: new (class extends Form<typeof fields, void> {
|
|
503
|
-
|
|
503
|
+
getFields = () => fields;
|
|
504
504
|
submitButtonText = "Submit";
|
|
505
|
-
|
|
505
|
+
getControls = () => [
|
|
506
506
|
new SimpleInput(fields.text, {
|
|
507
507
|
label: "This is a test:",
|
|
508
508
|
type: "text",
|
package/src/forms/multiform.ts
CHANGED
|
@@ -11,12 +11,15 @@ import { Form } from "./form.js";
|
|
|
11
11
|
const FIELD_PREFIX_SEPARATOR = "___";
|
|
12
12
|
|
|
13
13
|
export class Multiform extends MountableWithFields {
|
|
14
|
-
|
|
14
|
+
getControls = () => [];
|
|
15
15
|
public name: string;
|
|
16
16
|
public forms: Record<string, Form<Fields, unknown>>;
|
|
17
17
|
|
|
18
|
-
init(): void {
|
|
19
|
-
|
|
18
|
+
init(path: string, router: Router): void {
|
|
19
|
+
if (this.initialized) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
super.init(path, router);
|
|
20
23
|
for (const [key, form] of Object.entries(this.forms)) {
|
|
21
24
|
if (key !== attribute(key)) {
|
|
22
25
|
throw new Error(
|
|
@@ -30,7 +33,7 @@ export class Multiform extends MountableWithFields {
|
|
|
30
33
|
form.field_names_prefix = key + FIELD_PREFIX_SEPARATOR;
|
|
31
34
|
form.form_id = this.name;
|
|
32
35
|
form.action = "./" + key;
|
|
33
|
-
form.init();
|
|
36
|
+
form.init(path, router);
|
|
34
37
|
form.extractRawValues = async (context) =>
|
|
35
38
|
this.getSubformRawValues(context, key);
|
|
36
39
|
const submit_button_id = this.name + "_" + key + "_submit";
|
|
@@ -82,9 +85,9 @@ export class Multiform extends MountableWithFields {
|
|
|
82
85
|
throw new Error("Wrong turbo-frame header value type");
|
|
83
86
|
}
|
|
84
87
|
const forms_to_render =
|
|
85
|
-
requested_frame && this.forms[requested_frame]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
requested_frame && this.forms[requested_frame] ?
|
|
89
|
+
[<const>[requested_frame, this.forms[requested_frame]]]
|
|
90
|
+
: Object.entries(this.forms);
|
|
88
91
|
return forms_to_render;
|
|
89
92
|
}
|
|
90
93
|
|
package/src/mount.ts
CHANGED
|
@@ -19,9 +19,9 @@ export function mount(
|
|
|
19
19
|
url: string | { rawURL: string },
|
|
20
20
|
mountable: Mountable,
|
|
21
21
|
use_dummy_app = false,
|
|
22
|
-
file_manager: FileManager | undefined = use_dummy_app
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
file_manager: FileManager | undefined = use_dummy_app ?
|
|
23
|
+
new FileManager("/tmp", "/uploaded_files")
|
|
24
|
+
: undefined
|
|
25
25
|
): void {
|
|
26
26
|
const raw_url = typeof url === "string" ? url : url.rawURL;
|
|
27
27
|
|
|
@@ -35,8 +35,9 @@ export function mount(
|
|
|
35
35
|
|
|
36
36
|
const fileManager = new FileManager("/tmp", "/uploaded_files");
|
|
37
37
|
|
|
38
|
-
const args =
|
|
39
|
-
?
|
|
38
|
+
const args =
|
|
39
|
+
use_dummy_app ?
|
|
40
|
+
[
|
|
40
41
|
async (ctx: Context, next: any) => {
|
|
41
42
|
// this dummy is a temporary solution it's ilustrating the ways that
|
|
42
43
|
// sealgen is coupled with sealious. Definitely room for improvement in this
|
|
@@ -66,14 +67,17 @@ export function mount(
|
|
|
66
67
|
SealiousMiddlewares.parseBody(file_manager),
|
|
67
68
|
handleHtmlPromise,
|
|
68
69
|
]
|
|
69
|
-
:
|
|
70
|
+
: [
|
|
70
71
|
SealiousMiddlewares.extractContext(),
|
|
71
72
|
SealiousMiddlewares.parseBody(file_manager),
|
|
72
73
|
handleHtmlPromise,
|
|
73
74
|
];
|
|
74
75
|
|
|
75
76
|
router.use(raw_url, ...args);
|
|
76
|
-
|
|
77
|
+
router.use(raw_url, async (_ctx, next) => {
|
|
78
|
+
mountable.init(raw_url, router);
|
|
79
|
+
await next();
|
|
80
|
+
});
|
|
77
81
|
|
|
78
82
|
// to automatically add trailing slashes:
|
|
79
83
|
router.get(raw_url.slice(0, -1), async (ctx, next) => {
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
${page > 1
|
|
135
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
${page < totalIems
|
|
164
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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> = {};
|
package/src/page/mountable.ts
CHANGED
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
|
-
|
|
18
|
+
getControls = () => [];
|
|
19
19
|
|
|
20
20
|
mount(router: Router, path: string): void {
|
|
21
21
|
router.get(path, async (ctx) => {
|
package/src/templates/form.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
|
59
|
+
export default new (class ${action_name}Form extends Form<ReturnType<typeof getFields>, void> {
|
|
67
60
|
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
|
68
|
-
|
|
61
|
+
getFields = getFields;
|
|
69
62
|
|
|
70
|
-
|
|
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
|
|
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 {${
|
|
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
|
-
|
|
32
|
-
|
|
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(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:
|
|
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(
|
|
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
|
}
|