@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.
@@ -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 declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, (ctx: BaseContext, state: State, inputs: Record<string, string>, ...args: unknown[]) => State | Promise<State>>> extends Mountable {
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: ExtractTail<ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>>): string;
43
- makeActionButton<ActionName extends keyof Actions>(_state: State, action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>>): string;
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: ExtractTail<ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>>): string;
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
- <input
36
- type="submit"
37
- data-turbo-frame="${this.getFrameID()}"
38
- value="${this.options.label_remove || "remove"}"
39
- form="${fctx.form_id}"
40
- formnovalidate
41
- formaction="${this.getActionURL(
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, rows.length)} </template>
75
- ${this.options.allow_adding ? (
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
- onclick="this.closest('turbo-frame').querySelector('tbody').appendChild(this.closest('turbo-frame').querySelector('template').cloneNode(true).content)"
79
- >
80
- add
81
- </button>
82
- <noscript>
83
- <input
84
- type="submit"
85
- value="${this.options.label_add || "add"}"
86
- form="${fctx.form_id}"
87
- formnovalidate
88
- formaction="${this.getActionURL(
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
- </noscript>`
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
- ...JSON.parse(
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 { StatefulPage } from "./stateful-page.js";
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 (_ctx, _state, _inputs, _arg1) => {
13
+ some_action: async ({}) => {
7
14
  }
8
15
  };
9
16
  new class extends StatefulPage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.15.58",
3
+ "version": "0.15.60",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -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
- <input
82
- type="submit"
83
- data-turbo-frame="${this.getFrameID()}"
84
- value="${this.options.label_remove ||
85
- "remove"}"
86
- form="${fctx.form_id}"
87
- formnovalidate
88
- formaction="${this.getActionURL(
89
- this.table_field.name,
90
- {
91
- remove: {
92
- index: row_index,
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, rows.length)} </template>
131
- ${this.options.allow_adding
132
- ? /* HTML */ `<button
133
- onclick="this.closest('turbo-frame').querySelector('tbody').appendChild(this.closest('turbo-frame').querySelector('template').cloneNode(true).content)"
134
- >
135
- add
136
- </button>
137
- <noscript>
138
- <input
139
- type="submit"
140
- value="${this.options.label_add || "add"}"
141
- form="${fctx.form_id}"
142
- formnovalidate
143
- formaction="${this.getActionURL(
144
- this.table_field.name,
145
- {
146
- insert: {
147
- index: rows.length,
148
- value: {},
149
- },
150
- }
151
- )}"
152
- />
153
- </noscript>`
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 { StatefulPage } from "./stateful-page.js";
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: ExtractTail<
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: ExtractTail<
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: ExtractTail<
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
- ...(JSON.parse(
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);