@sealcode/sealgen 0.8.1 → 0.8.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/autocomplete.d.ts +20 -0
- package/@types/forms/controls/checkbox-group.d.ts +21 -0
- package/@types/forms/controls/checkbox.d.ts +8 -1
- package/@types/forms/controls/collapsible-checkbox-group.d.ts +12 -0
- package/@types/forms/controls/collapsible-field-group.d.ts +21 -0
- package/@types/forms/controls/controls.d.ts +4 -0
- package/@types/forms/controls/field-group.d.ts +12 -4
- package/@types/forms/controls/simple-input.d.ts +9 -2
- package/@types/forms/controls/tickable.d.ts +9 -12
- package/@types/forms/fields/enum-multiple.d.ts +18 -0
- package/@types/forms/fields/existing-sealious-value.d.ts +6 -0
- package/@types/forms/fields/field.d.ts +8 -4
- package/@types/forms/fields/proxy-field.d.ts +13 -0
- package/@types/page/list.d.ts +3 -3
- package/@types/utils/input-wrapper.d.ts +2 -1
- package/lib/forms/controls/autocomplete.js +27 -0
- package/lib/forms/controls/autocomplete.js.map +1 -0
- package/lib/forms/controls/checkbox-group.js +27 -0
- package/lib/forms/controls/checkbox-group.js.map +1 -0
- package/lib/forms/controls/checkbox.js +12 -1
- package/lib/forms/controls/checkbox.js.map +1 -1
- package/lib/forms/controls/collapsible-checkbox-group.js +17 -0
- package/lib/forms/controls/collapsible-checkbox-group.js.map +1 -0
- package/lib/forms/controls/collapsible-field-group.js +82 -0
- package/lib/forms/controls/collapsible-field-group.js.map +1 -0
- package/lib/forms/controls/controls.js +4 -0
- package/lib/forms/controls/controls.js.map +1 -1
- package/lib/forms/controls/field-group.js +39 -8
- package/lib/forms/controls/field-group.js.map +1 -1
- package/lib/forms/controls/simple-input.js +64 -29
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/controls/tickable.js +4 -2
- package/lib/forms/controls/tickable.js.map +1 -1
- package/lib/forms/fields/boolean.js +7 -0
- package/lib/forms/fields/boolean.js.map +1 -1
- package/lib/forms/fields/enum-multiple.js +45 -0
- package/lib/forms/fields/enum-multiple.js.map +1 -0
- package/lib/forms/fields/existing-sealious-value.js +20 -0
- package/lib/forms/fields/existing-sealious-value.js.map +1 -0
- package/lib/forms/fields/field.js +7 -0
- package/lib/forms/fields/field.js.map +1 -1
- package/lib/forms/fields/pick-from-list.js +1 -1
- package/lib/forms/fields/pick-from-list.js.map +1 -1
- package/lib/forms/fields/proxy-field.js +24 -0
- package/lib/forms/fields/proxy-field.js.map +1 -0
- package/lib/page/list.js +9 -6
- package/lib/page/list.js.map +1 -1
- package/lib/sanitize.js +1 -1
- package/lib/sanitize.js.map +1 -1
- package/lib/utils/input-wrapper.js.map +1 -1
- package/package.json +2 -2
- package/src/forms/controls/autocomplete.ts +46 -0
- package/src/forms/controls/checkbox-group.ts +81 -0
- package/src/forms/controls/checkbox.ts +16 -1
- package/src/forms/controls/collapsible-checkbox-group.ts +31 -0
- package/src/forms/controls/collapsible-field-group.ts +96 -0
- package/src/forms/controls/controls.ts +4 -0
- package/src/forms/controls/field-group.ts +60 -9
- package/src/forms/controls/simple-input.ts +90 -42
- package/src/forms/controls/tickable.ts +14 -10
- package/src/forms/fields/boolean.ts +6 -0
- package/src/forms/fields/enum-multiple.ts +58 -0
- package/src/forms/fields/existing-sealious-value.ts +22 -0
- package/src/forms/fields/field.ts +14 -2
- package/src/forms/fields/pick-from-list.ts +1 -1
- package/src/forms/fields/proxy-field.ts +24 -0
- package/src/forms/forms.scss +11 -0
- package/src/page/list.ts +12 -10
- package/src/sanitize.ts +1 -1
- package/src/utils/input-wrapper.ts +5 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { CheckboxWithValue } from "./checkbox";
|
|
5
|
+
import { FieldGroup, FormFieldControl } from "./controls";
|
|
6
|
+
import { CheckboxGroup, DefaultCheckboxOptions } from "./checkbox-group";
|
|
7
|
+
import { CollapsibleFieldGroup } from "./collapsible-field-group";
|
|
8
|
+
|
|
9
|
+
export class CollapsibleCheckboxGroup extends CheckboxGroup<
|
|
10
|
+
DefaultCheckboxOptions & {
|
|
11
|
+
textWhenOpen?: string;
|
|
12
|
+
textWhenClosed?: string;
|
|
13
|
+
groupID?: string;
|
|
14
|
+
}
|
|
15
|
+
> {
|
|
16
|
+
wrapGroup(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
data: Record<string, FormDataValue>,
|
|
19
|
+
messages: FormMessage[],
|
|
20
|
+
field_prefix: string,
|
|
21
|
+
form_id: string,
|
|
22
|
+
checkboxes: CheckboxWithValue[]
|
|
23
|
+
): Promise<FlatTemplatable> {
|
|
24
|
+
return new CollapsibleFieldGroup(checkboxes, {
|
|
25
|
+
label: this.options.label,
|
|
26
|
+
textWhenOpen: this.options.textWhenOpen,
|
|
27
|
+
textWhenClosed: this.options.textWhenClosed,
|
|
28
|
+
groupID: this.options.groupID,
|
|
29
|
+
}).render(ctx, data, messages, field_prefix, form_id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { FieldGroup } from "./field-group";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { FormControl } from "./form-control";
|
|
7
|
+
|
|
8
|
+
type CollapsibleFieldGroupOptions = {
|
|
9
|
+
label?: string;
|
|
10
|
+
classes?: string;
|
|
11
|
+
openOnStart?: boolean;
|
|
12
|
+
textWhenOpen?: string;
|
|
13
|
+
textWhenClosed?: string;
|
|
14
|
+
groupID?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class CollapsibleFieldGroup extends FieldGroup {
|
|
18
|
+
public collapseID: string;
|
|
19
|
+
constructor(
|
|
20
|
+
public controls: FormControl[],
|
|
21
|
+
public options: CollapsibleFieldGroupOptions = {}
|
|
22
|
+
) {
|
|
23
|
+
super([]);
|
|
24
|
+
if (this.options.groupID == undefined) {
|
|
25
|
+
this.options.groupID = "A" + randomUUID(); // "A" is needed as ids always have to start with a letter
|
|
26
|
+
}
|
|
27
|
+
this.collapseID = this.options.groupID + "collapse";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getGroupLabelProps() {
|
|
31
|
+
return super.getGroupLabelProps() + ` for="${this.collapseID}"`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
preLabelContainer(): FlatTemplatable {
|
|
35
|
+
return /* HTML */ `<style>
|
|
36
|
+
input#${this.collapseID} {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input#${this.collapseID} ~ .field-group__fields {
|
|
41
|
+
display: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
input#${this.collapseID}:checked ~ .field-group__fields {
|
|
45
|
+
display: block;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
input#${this.collapseID}
|
|
49
|
+
~ .field-group__label-container
|
|
50
|
+
.field-group__collapse-label-text--open {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
input#${this.collapseID}:checked
|
|
55
|
+
~ .field-group__label-container
|
|
56
|
+
.field-group__collapse-label-text--open {
|
|
57
|
+
display: inline-block;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
input#${this.collapseID}
|
|
61
|
+
~ .field-group__label-container
|
|
62
|
+
.field-group__collapse-label-text--closed {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
input#${this.collapseID}:checked
|
|
67
|
+
~ .field-group__label-container
|
|
68
|
+
.field-group__collapse-label-text--closed {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
<input
|
|
73
|
+
type="checkbox"
|
|
74
|
+
id="${this.collapseID}"
|
|
75
|
+
${this.options.openOnStart ? "checked" : ""}
|
|
76
|
+
/>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
postLabel(): FlatTemplatable {
|
|
80
|
+
return /* HTML */ `<label
|
|
81
|
+
class="field-group__collapse-label"
|
|
82
|
+
for="${this.collapseID}"
|
|
83
|
+
>
|
|
84
|
+
<span
|
|
85
|
+
class="field-group__collapse-label-text field-group__collapse-label-text--open"
|
|
86
|
+
>
|
|
87
|
+
${this.options.textWhenOpen || "close"}
|
|
88
|
+
</span>
|
|
89
|
+
<span
|
|
90
|
+
class="field-group__collapse-label-text field-group__collapse-label-text--closed"
|
|
91
|
+
>
|
|
92
|
+
${this.options.textWhenClosed || "open"}
|
|
93
|
+
</span>
|
|
94
|
+
</label>`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -16,3 +16,7 @@ export * from "./tickable";
|
|
|
16
16
|
export * from "./checkbox";
|
|
17
17
|
export * from "./radio";
|
|
18
18
|
export * from "./big-radio-group";
|
|
19
|
+
export * from "./checkbox-group";
|
|
20
|
+
export * from "./collapsible-field-group";
|
|
21
|
+
export * from "./collapsible-checkbox-group";
|
|
22
|
+
export * from "./autocomplete";
|
|
@@ -1,32 +1,83 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
-
import { FormDataValue } from "../form";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
4
|
import { FormControl } from "./form-control";
|
|
5
|
+
import { FormFieldControl } from "./form-field-control";
|
|
5
6
|
|
|
6
|
-
export class FieldGroup extends
|
|
7
|
+
export class FieldGroup extends FormFieldControl {
|
|
7
8
|
constructor(
|
|
8
9
|
public controls: FormControl[],
|
|
9
|
-
public options: {
|
|
10
|
+
public options: {
|
|
11
|
+
label?: string;
|
|
12
|
+
classes?: string;
|
|
13
|
+
containerID?: string;
|
|
14
|
+
} = {}
|
|
10
15
|
) {
|
|
11
|
-
super();
|
|
16
|
+
super([]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
preLabel(): FlatTemplatable {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
preLabelContainer(): FlatTemplatable {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
postLabel(): FlatTemplatable {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
preFields(): FlatTemplatable {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
postFields(): FlatTemplatable {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getGroupLabelProps(): FlatTemplatable {
|
|
40
|
+
return `class="field-group__label"`;
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
async render(
|
|
15
44
|
ctx: Context,
|
|
16
|
-
data: Record<string, FormDataValue
|
|
45
|
+
data: Record<string, FormDataValue>,
|
|
46
|
+
messages: FormMessage[],
|
|
47
|
+
field_prefix: string,
|
|
48
|
+
form_id: string
|
|
17
49
|
): Promise<FlatTemplatable> {
|
|
18
50
|
const { label, classes } = this.options;
|
|
19
51
|
|
|
20
52
|
return tempstream/* HTML */ `
|
|
21
|
-
<div
|
|
22
|
-
${
|
|
53
|
+
<div
|
|
54
|
+
class="field-group ${classes || ""}"
|
|
55
|
+
${this.options.containerID
|
|
56
|
+
? `id="${this.options.containerID}"`
|
|
57
|
+
: ""}
|
|
58
|
+
>
|
|
59
|
+
${this.preLabelContainer()}
|
|
60
|
+
<div class="field-group__label-container">
|
|
61
|
+
${this.preLabel()}
|
|
62
|
+
${label
|
|
63
|
+
? `<label ${this.getGroupLabelProps()}>${label}</label>`
|
|
64
|
+
: ""}
|
|
65
|
+
${this.postLabel()}
|
|
66
|
+
</div>
|
|
67
|
+
${this.preFields()}
|
|
23
68
|
<div class="field-group__fields">
|
|
24
69
|
${this.controls.map((control) =>
|
|
25
|
-
control.render(
|
|
70
|
+
control.render(
|
|
71
|
+
ctx,
|
|
72
|
+
data,
|
|
73
|
+
messages,
|
|
74
|
+
field_prefix,
|
|
75
|
+
form_id
|
|
76
|
+
)
|
|
26
77
|
)}
|
|
27
78
|
</div>
|
|
79
|
+
${this.postFields()}
|
|
28
80
|
</div>
|
|
29
81
|
`;
|
|
30
82
|
}
|
|
31
|
-
role = <const>"decoration";
|
|
32
83
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
|
-
import { FlatTemplatable } from "tempstream";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
3
|
import { attribute } from "../../sanitize";
|
|
4
4
|
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
5
5
|
import { FormField } from "../fields/field";
|
|
6
|
-
import { FormDataValue } from "../form";
|
|
6
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./controls";
|
|
8
8
|
|
|
9
9
|
export class SimpleInput extends FormFieldControl {
|
|
@@ -37,58 +37,106 @@ export class SimpleInput extends FormFieldControl {
|
|
|
37
37
|
super([field]);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
getType(): string {
|
|
41
|
+
return this.options.type || "text";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getLabel(): string {
|
|
45
|
+
return this.options.label != undefined
|
|
46
|
+
? this.options.label
|
|
47
|
+
: this.field.name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getID(): string {
|
|
51
|
+
return this.options.id || this.field.name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getPlaceholder(): string {
|
|
55
|
+
return this.options.placeholder || this.getType();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getInputAttributes(
|
|
59
|
+
ctx: Context,
|
|
60
|
+
data: Record<string, FormDataValue>,
|
|
61
|
+
field_prefix: string,
|
|
62
|
+
form_id: string
|
|
63
|
+
): Promise<Record<string, string | true>> {
|
|
64
|
+
const readonly = this.options.readonly || false;
|
|
65
|
+
const required = this.field.required;
|
|
66
|
+
const { parsed, raw, valid } = await this.field.getValue(ctx, data);
|
|
67
|
+
return Object.fromEntries([
|
|
68
|
+
<const>["id", this.getID()],
|
|
69
|
+
<const>["type", this.getType()],
|
|
70
|
+
<const>["name", `${field_prefix}${this.field.name}`],
|
|
71
|
+
<const>[
|
|
72
|
+
"value",
|
|
73
|
+
!valid && raw
|
|
74
|
+
? raw.toString()
|
|
75
|
+
: parsed !== undefined && parsed !== null
|
|
76
|
+
? ((parsed as any)?.toString() as string) || ""
|
|
77
|
+
: "",
|
|
78
|
+
],
|
|
79
|
+
...(form_id ? [["form", form_id]] : []),
|
|
80
|
+
<const>["placeholder", this.getPlaceholder()],
|
|
81
|
+
...(readonly ? [["readonly", true]] : []),
|
|
82
|
+
...(required ? [["required", true]] : []),
|
|
83
|
+
...(!this.options.autocomplete ? [["autocomplete", "off"]] : []),
|
|
84
|
+
...(this.options.step ? [["step", this.options.step]] : []),
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
renderAttributes(attrs: Record<string, string | true>) {
|
|
89
|
+
return Object.entries(attrs)
|
|
90
|
+
.map(([key, value]) => {
|
|
91
|
+
if (value === true) {
|
|
92
|
+
return key;
|
|
93
|
+
} else {
|
|
94
|
+
return `${key}="${attribute(value)}"`;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.join(" ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async postInput(ctx: Context): Promise<FlatTemplatable> {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
40
104
|
async render<Fields extends Record<string, FormField>>(
|
|
41
105
|
ctx: Context,
|
|
42
106
|
data: Record<string, FormDataValue>,
|
|
43
|
-
|
|
107
|
+
_messages: FormMessage[],
|
|
44
108
|
field_prefix: string,
|
|
45
109
|
form_id: string
|
|
46
110
|
): Promise<FlatTemplatable> {
|
|
47
|
-
const id = this.
|
|
48
|
-
const label =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: this.field.name;
|
|
52
|
-
const type = this.options.type || "text";
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
111
|
+
const id = this.getID();
|
|
112
|
+
const label = this.getLabel();
|
|
113
|
+
const type = this.getType();
|
|
114
|
+
|
|
54
115
|
const { parsed, raw, valid, message } = await this.field.getValue(
|
|
55
116
|
ctx,
|
|
56
117
|
data
|
|
57
118
|
);
|
|
58
|
-
|
|
59
|
-
const placeholder = this.options.placeholder || type;
|
|
60
|
-
const readonly = this.options.readonly || false;
|
|
61
|
-
const required = this.field.required;
|
|
119
|
+
const placeholder = this.getPlaceholder();
|
|
62
120
|
|
|
63
|
-
return
|
|
64
|
-
[id, type, getRequiredClass(required)],
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
${form_id ? `form="${form_id}"` : ""}
|
|
80
|
-
placeholder="${placeholder}"
|
|
81
|
-
${readonly ? "readonly" : ""}
|
|
82
|
-
${required ? "required" : ""}
|
|
83
|
-
${!this.options.autocomplete ? `autocomplete="off"` : ""}
|
|
84
|
-
${this.options.step ? `step="${this.options.step}"` : ""}
|
|
85
|
-
/>
|
|
86
|
-
${
|
|
87
|
-
~valid && !this.options.hide_errors
|
|
121
|
+
return inputWrapper(
|
|
122
|
+
[id, type, getRequiredClass(this.field.required)],
|
|
123
|
+
tempstream/* HTML */ `
|
|
124
|
+
<label for="${id}">${label}</label>
|
|
125
|
+
<input
|
|
126
|
+
${this.renderAttributes(
|
|
127
|
+
await this.getInputAttributes(
|
|
128
|
+
ctx,
|
|
129
|
+
data,
|
|
130
|
+
field_prefix,
|
|
131
|
+
form_id
|
|
132
|
+
)
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
${this.postInput(ctx)}
|
|
136
|
+
${~valid && !this.options.hide_errors
|
|
88
137
|
? `<div class="input__error">${message}</div>`
|
|
89
|
-
: ""
|
|
90
|
-
|
|
91
|
-
`
|
|
138
|
+
: ""}
|
|
139
|
+
`
|
|
92
140
|
);
|
|
93
141
|
}
|
|
94
142
|
}
|
|
@@ -6,15 +6,18 @@ import { FormField } from "../fields/field";
|
|
|
6
6
|
import { FormDataValue } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./controls";
|
|
8
8
|
|
|
9
|
+
export type TickableOptions = {
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
hide_errors?: boolean;
|
|
14
|
+
readonly?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
9
17
|
export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
10
18
|
constructor(
|
|
11
19
|
public field: FormField<boolean, T>,
|
|
12
|
-
public options: {
|
|
13
|
-
id?: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
hide_errors?: boolean;
|
|
16
|
-
readonly?: boolean;
|
|
17
|
-
} = {}
|
|
20
|
+
public options: TickableOptions = {}
|
|
18
21
|
) {
|
|
19
22
|
super([field]);
|
|
20
23
|
}
|
|
@@ -69,6 +72,8 @@ export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
|
69
72
|
const readonly = this.options.readonly || false;
|
|
70
73
|
const required = this.field.required;
|
|
71
74
|
|
|
75
|
+
const value = this.getValueAttribute(ctx, data, parsed);
|
|
76
|
+
|
|
72
77
|
return inputWrapper(
|
|
73
78
|
this.getWrapperClasses(ctx, data, parsed),
|
|
74
79
|
|
|
@@ -76,10 +81,9 @@ export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
|
76
81
|
<input
|
|
77
82
|
id="${id}"
|
|
78
83
|
type="${this.type}"
|
|
79
|
-
name="${field_prefix}${this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)}"
|
|
84
|
+
name="${field_prefix}${this.options.name ||
|
|
85
|
+
this.field.name}"
|
|
86
|
+
${value ? `value="${attribute(value)}"` : ""}
|
|
83
87
|
${form_id ? `form="${form_id}"` : ""}
|
|
84
88
|
${this.isChecked(ctx, data, parsed) ? "checked" : ""}
|
|
85
89
|
${readonly ? "readonly" : ""}
|
|
@@ -40,6 +40,12 @@ export class Boolean<Required extends boolean> extends FormField<
|
|
|
40
40
|
parsed_value: truthy.includes(value),
|
|
41
41
|
error: null,
|
|
42
42
|
};
|
|
43
|
+
} else if (is(value, predicates.undefined)) {
|
|
44
|
+
return {
|
|
45
|
+
parsable: true,
|
|
46
|
+
parsed_value: false,
|
|
47
|
+
error: null,
|
|
48
|
+
};
|
|
43
49
|
} else {
|
|
44
50
|
return {
|
|
45
51
|
parsable: false,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
2
|
+
import { Context } from "koa";
|
|
3
|
+
import { FormDataValue } from "../form";
|
|
4
|
+
import { FieldParseResult, FormField } from "./field";
|
|
5
|
+
|
|
6
|
+
export class EnumMultipleField<
|
|
7
|
+
Options extends readonly string[],
|
|
8
|
+
Required extends boolean
|
|
9
|
+
> extends FormField<Required, string[]> {
|
|
10
|
+
constructor(
|
|
11
|
+
required: Required,
|
|
12
|
+
public options: Options,
|
|
13
|
+
public error_message: (options: Options) => string = (options) =>
|
|
14
|
+
options.join(", ")
|
|
15
|
+
) {
|
|
16
|
+
super(required);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
predicate = predicates.enum<Options>(this.options);
|
|
20
|
+
|
|
21
|
+
public async isValueValid(
|
|
22
|
+
_: Context,
|
|
23
|
+
values: string[]
|
|
24
|
+
): Promise<{ valid: boolean; message: string }> {
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (!is(value, predicates.string)) {
|
|
27
|
+
return { valid: false, message: "expected a string" };
|
|
28
|
+
}
|
|
29
|
+
if (!this.options.includes(value)) {
|
|
30
|
+
return { valid: false, message: "invalid value" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { valid: true, message: "" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async parse(
|
|
37
|
+
_: Context,
|
|
38
|
+
value: FormDataValue
|
|
39
|
+
): Promise<FieldParseResult<string[]>> {
|
|
40
|
+
if (is(value, predicates.array(predicates.string))) {
|
|
41
|
+
return {
|
|
42
|
+
parsable: true,
|
|
43
|
+
parsed_value: value,
|
|
44
|
+
error: null,
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
return {
|
|
48
|
+
parsable: false,
|
|
49
|
+
parsed_value: null,
|
|
50
|
+
error: "Please enter a string",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public getEmptyValue(): string[] {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Field } from "sealious";
|
|
2
|
+
import { PickFromListField } from "./pick-from-list";
|
|
3
|
+
|
|
4
|
+
export class ExistingSealiousValue<
|
|
5
|
+
Required extends boolean
|
|
6
|
+
> extends PickFromListField<Required> {
|
|
7
|
+
constructor(required: Required, field: Field) {
|
|
8
|
+
super(required, async (ctx) => {
|
|
9
|
+
const { items } = await field.collection.list(ctx.$context).fetch();
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
items.map((item) => {
|
|
12
|
+
const value = item.get(field.name);
|
|
13
|
+
return [value, value];
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getEmptyValue() {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -37,12 +37,16 @@ export function fieldsToShape<Fields extends Record<string, FormField>>(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export type FieldParseResult<T> =
|
|
40
|
+
| {
|
|
41
|
+
parsable: true;
|
|
42
|
+
error: null;
|
|
43
|
+
parsed_value: T;
|
|
44
|
+
}
|
|
40
45
|
| {
|
|
41
46
|
parsable: false;
|
|
42
47
|
error: string;
|
|
43
48
|
parsed_value: null;
|
|
44
|
-
}
|
|
45
|
-
| { parsable: true; error: null; parsed_value: T };
|
|
49
|
+
};
|
|
46
50
|
|
|
47
51
|
export abstract class FormField<
|
|
48
52
|
Required extends boolean = boolean,
|
|
@@ -51,6 +55,7 @@ export abstract class FormField<
|
|
|
51
55
|
name: string;
|
|
52
56
|
constructor(public readonly required: Required) {}
|
|
53
57
|
predicate: Predicate = predicates.unknown;
|
|
58
|
+
mapToFilter: (parsed: ParsedValue) => unknown = (value) => value;
|
|
54
59
|
|
|
55
60
|
init(fieldname: string): void {
|
|
56
61
|
this.name = fieldname;
|
|
@@ -135,6 +140,11 @@ export abstract class FormField<
|
|
|
135
140
|
raw: all_form_values[this.name],
|
|
136
141
|
};
|
|
137
142
|
}
|
|
143
|
+
|
|
144
|
+
setMapToFilter(fn: (parsed: ParsedValue) => unknown) {
|
|
145
|
+
this.mapToFilter = fn;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export * from "./simple-form-field";
|
|
@@ -146,3 +156,5 @@ export * from "./regexp";
|
|
|
146
156
|
export * from "./email";
|
|
147
157
|
export * from "./enum";
|
|
148
158
|
export * from "./boolean";
|
|
159
|
+
export * from "./enum-multiple";
|
|
160
|
+
export * from "./existing-sealious-value";
|
|
@@ -38,7 +38,7 @@ export class PickFromListField<
|
|
|
38
38
|
if (!is(value, predicates.string)) {
|
|
39
39
|
return { valid: false, message: "not a string" };
|
|
40
40
|
}
|
|
41
|
-
if (!Object.keys(options).includes(value)) {
|
|
41
|
+
if (this.required && !Object.keys(options).includes(value)) {
|
|
42
42
|
return {
|
|
43
43
|
valid: false,
|
|
44
44
|
message: `"${value}" is not one of the options`,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FormField } from "./field";
|
|
2
|
+
|
|
3
|
+
export class ProxyFormField<T> extends FormField<boolean, T> {
|
|
4
|
+
constructor(
|
|
5
|
+
required: boolean,
|
|
6
|
+
public name: string,
|
|
7
|
+
public empty_value: T,
|
|
8
|
+
public parsed_value: T
|
|
9
|
+
) {
|
|
10
|
+
super(required);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getEmptyValue() {
|
|
14
|
+
return this.empty_value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async parse() {
|
|
18
|
+
return <const>{
|
|
19
|
+
parsable: true,
|
|
20
|
+
error: null,
|
|
21
|
+
parsed_value: this.parsed_value,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/forms/forms.scss
CHANGED
package/src/page/list.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Templatable, tempstream, FlatTemplatable } from "tempstream";
|
|
|
5
5
|
import { peopleWhoCan } from "./access-control";
|
|
6
6
|
import { Page } from "./page";
|
|
7
7
|
import { ShapeToType } from "@sealcode/ts-predicates";
|
|
8
|
-
import { FormFieldControl } from "../forms/controls/controls";
|
|
8
|
+
import { FormControl, FormFieldControl } from "../forms/controls/controls";
|
|
9
9
|
import { FormField, NumberField } from "../forms/fields/field";
|
|
10
10
|
import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
|
|
11
11
|
import { FormDataValue } from "..";
|
|
@@ -39,7 +39,7 @@ export abstract class ListPage<
|
|
|
39
39
|
abstract renderItem(ctx: Context, item: ItemType): Promise<FlatTemplatable>;
|
|
40
40
|
|
|
41
41
|
filterFields: Record<string, FormField> = {};
|
|
42
|
-
filterControls:
|
|
42
|
+
filterControls: FormControl[] = [];
|
|
43
43
|
|
|
44
44
|
init(): void {
|
|
45
45
|
super.init();
|
|
@@ -54,15 +54,17 @@ export abstract class ListPage<
|
|
|
54
54
|
|
|
55
55
|
async renderItems(
|
|
56
56
|
ctx: Context,
|
|
57
|
-
values: Record<string, FormDataValue> = this.extractRawValues(ctx)
|
|
57
|
+
values: Record<string, FormDataValue> = this.extractRawValues(ctx),
|
|
58
|
+
items?: ItemType[]
|
|
58
59
|
): Promise<FlatTemplatable> {
|
|
59
60
|
const { parsed: page } = await this.fields.page.getValue(ctx, values);
|
|
60
61
|
const { parsed: itemsPerPage } =
|
|
61
62
|
await this.fields.itemsPerPage.getValue(ctx, values);
|
|
62
63
|
const items_promise = this.getItems(ctx, page, itemsPerPage, values);
|
|
63
|
-
return tempstream`${
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
return tempstream`${(items
|
|
65
|
+
? Promise.resolve(items)
|
|
66
|
+
: items_promise
|
|
67
|
+
).then((items) => items.map((item) => this.renderItem(ctx, item)))}`;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
async render(ctx: Context): Promise<FlatTemplatable> {
|
|
@@ -132,7 +134,7 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
|
|
|
132
134
|
for (const [fieldname, field] of Object.entries(this.filterFields)) {
|
|
133
135
|
// eslint-disable-next-line no-await-in-loop
|
|
134
136
|
const { parsed } = await field.getValue(ctx, raw_values);
|
|
135
|
-
filter[fieldname] = parsed;
|
|
137
|
+
filter[fieldname] = field.mapToFilter(parsed);
|
|
136
138
|
}
|
|
137
139
|
return filter;
|
|
138
140
|
}
|
|
@@ -144,10 +146,10 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
|
|
|
144
146
|
"page",
|
|
145
147
|
...Object.values(this.filterFields).map((f) => f.name),
|
|
146
148
|
])}
|
|
147
|
-
${this.filterControls.map((control) =>
|
|
149
|
+
${this.filterControls.map((control) => {
|
|
148
150
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
149
|
-
control.render(ctx, values, [], "", "")
|
|
150
|
-
)}
|
|
151
|
+
return control.render(ctx, values, [], "", "");
|
|
152
|
+
})}
|
|
151
153
|
</form>`;
|
|
152
154
|
}
|
|
153
155
|
}
|