@sealcode/sealgen 0.15.17 → 0.15.19
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/src/forms/controls/checkbox.d.ts +1 -1
- package/@types/src/forms/controls/tickable.d.ts +4 -3
- package/@types/src/forms/fields/field.d.ts +7 -2
- package/@types/src/page/list.d.ts +2 -2
- package/lib/forms/controls/checkbox-group.js +3 -2
- package/lib/forms/controls/checkboxed-list-input.js +1 -1
- package/lib/forms/controls/file.js +5 -2
- package/lib/forms/controls/radio.js +1 -1
- package/lib/forms/controls/simple-input.js +4 -2
- package/lib/forms/controls/tickable.js +7 -5
- package/lib/forms/fields/field.js +33 -25
- package/lib/forms/fields/field.test.js +47 -0
- package/lib/forms/fields/file.js +1 -1
- package/lib/forms/form.test.js +1 -0
- package/lib/page/list.js +11 -3
- package/lib/page/mountable-with-fields.js +3 -3
- package/package.json +1 -1
- package/src/forms/controls/checkbox-group.ts +2 -1
- package/src/forms/controls/checkbox.ts +1 -1
- package/src/forms/controls/checkboxed-list-input.ts +1 -1
- package/src/forms/controls/file.ts +5 -2
- package/src/forms/controls/radio.ts +1 -1
- package/src/forms/controls/simple-input.ts +4 -2
- package/src/forms/controls/tickable.ts +10 -6
- package/src/forms/fields/field.test.ts +53 -0
- package/src/forms/fields/field.ts +55 -31
- package/src/forms/fields/file.ts +1 -1
- package/src/forms/form.test.ts +1 -0
- package/src/page/list.ts +13 -4
- package/src/page/mountable-with-fields.ts +11 -12
|
@@ -10,6 +10,6 @@ export declare class Checkbox extends Tickable<boolean> {
|
|
|
10
10
|
export declare class CheckboxWithValue extends Checkbox {
|
|
11
11
|
field: FormField<boolean, boolean>;
|
|
12
12
|
value: string;
|
|
13
|
-
constructor(field: FormField<boolean, boolean>, options: TickableOptions
|
|
13
|
+
constructor(field: FormField<boolean, boolean>, options: TickableOptions<boolean>, value: string);
|
|
14
14
|
getValueAttribute(): string;
|
|
15
15
|
}
|
|
@@ -4,17 +4,18 @@ import { FormField } from "../fields/field.js";
|
|
|
4
4
|
import { FormDataValue } from "../form-types.js";
|
|
5
5
|
import { FormControlContext } from "./form-control.js";
|
|
6
6
|
import { FormFieldControl } from "./form-field-control.js";
|
|
7
|
-
export type TickableOptions = {
|
|
7
|
+
export type TickableOptions<T> = {
|
|
8
8
|
id?: string;
|
|
9
9
|
name?: string;
|
|
10
10
|
label?: string;
|
|
11
11
|
hide_errors?: boolean;
|
|
12
12
|
readonly?: boolean;
|
|
13
|
+
default_value: T;
|
|
13
14
|
};
|
|
14
15
|
export declare abstract class Tickable<T> extends FormFieldControl {
|
|
15
16
|
field: FormField<boolean, T>;
|
|
16
|
-
options: TickableOptions
|
|
17
|
-
constructor(field: FormField<boolean, T>, options
|
|
17
|
+
options: TickableOptions<T>;
|
|
18
|
+
constructor(field: FormField<boolean, T>, options: TickableOptions<T>);
|
|
18
19
|
type: "checkbox" | "radio";
|
|
19
20
|
abstract getValueAttribute(ctx: Context, data: Record<string, FormDataValue>, value: T): string;
|
|
20
21
|
abstract isChecked(ctx: Context, data: Record<string, FormDataValue>, value: T): boolean;
|
|
@@ -32,10 +32,15 @@ export declare abstract class FormField<Required extends boolean = boolean, Pars
|
|
|
32
32
|
abstract getEmptyValue(): ParsedValue;
|
|
33
33
|
isValueValid(_ctx: Context, _value: ParsedValue): Promise<FormFieldValidationResponse>;
|
|
34
34
|
abstract parse(ctx: Context, raw_value: FormDataValue): Promise<FieldParseResult<ParsedValue>>;
|
|
35
|
-
getValue(ctx: Context, all_form_values: Record<string, FormDataValue
|
|
35
|
+
getValue(ctx: Context, all_form_values: Record<string, FormDataValue>, validate: true): Promise<{
|
|
36
36
|
valid: boolean;
|
|
37
37
|
message: string;
|
|
38
|
-
parsed: ParsedValue;
|
|
38
|
+
parsed: ParsedValue | null;
|
|
39
|
+
raw: FormDataValue;
|
|
40
|
+
}>;
|
|
41
|
+
getValue(ctx: Context, all_form_values: Record<string, FormDataValue>, validate?: false): Promise<{
|
|
42
|
+
message: string;
|
|
43
|
+
parsed: ParsedValue | null;
|
|
39
44
|
raw: FormDataValue;
|
|
40
45
|
}>;
|
|
41
46
|
setMapToFilter(fn: (parsed: ParsedValue) => unknown): this;
|
|
@@ -19,7 +19,7 @@ export type ListSort = {
|
|
|
19
19
|
order: "asc" | "desc";
|
|
20
20
|
};
|
|
21
21
|
export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFields> extends Page<F> {
|
|
22
|
-
abstract getItems(ctx: Context, page: number, itemsPerPage: number, values: Record<string, FormDataValue>): Promise<ItemType[]>;
|
|
22
|
+
abstract getItems(ctx: Context, page: number, itemsPerPage: number | null, values: Record<string, FormDataValue>): Promise<ItemType[]>;
|
|
23
23
|
abstract getTotalPages(ctx: Context, itemsPerPage: number, values: Record<string, FormDataValue>): Promise<number>;
|
|
24
24
|
abstract renderItem(ctx: Context, item: ItemType, index: number): Promise<FlatTemplatable>;
|
|
25
25
|
filterFields: Record<string, FormField>;
|
|
@@ -28,7 +28,7 @@ export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFi
|
|
|
28
28
|
renderListContainer(_: Context, content: Templatable): FlatTemplatable;
|
|
29
29
|
getPaginationConfig(ctx: Context): Promise<{
|
|
30
30
|
page: number;
|
|
31
|
-
itemsPerPage: number;
|
|
31
|
+
itemsPerPage: number | null;
|
|
32
32
|
}>;
|
|
33
33
|
renderItems(ctx: Context, values?: Record<string, FormDataValue>, items?: ItemType[]): Promise<FlatTemplatable>;
|
|
34
34
|
render(ctx: Context): Promise<FlatTemplatable>;
|
|
@@ -28,11 +28,12 @@ class CheckboxGroup extends FormFieldControl {
|
|
|
28
28
|
false,
|
|
29
29
|
this.field.name,
|
|
30
30
|
false,
|
|
31
|
-
values.includes(option)
|
|
31
|
+
(values || []).includes(option)
|
|
32
32
|
),
|
|
33
33
|
{
|
|
34
34
|
id: this.field.name + "__" + option,
|
|
35
|
-
label: (this.options.checkboxNameToLabel || ((s) => s))(option)
|
|
35
|
+
label: (this.options.checkboxNameToLabel || ((s) => s))(option),
|
|
36
|
+
default_value: false
|
|
36
37
|
},
|
|
37
38
|
option
|
|
38
39
|
)
|
|
@@ -12,7 +12,7 @@ class CheckboxedListInput extends FormFieldControl {
|
|
|
12
12
|
fctx.ctx,
|
|
13
13
|
fctx.data
|
|
14
14
|
);
|
|
15
|
-
const pickedValues = Object.entries(currentValue).filter(([, is_on]) => is_on).map(([key]) => key);
|
|
15
|
+
const pickedValues = Object.entries(currentValue || {}).filter(([, is_on]) => is_on).map(([key]) => key);
|
|
16
16
|
const [options, isVisible] = await Promise.all([
|
|
17
17
|
this.field.generateOptions(fctx.ctx),
|
|
18
18
|
this.field.isVisible(fctx.ctx)
|
|
@@ -16,7 +16,10 @@ class File extends SimpleInput {
|
|
|
16
16
|
</div>`;
|
|
17
17
|
}
|
|
18
18
|
async preInput(fctx, data) {
|
|
19
|
-
|
|
19
|
+
let { parsed } = await this.field.getValue(fctx.ctx, data);
|
|
20
|
+
if (parsed === null) {
|
|
21
|
+
parsed = { old: null, new: null };
|
|
22
|
+
}
|
|
20
23
|
return tempstream`${this.renderFilePreview(
|
|
21
24
|
fctx.ctx,
|
|
22
25
|
parsed.old
|
|
@@ -36,7 +39,7 @@ class File extends SimpleInput {
|
|
|
36
39
|
return {
|
|
37
40
|
...original,
|
|
38
41
|
name: this.field.name + "[new]",
|
|
39
|
-
required: original.required && !parsed
|
|
42
|
+
required: original.required && !parsed?.old,
|
|
40
43
|
value: false
|
|
41
44
|
};
|
|
42
45
|
}
|
|
@@ -28,7 +28,8 @@ class SimpleInput extends FormFieldControl {
|
|
|
28
28
|
const required = this.field.required;
|
|
29
29
|
const { parsed, raw, valid } = await this.field.getValue(
|
|
30
30
|
fctx.ctx,
|
|
31
|
-
fctx.data
|
|
31
|
+
fctx.data,
|
|
32
|
+
true
|
|
32
33
|
);
|
|
33
34
|
return Object.fromEntries([
|
|
34
35
|
["id", this.getID()],
|
|
@@ -68,7 +69,8 @@ class SimpleInput extends FormFieldControl {
|
|
|
68
69
|
const label = this.getLabel();
|
|
69
70
|
const { valid, message } = await this.field.getValue(
|
|
70
71
|
fctx.ctx,
|
|
71
|
-
fctx.data
|
|
72
|
+
fctx.data,
|
|
73
|
+
true
|
|
72
74
|
);
|
|
73
75
|
return inputWrapper(
|
|
74
76
|
this.getWrapperModifiers(),
|
|
@@ -2,7 +2,7 @@ import { attribute } from "../../sanitize.js";
|
|
|
2
2
|
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper.js";
|
|
3
3
|
import { FormFieldControl } from "./form-field-control.js";
|
|
4
4
|
class Tickable extends FormFieldControl {
|
|
5
|
-
constructor(field, options
|
|
5
|
+
constructor(field, options) {
|
|
6
6
|
super([field]);
|
|
7
7
|
this.field = field;
|
|
8
8
|
this.options = options;
|
|
@@ -21,10 +21,12 @@ class Tickable extends FormFieldControl {
|
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
23
|
async render(fctx) {
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const {
|
|
25
|
+
parsed: _parsed,
|
|
26
|
+
valid,
|
|
27
|
+
message
|
|
28
|
+
} = await this.field.getValue(fctx.ctx, fctx.data, true);
|
|
29
|
+
const parsed = _parsed === null ? this.options.default_value : _parsed;
|
|
28
30
|
const value = this.getValueAttribute(fctx.ctx, fctx.data, parsed);
|
|
29
31
|
const id = this.makeInputID(fctx.ctx, fctx.data, parsed);
|
|
30
32
|
const label = this.options.label != void 0 ? this.options.label : this.field.name;
|
|
@@ -15,32 +15,36 @@ class FormField {
|
|
|
15
15
|
this.name = fieldname;
|
|
16
16
|
}
|
|
17
17
|
async _validate(ctx, value) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
if (value === "" || value === null || value === void 0) {
|
|
19
|
+
if (this.required) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
message: t(
|
|
23
|
+
ctx,
|
|
24
|
+
"field_is_required",
|
|
25
|
+
[],
|
|
26
|
+
"This field is required"
|
|
27
|
+
)
|
|
28
|
+
};
|
|
29
|
+
} else {
|
|
30
|
+
return { valid: true, message: "Empty, but not required" };
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
return this.isValueValid(ctx, value);
|
|
30
34
|
}
|
|
31
35
|
async isValueValid(_ctx, _value) {
|
|
32
36
|
return { valid: true, message: "" };
|
|
33
37
|
}
|
|
34
|
-
async getValue(ctx, all_form_values) {
|
|
35
|
-
|
|
38
|
+
async getValue(ctx, all_form_values, validate = false) {
|
|
39
|
+
const raw = all_form_values[this.name];
|
|
40
|
+
if (raw == void 0 || raw == null || raw == "") {
|
|
36
41
|
return {
|
|
37
|
-
parsed:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
parsed: null,
|
|
43
|
+
message: "empty value",
|
|
44
|
+
raw,
|
|
45
|
+
...validate ? { valid: !this.required } : {}
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
|
-
const raw = all_form_values[this.name];
|
|
44
48
|
const {
|
|
45
49
|
parsable,
|
|
46
50
|
parsed_value,
|
|
@@ -48,19 +52,23 @@ class FormField {
|
|
|
48
52
|
} = await this.parse(ctx, raw);
|
|
49
53
|
if (!parsable) {
|
|
50
54
|
return {
|
|
51
|
-
valid: false,
|
|
55
|
+
...validate ? { valid: false } : {},
|
|
52
56
|
parsed: this.getEmptyValue(),
|
|
53
57
|
raw,
|
|
54
58
|
message: parse_error || "Error"
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
if (validate) {
|
|
62
|
+
const { valid, message } = await this._validate(ctx, parsed_value);
|
|
63
|
+
return {
|
|
64
|
+
parsed: parsed_value,
|
|
65
|
+
...validate ? { valid } : {},
|
|
66
|
+
message,
|
|
67
|
+
raw: all_form_values[this.name]
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
return { parsed: parsed_value, raw, message: "parsed" };
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
setMapToFilter(fn) {
|
|
66
74
|
this.mapToFilter = fn;
|
|
@@ -4,6 +4,7 @@ import Koa from "koa";
|
|
|
4
4
|
import { mount } from "../../mount.js";
|
|
5
5
|
import { Form } from "../form.js";
|
|
6
6
|
import { getBrowser } from "../../utils/browser-creator.js";
|
|
7
|
+
import assert from "assert";
|
|
7
8
|
import { SimpleFormField } from "./simple-form-field.js";
|
|
8
9
|
import { SimpleInput } from "../controls/simple-input.js";
|
|
9
10
|
describe("basic field", () => {
|
|
@@ -53,6 +54,52 @@ describe("basic field", () => {
|
|
|
53
54
|
page = await context.newPage();
|
|
54
55
|
await page.goto(`http://localhost:${port}`);
|
|
55
56
|
});
|
|
57
|
+
it("Doesn't call `isValueValid` and `parse` if the value is empty", async () => {
|
|
58
|
+
const port = await getPort();
|
|
59
|
+
console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
|
|
60
|
+
const app = new Koa();
|
|
61
|
+
const router = new Router();
|
|
62
|
+
let is_value_valid_called = false;
|
|
63
|
+
let parse_called = false;
|
|
64
|
+
const fields = {
|
|
65
|
+
text: new class MockField extends SimpleFormField {
|
|
66
|
+
async isValueValid() {
|
|
67
|
+
is_value_valid_called = true;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
async parse() {
|
|
71
|
+
parse_called = true;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}(false)
|
|
75
|
+
};
|
|
76
|
+
mount(
|
|
77
|
+
router,
|
|
78
|
+
"/",
|
|
79
|
+
new class extends Form {
|
|
80
|
+
constructor() {
|
|
81
|
+
super(...arguments);
|
|
82
|
+
this.fields = fields;
|
|
83
|
+
this.controls = [new SimpleInput(fields.text)];
|
|
84
|
+
}
|
|
85
|
+
async onSubmit() {
|
|
86
|
+
}
|
|
87
|
+
async canAccess(_) {
|
|
88
|
+
return { canAccess: true, message: "" };
|
|
89
|
+
}
|
|
90
|
+
}(),
|
|
91
|
+
true
|
|
92
|
+
);
|
|
93
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
94
|
+
server = app.listen(port);
|
|
95
|
+
browser = await getBrowser();
|
|
96
|
+
const context = await browser.newContext();
|
|
97
|
+
page = await context.newPage();
|
|
98
|
+
await page.goto(`http://localhost:${port}`);
|
|
99
|
+
await page.getByRole("button", { name: "Wy\u015Blij" }).click();
|
|
100
|
+
assert(is_value_valid_called == false);
|
|
101
|
+
assert(parse_called == false);
|
|
102
|
+
});
|
|
56
103
|
});
|
|
57
104
|
});
|
|
58
105
|
//# sourceMappingURL=field.test.js.map
|
package/lib/forms/fields/file.js
CHANGED
package/lib/forms/form.test.js
CHANGED
package/lib/page/list.js
CHANGED
|
@@ -6,9 +6,10 @@ import qs from "qs";
|
|
|
6
6
|
import { NumberField } from "../forms/fields/number.js";
|
|
7
7
|
import { SimpleFormField } from "../forms/fields/simple-form-field.js";
|
|
8
8
|
const BasePagePropsShape = {};
|
|
9
|
+
const DEFAULT_ITEMS_PER_PAGE = 12;
|
|
9
10
|
const BaseListPageFields = {
|
|
10
11
|
page: new NumberField(false, 1),
|
|
11
|
-
itemsPerPage: new NumberField(false,
|
|
12
|
+
itemsPerPage: new NumberField(false, DEFAULT_ITEMS_PER_PAGE),
|
|
12
13
|
sort: new SimpleFormField(false)
|
|
13
14
|
};
|
|
14
15
|
const SORT_SEPARATOR = ":";
|
|
@@ -48,7 +49,10 @@ class ListPage extends Page {
|
|
|
48
49
|
}
|
|
49
50
|
async getPaginationConfig(ctx) {
|
|
50
51
|
const values = await this.extractRawValues(ctx);
|
|
51
|
-
|
|
52
|
+
let { parsed: page } = await this.fields.page.getValue(ctx, values);
|
|
53
|
+
if (!page) {
|
|
54
|
+
page = 1;
|
|
55
|
+
}
|
|
52
56
|
const { parsed: itemsPerPage } = await this.fields.itemsPerPage.getValue(ctx, values);
|
|
53
57
|
return { page, itemsPerPage };
|
|
54
58
|
}
|
|
@@ -70,7 +74,11 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
|
|
|
70
74
|
}
|
|
71
75
|
async renderPagination(ctx, values) {
|
|
72
76
|
const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
|
|
73
|
-
const totalIems = await this.getTotalPages(
|
|
77
|
+
const totalIems = await this.getTotalPages(
|
|
78
|
+
ctx,
|
|
79
|
+
itemsPerPage || DEFAULT_ITEMS_PER_PAGE,
|
|
80
|
+
values
|
|
81
|
+
);
|
|
74
82
|
return tempstream`<div class="list-pagination">
|
|
75
83
|
<div class="list-pagination__left">
|
|
76
84
|
${page > 1 ? this.renderPageButton(ctx, 1, "Pierwsza strona") : ""}
|
|
@@ -42,9 +42,9 @@ class MountableWithFields extends Mountable {
|
|
|
42
42
|
let valid = true;
|
|
43
43
|
const form_messages = [];
|
|
44
44
|
await Promise.all(
|
|
45
|
-
Object.
|
|
45
|
+
Object.keys(this.fields).map(async (key) => {
|
|
46
46
|
const field = this.fields[key];
|
|
47
|
-
const { valid: fieldvalid, message: fieldmessage } = await field.getValue(ctx, values);
|
|
47
|
+
const { valid: fieldvalid, message: fieldmessage } = await field.getValue(ctx, values, true);
|
|
48
48
|
if (!fieldvalid) {
|
|
49
49
|
valid = false;
|
|
50
50
|
field_errors[field.name] = fieldmessage;
|
|
@@ -81,7 +81,7 @@ class MountableWithFields extends Mountable {
|
|
|
81
81
|
const result = {};
|
|
82
82
|
const promises = Object.entries(this.fields).map(
|
|
83
83
|
async ([key, field]) => {
|
|
84
|
-
const { parsed } = await field.getValue(ctx, raw_values);
|
|
84
|
+
const { parsed } = await field.getValue(ctx, raw_values, false);
|
|
85
85
|
result[key] = parsed;
|
|
86
86
|
}
|
|
87
87
|
);
|
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ export class CheckboxGroup<
|
|
|
51
51
|
false,
|
|
52
52
|
this.field.name,
|
|
53
53
|
false,
|
|
54
|
-
values.includes(option)
|
|
54
|
+
(values || []).includes(option)
|
|
55
55
|
),
|
|
56
56
|
{
|
|
57
57
|
id: this.field.name + "__" + option,
|
|
@@ -59,6 +59,7 @@ export class CheckboxGroup<
|
|
|
59
59
|
this.options.checkboxNameToLabel ||
|
|
60
60
|
((s: string) => s)
|
|
61
61
|
)(option),
|
|
62
|
+
default_value: false,
|
|
62
63
|
},
|
|
63
64
|
option
|
|
64
65
|
)
|
|
@@ -22,7 +22,7 @@ export class Checkbox extends Tickable<boolean> {
|
|
|
22
22
|
export class CheckboxWithValue extends Checkbox {
|
|
23
23
|
constructor(
|
|
24
24
|
public field: FormField<boolean, boolean>,
|
|
25
|
-
options: TickableOptions
|
|
25
|
+
options: TickableOptions<boolean>,
|
|
26
26
|
public value: string
|
|
27
27
|
) {
|
|
28
28
|
super(field, options);
|
|
@@ -17,7 +17,7 @@ export class CheckboxedListInput extends FormFieldControl {
|
|
|
17
17
|
fctx.ctx,
|
|
18
18
|
fctx.data
|
|
19
19
|
);
|
|
20
|
-
const pickedValues = Object.entries(currentValue)
|
|
20
|
+
const pickedValues = Object.entries(currentValue || {})
|
|
21
21
|
.filter(([, is_on]) => is_on)
|
|
22
22
|
.map(([key]) => key);
|
|
23
23
|
const [options, isVisible] = await Promise.all([
|
|
@@ -30,7 +30,10 @@ export class File extends SimpleInput<FileContainer> {
|
|
|
30
30
|
fctx: FormControlContext,
|
|
31
31
|
data: Record<string, FormDataValue>
|
|
32
32
|
) {
|
|
33
|
-
|
|
33
|
+
let { parsed } = await this.field.getValue(fctx.ctx, data);
|
|
34
|
+
if (parsed === null) {
|
|
35
|
+
parsed = { old: null, new: null };
|
|
36
|
+
}
|
|
34
37
|
return tempstream/* HTML */ `${this.renderFilePreview(
|
|
35
38
|
fctx.ctx,
|
|
36
39
|
parsed.old
|
|
@@ -53,7 +56,7 @@ export class File extends SimpleInput<FileContainer> {
|
|
|
53
56
|
return {
|
|
54
57
|
...original,
|
|
55
58
|
name: this.field.name + "[new]",
|
|
56
|
-
required: original.required && !parsed
|
|
59
|
+
required: original.required && !parsed?.old,
|
|
57
60
|
value: false,
|
|
58
61
|
};
|
|
59
62
|
}
|
|
@@ -80,7 +80,8 @@ export class SimpleInput<
|
|
|
80
80
|
const required = this.field.required;
|
|
81
81
|
const { parsed, raw, valid } = await this.field.getValue(
|
|
82
82
|
fctx.ctx,
|
|
83
|
-
fctx.data
|
|
83
|
+
fctx.data,
|
|
84
|
+
true
|
|
84
85
|
);
|
|
85
86
|
return Object.fromEntries([
|
|
86
87
|
<const>["id", this.getID()],
|
|
@@ -132,7 +133,8 @@ export class SimpleInput<
|
|
|
132
133
|
|
|
133
134
|
const { valid, message } = await this.field.getValue(
|
|
134
135
|
fctx.ctx,
|
|
135
|
-
fctx.data
|
|
136
|
+
fctx.data,
|
|
137
|
+
true
|
|
136
138
|
);
|
|
137
139
|
return inputWrapper(
|
|
138
140
|
this.getWrapperModifiers(),
|
|
@@ -7,18 +7,19 @@ import { FormDataValue } from "../form-types.js";
|
|
|
7
7
|
import { FormControlContext } from "./form-control.js";
|
|
8
8
|
import { FormFieldControl } from "./form-field-control.js";
|
|
9
9
|
|
|
10
|
-
export type TickableOptions = {
|
|
10
|
+
export type TickableOptions<T> = {
|
|
11
11
|
id?: string;
|
|
12
12
|
name?: string;
|
|
13
13
|
label?: string;
|
|
14
14
|
hide_errors?: boolean;
|
|
15
15
|
readonly?: boolean;
|
|
16
|
+
default_value: T;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export abstract class Tickable<T> extends FormFieldControl {
|
|
19
20
|
constructor(
|
|
20
21
|
public field: FormField<boolean, T>,
|
|
21
|
-
public options: TickableOptions
|
|
22
|
+
public options: TickableOptions<T>
|
|
22
23
|
) {
|
|
23
24
|
super([field]);
|
|
24
25
|
}
|
|
@@ -65,10 +66,13 @@ export abstract class Tickable<T> extends FormFieldControl {
|
|
|
65
66
|
|
|
66
67
|
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
67
68
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
68
|
-
const {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const {
|
|
70
|
+
parsed: _parsed,
|
|
71
|
+
valid,
|
|
72
|
+
message,
|
|
73
|
+
} = await this.field.getValue(fctx.ctx, fctx.data, true);
|
|
74
|
+
const parsed = _parsed === null ? this.options.default_value : _parsed;
|
|
75
|
+
|
|
72
76
|
const value = this.getValueAttribute(fctx.ctx, fctx.data, parsed);
|
|
73
77
|
const id = this.makeInputID(fctx.ctx, fctx.data, parsed);
|
|
74
78
|
const label =
|
|
@@ -10,6 +10,7 @@ import assert from "assert";
|
|
|
10
10
|
import { SimpleFormField } from "./simple-form-field.js";
|
|
11
11
|
import { SimpleInput } from "../controls/simple-input.js";
|
|
12
12
|
import { FormData } from "../form-types.js";
|
|
13
|
+
import { Field } from "sealious";
|
|
13
14
|
|
|
14
15
|
describe("basic field", () => {
|
|
15
16
|
describe("basic tests", async () => {
|
|
@@ -63,5 +64,57 @@ describe("basic field", () => {
|
|
|
63
64
|
page = await context.newPage();
|
|
64
65
|
await page.goto(`http://localhost:${port}`);
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
it("Doesn't call `isValueValid` and `parse` if the value is empty", async () => {
|
|
69
|
+
const port = await getPort();
|
|
70
|
+
console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
|
|
71
|
+
const app = new Koa();
|
|
72
|
+
const router = new Router();
|
|
73
|
+
|
|
74
|
+
let is_value_valid_called = false;
|
|
75
|
+
let parse_called = false;
|
|
76
|
+
|
|
77
|
+
const fields = {
|
|
78
|
+
text: new (class MockField extends SimpleFormField<false> {
|
|
79
|
+
public async isValueValid() {
|
|
80
|
+
is_value_valid_called = true;
|
|
81
|
+
return true as any;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async parse() {
|
|
85
|
+
parse_called = true;
|
|
86
|
+
return true as any;
|
|
87
|
+
}
|
|
88
|
+
})(false),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
mount(
|
|
92
|
+
router,
|
|
93
|
+
"/",
|
|
94
|
+
new (class extends Form<typeof fields, unknown> {
|
|
95
|
+
fields = fields;
|
|
96
|
+
controls = [new SimpleInput(fields.text)];
|
|
97
|
+
|
|
98
|
+
async onSubmit() {
|
|
99
|
+
//noop
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async canAccess(_: Context) {
|
|
103
|
+
return { canAccess: true, message: "" };
|
|
104
|
+
}
|
|
105
|
+
})(),
|
|
106
|
+
true
|
|
107
|
+
);
|
|
108
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
109
|
+
server = app.listen(port);
|
|
110
|
+
|
|
111
|
+
browser = await getBrowser();
|
|
112
|
+
const context = await browser.newContext();
|
|
113
|
+
page = await context.newPage();
|
|
114
|
+
await page.goto(`http://localhost:${port}`);
|
|
115
|
+
await page.getByRole("button", { name: "Wyślij" }).click();
|
|
116
|
+
assert(is_value_valid_called == false);
|
|
117
|
+
assert(parse_called == false);
|
|
118
|
+
});
|
|
66
119
|
});
|
|
67
120
|
});
|
|
@@ -64,20 +64,21 @@ export abstract class FormField<
|
|
|
64
64
|
ctx: Context,
|
|
65
65
|
value: ParsedValue
|
|
66
66
|
): Promise<FormFieldValidationResponse> {
|
|
67
|
-
if (
|
|
68
|
-
this.required
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
if (value === "" || value === null || value === undefined) {
|
|
68
|
+
if (this.required) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
|
|
72
|
+
message: t(
|
|
73
|
+
ctx,
|
|
74
|
+
"field_is_required",
|
|
75
|
+
[],
|
|
76
|
+
"This field is required"
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
return { valid: true, message: "Empty, but not required" };
|
|
81
|
+
}
|
|
81
82
|
}
|
|
82
83
|
return this.isValueValid(ctx, value);
|
|
83
84
|
}
|
|
@@ -98,23 +99,42 @@ export abstract class FormField<
|
|
|
98
99
|
|
|
99
100
|
async getValue(
|
|
100
101
|
ctx: Context,
|
|
101
|
-
all_form_values: Record<string, FormDataValue
|
|
102
|
+
all_form_values: Record<string, FormDataValue>,
|
|
103
|
+
validate: true
|
|
102
104
|
): Promise<{
|
|
103
105
|
valid: boolean;
|
|
104
106
|
message: string;
|
|
105
|
-
parsed: ParsedValue;
|
|
107
|
+
parsed: ParsedValue | null;
|
|
108
|
+
raw: FormDataValue;
|
|
109
|
+
}>;
|
|
110
|
+
async getValue(
|
|
111
|
+
ctx: Context,
|
|
112
|
+
all_form_values: Record<string, FormDataValue>,
|
|
113
|
+
validate?: false
|
|
114
|
+
): Promise<{
|
|
115
|
+
message: string;
|
|
116
|
+
parsed: ParsedValue | null;
|
|
117
|
+
raw: FormDataValue;
|
|
118
|
+
}>;
|
|
119
|
+
async getValue(
|
|
120
|
+
ctx: Context,
|
|
121
|
+
all_form_values: Record<string, FormDataValue>,
|
|
122
|
+
validate = false
|
|
123
|
+
): Promise<{
|
|
124
|
+
valid?: boolean;
|
|
125
|
+
message: string;
|
|
126
|
+
parsed: ParsedValue | null;
|
|
106
127
|
raw: FormDataValue;
|
|
107
128
|
}> {
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
const raw = all_form_values[this.name];
|
|
130
|
+
if (raw == undefined || raw == null || raw == "") {
|
|
110
131
|
return {
|
|
111
|
-
parsed:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
parsed: null,
|
|
133
|
+
message: "empty value",
|
|
134
|
+
raw,
|
|
135
|
+
...(validate ? { valid: !this.required } : {}),
|
|
115
136
|
};
|
|
116
137
|
}
|
|
117
|
-
const raw = all_form_values[this.name];
|
|
118
138
|
const {
|
|
119
139
|
parsable,
|
|
120
140
|
parsed_value,
|
|
@@ -122,19 +142,23 @@ export abstract class FormField<
|
|
|
122
142
|
} = await this.parse(ctx, raw);
|
|
123
143
|
if (!parsable) {
|
|
124
144
|
return {
|
|
125
|
-
valid: false,
|
|
145
|
+
...(validate ? { valid: false } : {}),
|
|
126
146
|
parsed: this.getEmptyValue(),
|
|
127
147
|
raw,
|
|
128
148
|
message: parse_error || "Error",
|
|
129
149
|
};
|
|
130
150
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
if (validate) {
|
|
152
|
+
const { valid, message } = await this._validate(ctx, parsed_value);
|
|
153
|
+
return {
|
|
154
|
+
parsed: parsed_value,
|
|
155
|
+
...(validate ? { valid } : {}),
|
|
156
|
+
message,
|
|
157
|
+
raw: all_form_values[this.name],
|
|
158
|
+
};
|
|
159
|
+
} else {
|
|
160
|
+
return { parsed: parsed_value, raw, message: "parsed" };
|
|
161
|
+
}
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
setMapToFilter(fn: (parsed: ParsedValue) => unknown) {
|
package/src/forms/fields/file.ts
CHANGED
|
@@ -76,7 +76,7 @@ export class File<Required extends boolean> extends FormField<
|
|
|
76
76
|
|
|
77
77
|
async getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>) {
|
|
78
78
|
const { parsed } = await this.getValue(ctx, data);
|
|
79
|
-
return parsed
|
|
79
|
+
return parsed?.new || undefined;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
getEmptyValue() {
|
package/src/forms/form.test.ts
CHANGED
package/src/page/list.ts
CHANGED
|
@@ -14,9 +14,11 @@ import { FormDataValue } from "../forms/form-types.js";
|
|
|
14
14
|
export const BasePagePropsShape = <const>{};
|
|
15
15
|
export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
|
|
16
16
|
|
|
17
|
+
const DEFAULT_ITEMS_PER_PAGE = 12;
|
|
18
|
+
|
|
17
19
|
export const BaseListPageFields = <const>{
|
|
18
20
|
page: new NumberField(false, 1),
|
|
19
|
-
itemsPerPage: new NumberField(false,
|
|
21
|
+
itemsPerPage: new NumberField(false, DEFAULT_ITEMS_PER_PAGE),
|
|
20
22
|
sort: new SimpleFormField(false),
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -51,7 +53,7 @@ export abstract class ListPage<
|
|
|
51
53
|
abstract getItems(
|
|
52
54
|
ctx: Context,
|
|
53
55
|
page: number,
|
|
54
|
-
itemsPerPage: number,
|
|
56
|
+
itemsPerPage: number | null,
|
|
55
57
|
values: Record<string, FormDataValue>
|
|
56
58
|
): Promise<ItemType[]>;
|
|
57
59
|
|
|
@@ -83,7 +85,10 @@ export abstract class ListPage<
|
|
|
83
85
|
|
|
84
86
|
async getPaginationConfig(ctx: Context) {
|
|
85
87
|
const values = await this.extractRawValues(ctx);
|
|
86
|
-
|
|
88
|
+
let { parsed: page } = await this.fields.page.getValue(ctx, values);
|
|
89
|
+
if (!page) {
|
|
90
|
+
page = 1;
|
|
91
|
+
}
|
|
87
92
|
const { parsed: itemsPerPage } =
|
|
88
93
|
await this.fields.itemsPerPage.getValue(ctx, values);
|
|
89
94
|
return { page, itemsPerPage };
|
|
@@ -120,7 +125,11 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
|
|
|
120
125
|
values: Record<string, FormDataValue>
|
|
121
126
|
): Promise<FlatTemplatable> {
|
|
122
127
|
const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
|
|
123
|
-
const totalIems = await this.getTotalPages(
|
|
128
|
+
const totalIems = await this.getTotalPages(
|
|
129
|
+
ctx,
|
|
130
|
+
itemsPerPage || DEFAULT_ITEMS_PER_PAGE,
|
|
131
|
+
values
|
|
132
|
+
);
|
|
124
133
|
|
|
125
134
|
return tempstream/* HTML */ `<div class="list-pagination">
|
|
126
135
|
<div class="list-pagination__left">
|
|
@@ -82,18 +82,17 @@ export abstract class MountableWithFields<
|
|
|
82
82
|
const field_errors = {} as Record<keyof Fields, string>;
|
|
83
83
|
let valid = true;
|
|
84
84
|
const form_messages = [] as FormMessage[];
|
|
85
|
+
|
|
85
86
|
await Promise.all(
|
|
86
|
-
Object.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
})
|
|
87
|
+
Object.keys(this.fields).map(async (key: keyof F) => {
|
|
88
|
+
const field = this.fields[key];
|
|
89
|
+
const { valid: fieldvalid, message: fieldmessage } =
|
|
90
|
+
await field.getValue(ctx, values, true);
|
|
91
|
+
if (!fieldvalid) {
|
|
92
|
+
valid = false;
|
|
93
|
+
field_errors[field.name] = fieldmessage;
|
|
94
|
+
}
|
|
95
|
+
})
|
|
97
96
|
);
|
|
98
97
|
const formValidationResult = await this.validateValues(ctx, values);
|
|
99
98
|
if (!formValidationResult.valid) {
|
|
@@ -143,7 +142,7 @@ export abstract class MountableWithFields<
|
|
|
143
142
|
const promises = Object.entries(this.fields).map(
|
|
144
143
|
async ([key, field]) => {
|
|
145
144
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
146
|
-
const { parsed } = await field.getValue(ctx, raw_values);
|
|
145
|
+
const { parsed } = await field.getValue(ctx, raw_values, false);
|
|
147
146
|
result[key] = parsed;
|
|
148
147
|
}
|
|
149
148
|
);
|