@sealcode/sealgen 0.15.58 → 0.15.60
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/page/stateful-page.d.ts +12 -5
- package/lib/forms/controls/table.js +40 -24
- package/lib/page/stateful-page.js +3 -3
- package/lib/page/stateful-page.test.js +9 -2
- package/package.json +1 -1
- package/src/forms/controls/table.ts +63 -43
- package/src/page/stateful-page.test.ts +13 -7
- package/src/page/stateful-page.ts +30 -23
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import Router from "@koa/router";
|
|
3
3
|
import { BaseContext, Context } from "koa";
|
|
4
4
|
import { Templatable } from "tempstream";
|
|
5
|
-
import { ExtractTail } from "../utils/utils.js";
|
|
6
5
|
import { Mountable } from "./mountable.js";
|
|
7
6
|
export type StatefulPageActionDescription<ActionName> = {
|
|
8
7
|
action: ActionName;
|
|
@@ -22,7 +21,15 @@ export type StateAndMetadata<State, Actions> = {
|
|
|
22
21
|
action_args: null;
|
|
23
22
|
$: Record<string, unknown>;
|
|
24
23
|
};
|
|
25
|
-
export
|
|
24
|
+
export type StatefulPageActionArgument<State extends Record<string, unknown>, Args extends unknown[] = unknown[]> = {
|
|
25
|
+
ctx: BaseContext;
|
|
26
|
+
state: State;
|
|
27
|
+
inputs: Record<string, string>;
|
|
28
|
+
args: Args;
|
|
29
|
+
};
|
|
30
|
+
export type StatefulPageAction<State extends Record<string, unknown>, Args extends unknown[] = unknown[]> = (obj: StatefulPageActionArgument<State, Args>) => State | Promise<State>;
|
|
31
|
+
export type ExtractStatefulPageActionArgs<X> = X extends StatefulPageAction<any, infer Args> ? Args : never;
|
|
32
|
+
export declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, StatefulPageAction<State>>> extends Mountable {
|
|
26
33
|
abstract actions: Actions;
|
|
27
34
|
abstract getInitialState(ctx: BaseContext): State | Promise<State>;
|
|
28
35
|
abstract render(ctx: BaseContext, state: State, inputs: Record<string, string>): Templatable | Promise<Templatable>;
|
|
@@ -39,12 +46,12 @@ export declare abstract class StatefulPage<State extends Record<string, unknown>
|
|
|
39
46
|
content: string;
|
|
40
47
|
disabled: boolean;
|
|
41
48
|
};
|
|
42
|
-
makeActionURL<ActionName extends keyof Actions>(action_description: StatefulPageActionDescription<ActionName>, ...args:
|
|
43
|
-
makeActionButton<ActionName extends keyof Actions>(_state: State, action_description: StatefulPageActionDescription<ActionName>, ...args:
|
|
49
|
+
makeActionURL<ActionName extends keyof Actions>(action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractStatefulPageActionArgs<Actions[ActionName]>): string;
|
|
50
|
+
makeActionButton<ActionName extends keyof Actions>(_state: State, action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractStatefulPageActionArgs<Actions[ActionName]>): string;
|
|
44
51
|
makeActionCallback<ActionName extends keyof Actions>(action_description: {
|
|
45
52
|
action: ActionName;
|
|
46
53
|
label?: string;
|
|
47
|
-
} | ActionName, ...args:
|
|
54
|
+
} | ActionName, ...args: ExtractStatefulPageActionArgs<Actions[ActionName]>): string;
|
|
48
55
|
rerender(): string;
|
|
49
56
|
preprocessState(values: State): Promise<State>;
|
|
50
57
|
preprocessOverrides(_context: BaseContext, _state: State, values: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -2,6 +2,7 @@ import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
|
|
2
2
|
import qs from "qs";
|
|
3
3
|
import { tempstream } from "tempstream";
|
|
4
4
|
import { FormControl } from "./form-control.js";
|
|
5
|
+
const TO_REPLACE = "____$$$$";
|
|
5
6
|
class Table extends FormControl {
|
|
6
7
|
constructor(table_field, options) {
|
|
7
8
|
super();
|
|
@@ -24,7 +25,7 @@ class Table extends FormControl {
|
|
|
24
25
|
${Object.entries(this.table_field.columns).map(([key]) => {
|
|
25
26
|
return tempstream`<td>${this.options.render_fields[key](
|
|
26
27
|
fctx,
|
|
27
|
-
`${this.table_field.name}[][${key}]`,
|
|
28
|
+
`${this.table_field.name}[${row_index}][${key}]`,
|
|
28
29
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
29
30
|
row?.[key] || ""
|
|
30
31
|
)}</td>`;
|
|
@@ -32,13 +33,19 @@ class Table extends FormControl {
|
|
|
32
33
|
${this.options.allow_removing ? (
|
|
33
34
|
/* HTML */
|
|
34
35
|
`<td>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
<button
|
|
37
|
+
onclick="this.closest('tr').remove()"
|
|
38
|
+
>
|
|
39
|
+
remove
|
|
40
|
+
</button>
|
|
41
|
+
<noscript>
|
|
42
|
+
<input
|
|
43
|
+
type="submit"
|
|
44
|
+
data-turbo-frame="${this.getFrameID()}"
|
|
45
|
+
value="${this.options.label_remove || "remove"}"
|
|
46
|
+
form="${fctx.form_id}"
|
|
47
|
+
formnovalidate
|
|
48
|
+
formaction="${this.getActionURL(
|
|
42
49
|
this.table_field.name,
|
|
43
50
|
{
|
|
44
51
|
remove: {
|
|
@@ -46,7 +53,8 @@ class Table extends FormControl {
|
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
)}"
|
|
49
|
-
|
|
56
|
+
/>
|
|
57
|
+
</noscript>
|
|
50
58
|
</td>`
|
|
51
59
|
) : ""}
|
|
52
60
|
</tr>`
|
|
@@ -71,21 +79,29 @@ class Table extends FormControl {
|
|
|
71
79
|
${rows?.map((row, index) => make_row(row, index))}
|
|
72
80
|
</tbody>
|
|
73
81
|
</table>
|
|
74
|
-
<template> ${make_row(null,
|
|
75
|
-
|
|
82
|
+
<template> ${make_row(null, TO_REPLACE)} </template>
|
|
83
|
+
|
|
84
|
+
${/* because of https://github.com/ljharb/qs/issues/252 we have to
|
|
85
|
+
put the indexes in the field names */
|
|
86
|
+
this.options.allow_adding ? (
|
|
76
87
|
/* HTML */
|
|
77
88
|
`<button
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
onclick="const template = this.closest('turbo-frame').querySelector('template');
|
|
90
|
+
const tbody = this.closest('turbo-frame').querySelector('tbody');
|
|
91
|
+
const orig_template = template.innerHTML;
|
|
92
|
+
template.innerHTML = orig_template.replaceAll('${TO_REPLACE}', tbody.querySelectorAll('tr').length);
|
|
93
|
+
tbody.appendChild(template.cloneNode(true).content);
|
|
94
|
+
template.innerHTML = orig_template;"
|
|
95
|
+
>
|
|
96
|
+
add
|
|
97
|
+
</button>
|
|
98
|
+
<noscript>
|
|
99
|
+
<input
|
|
100
|
+
type="submit"
|
|
101
|
+
value="${this.options.label_add || "add"}"
|
|
102
|
+
form="${fctx.form_id}"
|
|
103
|
+
formnovalidate
|
|
104
|
+
formaction="${this.getActionURL(
|
|
89
105
|
this.table_field.name,
|
|
90
106
|
{
|
|
91
107
|
insert: {
|
|
@@ -94,8 +110,8 @@ class Table extends FormControl {
|
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
)}"
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
/>
|
|
114
|
+
</noscript>`
|
|
99
115
|
) : ""}
|
|
100
116
|
</div>
|
|
101
117
|
</turbo-frame>`;
|
|
@@ -176,14 +176,14 @@ class StatefulPage extends Mountable {
|
|
|
176
176
|
router.post(path, async (ctx) => {
|
|
177
177
|
const { action, state, inputs, action_args } = await this.extractState(ctx);
|
|
178
178
|
if (action) {
|
|
179
|
-
const new_state = await this.actions[action](
|
|
179
|
+
const new_state = await this.actions[action]({
|
|
180
180
|
ctx,
|
|
181
181
|
state,
|
|
182
182
|
inputs,
|
|
183
|
-
|
|
183
|
+
args: JSON.parse(
|
|
184
184
|
from_base64(action_args)
|
|
185
185
|
)
|
|
186
|
-
);
|
|
186
|
+
});
|
|
187
187
|
ctx.body = this.render(ctx, new_state, inputs);
|
|
188
188
|
} else {
|
|
189
189
|
ctx.body = this.render(ctx, state, inputs);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { tempstream } from "tempstream";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
StatefulPage
|
|
4
|
+
} from "./stateful-page.js";
|
|
3
5
|
describe("stateful page", () => {
|
|
6
|
+
it("has types that allow for extracting action argument types", () => {
|
|
7
|
+
const action = async ({}) => {
|
|
8
|
+
};
|
|
9
|
+
const a = [2];
|
|
10
|
+
});
|
|
4
11
|
it("handles a basic case with action buttons", () => {
|
|
5
12
|
const actions = {
|
|
6
|
-
some_action: async (
|
|
13
|
+
some_action: async ({}) => {
|
|
7
14
|
}
|
|
8
15
|
};
|
|
9
16
|
new class extends StatefulPage {
|
package/package.json
CHANGED
|
@@ -38,6 +38,8 @@ export type TableControlOptions<F extends Record<string, FormField>> = {
|
|
|
38
38
|
}
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
+
const TO_REPLACE = "____$$$$";
|
|
42
|
+
|
|
41
43
|
export class Table<F extends Record<string, FormField>> extends FormControl {
|
|
42
44
|
role = <const>"input";
|
|
43
45
|
constructor(
|
|
@@ -64,13 +66,13 @@ export class Table<F extends Record<string, FormField>> extends FormControl {
|
|
|
64
66
|
>;
|
|
65
67
|
}
|
|
66
68
|
| null,
|
|
67
|
-
row_index: number
|
|
69
|
+
row_index: number | string
|
|
68
70
|
) => {
|
|
69
71
|
return /* HTML */ tempstream`<tr>
|
|
70
72
|
${Object.entries(this.table_field.columns).map(([key]) => {
|
|
71
73
|
return tempstream`<td>${this.options.render_fields[key](
|
|
72
74
|
fctx,
|
|
73
|
-
`${this.table_field.name}[][${key}]`,
|
|
75
|
+
`${this.table_field.name}[${row_index}][${key}]`,
|
|
74
76
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
75
77
|
(row?.[key] as string) || ""
|
|
76
78
|
)}</td>`;
|
|
@@ -78,22 +80,29 @@ export class Table<F extends Record<string, FormField>> extends FormControl {
|
|
|
78
80
|
${
|
|
79
81
|
this.options.allow_removing
|
|
80
82
|
? /* HTML */ `<td>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
{
|
|
91
|
-
remove
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
<button
|
|
84
|
+
onclick="this.closest('tr').remove()"
|
|
85
|
+
>
|
|
86
|
+
remove
|
|
87
|
+
</button>
|
|
88
|
+
<noscript>
|
|
89
|
+
<input
|
|
90
|
+
type="submit"
|
|
91
|
+
data-turbo-frame="${this.getFrameID()}"
|
|
92
|
+
value="${this.options
|
|
93
|
+
.label_remove || "remove"}"
|
|
94
|
+
form="${fctx.form_id}"
|
|
95
|
+
formnovalidate
|
|
96
|
+
formaction="${this.getActionURL(
|
|
97
|
+
this.table_field.name,
|
|
98
|
+
{
|
|
99
|
+
remove: {
|
|
100
|
+
index: row_index,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
)}"
|
|
104
|
+
/>
|
|
105
|
+
</noscript>
|
|
97
106
|
</td>`
|
|
98
107
|
: ""
|
|
99
108
|
}
|
|
@@ -127,31 +136,42 @@ export class Table<F extends Record<string, FormField>> extends FormControl {
|
|
|
127
136
|
${rows?.map((row, index) => make_row(row, index))}
|
|
128
137
|
</tbody>
|
|
129
138
|
</table>
|
|
130
|
-
<template> ${make_row(null,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
139
|
+
<template> ${make_row(null, TO_REPLACE)} </template>
|
|
140
|
+
|
|
141
|
+
${
|
|
142
|
+
/* because of https://github.com/ljharb/qs/issues/252 we have to
|
|
143
|
+
put the indexes in the field names */
|
|
144
|
+
this.options.allow_adding
|
|
145
|
+
? /* HTML */ `<button
|
|
146
|
+
onclick="const template = this.closest('turbo-frame').querySelector('template');
|
|
147
|
+
const tbody = this.closest('turbo-frame').querySelector('tbody');
|
|
148
|
+
const orig_template = template.innerHTML;
|
|
149
|
+
template.innerHTML = orig_template.replaceAll('${TO_REPLACE}', tbody.querySelectorAll('tr').length);
|
|
150
|
+
tbody.appendChild(template.cloneNode(true).content);
|
|
151
|
+
template.innerHTML = orig_template;"
|
|
152
|
+
>
|
|
153
|
+
add
|
|
154
|
+
</button>
|
|
155
|
+
<noscript>
|
|
156
|
+
<input
|
|
157
|
+
type="submit"
|
|
158
|
+
value="${this.options.label_add ||
|
|
159
|
+
"add"}"
|
|
160
|
+
form="${fctx.form_id}"
|
|
161
|
+
formnovalidate
|
|
162
|
+
formaction="${this.getActionURL(
|
|
163
|
+
this.table_field.name,
|
|
164
|
+
{
|
|
165
|
+
insert: {
|
|
166
|
+
index: rows.length,
|
|
167
|
+
value: {},
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
)}"
|
|
171
|
+
/>
|
|
172
|
+
</noscript>`
|
|
173
|
+
: ""
|
|
174
|
+
}
|
|
155
175
|
</div>
|
|
156
176
|
</turbo-frame>`;
|
|
157
177
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { BaseContext } from "koa";
|
|
2
2
|
import { Templatable, tempstream } from "tempstream";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ExtractStatefulPageActionArgs,
|
|
5
|
+
StatefulPage,
|
|
6
|
+
StatefulPageActionArgument,
|
|
7
|
+
} from "./stateful-page.js";
|
|
4
8
|
|
|
5
9
|
describe("stateful page", () => {
|
|
10
|
+
it("has types that allow for extracting action argument types", () => {
|
|
11
|
+
type TestState = {};
|
|
12
|
+
const action = async ({}: StatefulPageActionArgument<TestState, [number]>) => {};
|
|
13
|
+
type Args = ExtractStatefulPageActionArgs<typeof action>;
|
|
14
|
+
const a = [2] as Args; // should not throw a typescript error;
|
|
15
|
+
});
|
|
16
|
+
|
|
6
17
|
it("handles a basic case with action buttons", () => {
|
|
7
18
|
type TestState = {};
|
|
8
19
|
const actions = <const>{
|
|
9
|
-
some_action: async (
|
|
10
|
-
_ctx: BaseContext,
|
|
11
|
-
_state: TestState,
|
|
12
|
-
_inputs: Record<string, unknown>,
|
|
13
|
-
_arg1: number
|
|
14
|
-
) => {},
|
|
20
|
+
some_action: async ({}: StatefulPageActionArgument<TestState, [number]>) => {},
|
|
15
21
|
};
|
|
16
22
|
new (class extends StatefulPage<TestState, typeof actions> {
|
|
17
23
|
actions = actions;
|
|
@@ -5,7 +5,6 @@ import deepmerge, { ArrayMergeOptions } from "deepmerge";
|
|
|
5
5
|
import { BaseContext, Context } from "koa";
|
|
6
6
|
import { Templatable, tempstream } from "tempstream";
|
|
7
7
|
import { from_base64, to_base64 } from "../utils/base64.js";
|
|
8
|
-
import { ExtractTail } from "../utils/utils.js";
|
|
9
8
|
import { Mountable } from "./mountable.js";
|
|
10
9
|
import { isPlainObject } from "is-what";
|
|
11
10
|
|
|
@@ -33,17 +32,31 @@ export type StateAndMetadata<State, Actions> =
|
|
|
33
32
|
$: Record<string, unknown>;
|
|
34
33
|
};
|
|
35
34
|
|
|
35
|
+
export type StatefulPageActionArgument<
|
|
36
|
+
State extends Record<string, unknown>,
|
|
37
|
+
Args extends unknown[] = unknown[]
|
|
38
|
+
> = {
|
|
39
|
+
ctx: BaseContext;
|
|
40
|
+
state: State;
|
|
41
|
+
inputs: Record<string, string>;
|
|
42
|
+
args: Args;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type StatefulPageAction<
|
|
46
|
+
State extends Record<string, unknown>,
|
|
47
|
+
Args extends unknown[] = unknown[]
|
|
48
|
+
> = (obj: StatefulPageActionArgument<State, Args>) => State | Promise<State>;
|
|
49
|
+
|
|
50
|
+
export type ExtractStatefulPageActionArgs<X> = X extends StatefulPageAction<
|
|
51
|
+
any,
|
|
52
|
+
infer Args
|
|
53
|
+
>
|
|
54
|
+
? Args
|
|
55
|
+
: never;
|
|
56
|
+
|
|
36
57
|
export abstract class StatefulPage<
|
|
37
58
|
State extends Record<string, unknown>,
|
|
38
|
-
Actions extends Record<
|
|
39
|
-
string,
|
|
40
|
-
(
|
|
41
|
-
ctx: BaseContext,
|
|
42
|
-
state: State,
|
|
43
|
-
inputs: Record<string, string>,
|
|
44
|
-
...args: unknown[]
|
|
45
|
-
) => State | Promise<State>
|
|
46
|
-
>
|
|
59
|
+
Actions extends Record<string, StatefulPageAction<State>>
|
|
47
60
|
> extends Mountable {
|
|
48
61
|
abstract actions: Actions;
|
|
49
62
|
abstract getInitialState(ctx: BaseContext): State | Promise<State>;
|
|
@@ -123,9 +136,7 @@ export abstract class StatefulPage<
|
|
|
123
136
|
|
|
124
137
|
makeActionURL<ActionName extends keyof Actions>(
|
|
125
138
|
action_description: StatefulPageActionDescription<ActionName>,
|
|
126
|
-
...args:
|
|
127
|
-
ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
|
|
128
|
-
>
|
|
139
|
+
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
|
|
129
140
|
) {
|
|
130
141
|
const { action } = this.extractActionAndLabel(action_description);
|
|
131
142
|
return `./?action=${action}&action_args=${encodeURIComponent(
|
|
@@ -137,9 +148,7 @@ export abstract class StatefulPage<
|
|
|
137
148
|
makeActionButton<ActionName extends keyof Actions>(
|
|
138
149
|
_state: State,
|
|
139
150
|
action_description: StatefulPageActionDescription<ActionName>,
|
|
140
|
-
...args:
|
|
141
|
-
ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
|
|
142
|
-
>
|
|
151
|
+
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
|
|
143
152
|
) {
|
|
144
153
|
const { label, content, disabled } =
|
|
145
154
|
this.extractActionAndLabel(action_description);
|
|
@@ -162,9 +171,7 @@ export abstract class StatefulPage<
|
|
|
162
171
|
label?: string;
|
|
163
172
|
}
|
|
164
173
|
| ActionName,
|
|
165
|
-
...args:
|
|
166
|
-
ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
|
|
167
|
-
>
|
|
174
|
+
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
|
|
168
175
|
) {
|
|
169
176
|
return `(()=>{const form = this.closest('form'); form.action='${this.makeActionURL(
|
|
170
177
|
action_description,
|
|
@@ -301,14 +308,14 @@ export abstract class StatefulPage<
|
|
|
301
308
|
const { action, state, inputs, action_args } =
|
|
302
309
|
await this.extractState(ctx);
|
|
303
310
|
if (action) {
|
|
304
|
-
const new_state = await this.actions[action](
|
|
311
|
+
const new_state = await this.actions[action]({
|
|
305
312
|
ctx,
|
|
306
313
|
state,
|
|
307
314
|
inputs,
|
|
308
|
-
|
|
315
|
+
args: JSON.parse(
|
|
309
316
|
from_base64(action_args as string)
|
|
310
|
-
) as unknown[]
|
|
311
|
-
);
|
|
317
|
+
) as unknown[],
|
|
318
|
+
});
|
|
312
319
|
ctx.body = this.render(ctx, new_state, inputs);
|
|
313
320
|
} else {
|
|
314
321
|
ctx.body = this.render(ctx, state, inputs);
|