@sealcode/sealgen 0.15.30 → 0.15.32

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.
@@ -0,0 +1,3 @@
1
+ export declare function addCRUD(params: Partial<{
2
+ [key in "collection" | "url"]: string;
3
+ }>, app_directory?: string): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,17 @@
1
1
  import { Field } from "sealious";
2
2
  import { FlatTemplatable } from "tempstream";
3
3
  export type ListFilterRender = (value: string, field: Field<any>) => FlatTemplatable;
4
- export declare const BooleanListFilter: (value: string, field: Field<any>) => string;
5
- export declare const SimpleTextFilter: (value: string, field: Field<any>) => string;
6
- export declare const DefaultListFilters: Record<string, ListFilterRender>;
4
+ export type DefaultListFilter = {
5
+ render: ListFilterRender;
6
+ prepareValue?: (filter_value: unknown) => unknown;
7
+ };
8
+ export declare const BooleanListFilter: {
9
+ render: (value: string, field: Field<any>) => string;
10
+ };
11
+ export declare const SimpleTextFilter: {
12
+ render: (value: string, field: Field<any>) => string;
13
+ prepareValue: (value: string) => {
14
+ regex: string;
15
+ };
16
+ };
17
+ export declare const DefaultListFilters: Record<string, DefaultListFilter>;
@@ -32,7 +32,6 @@ export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFi
32
32
  itemsPerPage: number | null;
33
33
  }>;
34
34
  renderItems(ctx: Context, values?: Record<string, FormDataValue>, items?: ItemType[]): Promise<FlatTemplatable>;
35
- render(ctx: Context): Promise<FlatTemplatable>;
36
35
  renderPagination(ctx: Context, values: Record<string, FormDataValue>): Promise<FlatTemplatable>;
37
36
  private renderPageButton;
38
37
  getFilterValues(ctx: Context): Promise<Record<string, unknown>>;
@@ -45,4 +44,5 @@ export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFi
45
44
  field: string;
46
45
  label?: string;
47
46
  }[]): FlatTemplatable;
47
+ render(ctx: Context): Promise<FlatTemplatable>;
48
48
  }
@@ -10,6 +10,7 @@ export type PageErrorMessage = {
10
10
  message: string;
11
11
  };
12
12
  export type Fields = Record<string, FormField<boolean, any>>;
13
+ type Resolved<T> = T extends Promise<infer X> ? X : never;
13
14
  export declare abstract class MountableWithFields<F extends Fields = Fields> extends Mountable {
14
15
  fields: F;
15
16
  field_names_prefix: string;
@@ -32,6 +33,9 @@ export declare abstract class MountableWithFields<F extends Fields = Fields> ext
32
33
  renderError(_: Context, error: PageErrorMessage): Promise<FlatTemplatable>;
33
34
  renderMessages(_: Context, data: FormData<keyof Fields>): FlatTemplatable;
34
35
  abstract extractRawValues(ctx: Context): Promise<Record<string, FormDataValue>>;
35
- getParsedValues(ctx: Context): Promise<ShapeToType<FieldsToShape<F>>>;
36
+ getParsedValues(ctx: Context): Promise<{
37
+ [field in keyof F]: Resolved<ReturnType<F[field]["getValue"]>>["parsed"];
38
+ }>;
36
39
  getDatabaseValues(ctx: Context): Promise<ShapeToType<FieldsToShape<F>>>;
37
40
  }
41
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function createItemFormTemplate(create_action_name: string, newfilefullpath: string, collection_name: string, list_action: string): Promise<string>;
@@ -0,0 +1,15 @@
1
+ export declare function makeDefaultFilterFields(collection_fields: {
2
+ name: string;
3
+ type: string;
4
+ }[]): string;
5
+ export declare function makeDefaultDisplayFields(collection_fields: {
6
+ name: string;
7
+ type: string;
8
+ }[]): string;
9
+ declare const default_hooks: {
10
+ pre_header_html: string;
11
+ post_header_html: string;
12
+ post_import_js: string;
13
+ };
14
+ export declare function collectionListTemplate(collection_name: string, action_name: string, newfilefullpath: string, _hooks?: Partial<typeof default_hooks>): Promise<string>;
15
+ export {};
@@ -0,0 +1 @@
1
+ export declare function sharedCrudFormFields(collection_name: string, newfilefullpath: string): Promise<string>;
@@ -12,6 +12,7 @@ export declare class RealAppTest {
12
12
  start(): Promise<void>;
13
13
  close(): Promise<void>;
14
14
  runSealgenCommand(command: string): Promise<void>;
15
+ build(): Promise<void>;
15
16
  throwIfItKillsTheApp<T>(cb: () => Promise<T>): Promise<T>;
16
17
  httpGET(path: string): Promise<{
17
18
  status: number;
@@ -22,4 +23,5 @@ export declare class RealAppTest {
22
23
  response: string;
23
24
  }>;
24
25
  addFile(target_path: string, content: string): Promise<void>;
26
+ fullURL(path: string): string;
25
27
  }
@@ -0,0 +1 @@
1
+ export declare function writeFile(path: string, content: string): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { getBrowser } from "./utils/browser-creator.js";
2
+ import { RealAppTest } from "./test_utils/test-on-real-app.js";
3
+ import { collectionTemplate } from "./templates/collection.js";
4
+ import assert from "node:assert";
5
+ describe("single-reference.test", () => {
6
+ let page;
7
+ before(async () => {
8
+ const browser = await getBrowser();
9
+ const context = await browser.newContext();
10
+ page = await context.newPage();
11
+ });
12
+ after(async () => {
13
+ await page.close();
14
+ });
15
+ it("creates an item list page with working partial string search for text fields", async function() {
16
+ const test_app = await RealAppTest.init();
17
+ await test_app.addFile(
18
+ "src/back/collections/articles.ts",
19
+ collectionTemplate("articles", `title: new FieldTypes.Text(),`)
20
+ );
21
+ await test_app.runSealgenCommand(
22
+ `add-route --action=ListArticles --mode=list --url=/list-articles --collection=articles`
23
+ );
24
+ await test_app.start();
25
+ await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article ABC" });
26
+ await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article DEF" });
27
+ await page.goto(test_app.fullURL(`list-articles/`));
28
+ await page.getByLabel("title").click();
29
+ await page.getByLabel("title").fill("ABC");
30
+ await page.getByLabel("title").press("Enter");
31
+ await page.getByRole("cell", { name: "article ABC" }).click();
32
+ const { response } = await test_app.httpGET(`/list-articles/?page=1&filter%5Btitle%5D=ABC`);
33
+ assert(response.includes("article ABC"));
34
+ assert(!response.includes("article DEF"));
35
+ await test_app.close();
36
+ }).timeout(100 * 1e3);
37
+ });
38
+ //# sourceMappingURL=add-list-route.test.js.map
@@ -37,8 +37,6 @@ describe("single-reference.test", () => {
37
37
  validate_method: ""
38
38
  })
39
39
  );
40
- const path = "/sample";
41
- await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
42
40
  await test_app.start();
43
41
  await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 1" });
44
42
  await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 2" });
@@ -1,37 +1,44 @@
1
- const BooleanListFilter = (value, field) => {
2
- return (
3
- /* HTML */
4
- `<label>
5
- ${field.name}
6
- <select name="filter[${field.name}]" autocomplete="off">
7
- <option value="true" ${value == "true" ? "selected" : ""}>
8
- True
9
- </option>
10
- <option value="false" ${value == "false" ? "selected" : ""}>
11
- False
12
- </option>
13
- <option
14
- value=""
15
- ${value == "null" || value == "" || value == "undefined" || value == void 0 ? "selected" : ""}
16
- >
17
- -
18
- </option>
19
- </select>
20
- </label>`
21
- );
1
+ const BooleanListFilter = {
2
+ render: (value, field) => {
3
+ return (
4
+ /* HTML */
5
+ `<label>
6
+ ${field.name}
7
+ <select name="filter[${field.name}]" autocomplete="off">
8
+ <option value="true" ${value == "true" ? "selected" : ""}>
9
+ True
10
+ </option>
11
+ <option value="false" ${value == "false" ? "selected" : ""}>
12
+ False
13
+ </option>
14
+ <option
15
+ value=""
16
+ ${value == "null" || value == "" || value == "undefined" || value == void 0 ? "selected" : ""}
17
+ >
18
+ -
19
+ </option>
20
+ </select>
21
+ </label>`
22
+ );
23
+ }
22
24
  };
23
- const SimpleTextFilter = (value, field) => {
24
- return (
25
- /* HTML */
26
- `<label>
27
- ${field.name}
28
- <input
29
- type="text"
30
- name="${field.name}"
31
- ${value ? `value="${value}"` : value}
32
- />
33
- </label>`
34
- );
25
+ const SimpleTextFilter = {
26
+ render: (value, field) => {
27
+ return (
28
+ /* HTML */
29
+ `<label>
30
+ ${field.name}
31
+ <input
32
+ type="text"
33
+ name="filter[${field.name}]"
34
+ ${value ? `value="${value}"` : value}
35
+ />
36
+ </label>`
37
+ );
38
+ },
39
+ prepareValue: (value) => {
40
+ return { regex: `.*${value}.*` };
41
+ }
35
42
  };
36
43
  const DefaultListFilters = {
37
44
  boolean: BooleanListFilter,
package/lib/page/list.js CHANGED
@@ -66,12 +66,6 @@ class ListPage extends Page {
66
66
  (items2) => items2.map((item, index) => this.renderItem(ctx, item, index))
67
67
  )}`;
68
68
  }
69
- async render(ctx) {
70
- const values = await this.extractRawValues(ctx);
71
- return tempstream`${this.renderPagination(ctx, values)}
72
- ${this.renderFilters(ctx)}
73
- ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
74
- }
75
69
  async renderPagination(ctx, values) {
76
70
  const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
77
71
  const totalIems = await this.getTotalPages(
@@ -198,6 +192,12 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
198
192
  </tr>
199
193
  </thead>`;
200
194
  }
195
+ async render(ctx) {
196
+ const values = await this.extractRawValues(ctx);
197
+ return tempstream`${this.renderPagination(ctx, values)}
198
+ ${this.renderFilters(ctx)}
199
+ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
200
+ }
201
201
  }
202
202
  export {
203
203
  BaseListPageFields,
@@ -0,0 +1,25 @@
1
+ import { Form } from "../forms/form.js";
2
+ import { FieldTypes } from "sealious";
3
+ import { CollectionField } from "../forms/fields/collection-field.js";
4
+ describe("mountable-with-fields", () => {
5
+ it("properly types the getParsedValues method", () => {
6
+ const fields = { name: new CollectionField(true, new FieldTypes.Text()) };
7
+ new class extends Form {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.fields = fields;
11
+ this.controls = [];
12
+ }
13
+ async onSubmit(ctx, form_values) {
14
+ const data = await this.getParsedValues(ctx);
15
+ data.name?.trim();
16
+ const { parsed: data2 } = await this.fields.name.getValue(
17
+ ctx,
18
+ form_values.raw_values
19
+ );
20
+ data2?.trim();
21
+ }
22
+ }();
23
+ });
24
+ });
25
+ //# sourceMappingURL=mountable-with-fields.test.js.map
@@ -53,6 +53,11 @@ class SealiousItemListPage extends ListPage {
53
53
  _filter_values,
54
54
  predicates.record(predicates.string, predicates.string)
55
55
  ) ? _filter_values : {};
56
+ for (const key in filter_values) {
57
+ if (filter_values[key] == "") {
58
+ delete filter_values[key];
59
+ }
60
+ }
56
61
  return filter_values;
57
62
  }
58
63
  renderListContainer(_, content) {
@@ -29,7 +29,7 @@ const listTemplate = async (action_name, newfilefullpath) => {
29
29
  ]);
30
30
  const importPath = curryImportPath(newfilefullpath);
31
31
  const result = `import type { Context } from "koa";
32
- import type { CollectionItem } from "sealious";
32
+ import type { CollectionItem, ItemListResult } from "sealious";
33
33
  import type { FlatTemplatable } from "tempstream";
34
34
  import { TempstreamJSX } from "tempstream";
35
35
  import { ${uppercase_collection} } from "${importPath(
@@ -53,11 +53,12 @@ export const actionName = "${action_name}";
53
53
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
54
54
  const filterFields = [
55
55
  ${collection_fields.map(
56
- (field) => ` { field: "${field.name}", render: ${DefaultListFilters[field.type] ? `DefaultListFilters["${field.type}"]` : `DefaultListFilters.fallback`} }`
56
+ (field) => ` { field: "${field.name}", ...${DefaultListFilters[field.type] ? `DefaultListFilters["${field.type}"]` : `DefaultListFilters.fallback`} }`
57
57
  ).join(",\n")}
58
58
  ] as {
59
59
  field: keyof (typeof ${uppercase_collection})["fields"];
60
60
  render?: ListFilterRender;
61
+ prepareValue?: (filter_value: unknown)=>unknown; // set this function to change what filter value is passed to Sealious
61
62
  }[];
62
63
 
63
64
  const displayFields = [
@@ -87,6 +88,7 @@ ${collection_fields.map((field) => {
87
88
  format: async (value: FilePointer) => {
88
89
  return imageRouter.image(await value.getPath(), {
89
90
  container: { width: 45, height: 45 },
91
+ alt: "",
90
92
  });
91
93
  },
92
94
  }`;
@@ -96,7 +98,7 @@ ${collection_fields.map((field) => {
96
98
  ] as {
97
99
  field: string;
98
100
  label: string;
99
- format?: (value: unknown) => FlatTemplatable;
101
+ format?: (value: unknown, item: CollectionItem<typeof Opinions>) => FlatTemplatable;
100
102
  }[];
101
103
 
102
104
  export default new (class ${action_name}Page extends SealiousItemListPage<
@@ -108,23 +110,19 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
108
110
  async renderFilters(ctx: Context): Promise<FlatTemplatable> {
109
111
  const query_params = qs.parse(ctx.search.slice(1));
110
112
  query_params.page = "1";
111
- const filter_values = await this.getFilterValues(ctx);
113
+ const filter_values = await super.getFilterValues(ctx);
112
114
  return <form>
113
115
  {Object.entries(query_params).map(([key, value]) => {
114
116
  if (key == "filter") {
115
117
  return "";
116
118
  }
117
- return <input
118
- type="hidden"
119
- name={key}
120
- value={value}
121
- />;
119
+ // this is necessary to not lose any query params when the user changes the filter values
120
+ return <input type="hidden" name={key} value={value}/>;
122
121
  })}
123
122
  {filterFields.map(({ field, render }) => {
124
123
  if (!render) {
125
124
  render = DefaultListFilters.fallback;
126
125
  }
127
-
128
126
  return (
129
127
  render(filter_values[field] || "", this.collection.fields[field]) ||
130
128
  ""
@@ -132,6 +130,21 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
132
130
  })}
133
131
  <input type="submit" />
134
132
  </form>;
133
+ }
134
+
135
+ async getFilterValues(ctx: Context) {
136
+ // adding opportunity to adjust the values for a given field filter before it's sent to Sealious
137
+ const values = await super.getFilterValues(ctx);
138
+ for (const filterField of filterFields) {
139
+ const key = filterField.field as keyof typeof values;
140
+ if (key in values) {
141
+ const prepare_fn = filterField.prepareValue;
142
+ if (prepare_fn) {
143
+ values[key] = prepare_fn(values[key]) as any;
144
+ }
145
+ }
146
+ }
147
+ return values;
135
148
  }
136
149
 
137
150
  async renderItem(ctx: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
@@ -139,9 +152,18 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
139
152
  {displayFields.map(({ field, format }) => {
140
153
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
141
154
  const value = item.get(field as any);
142
- return <td>{format ? format(value) : value}</td>;
155
+ return <td>{format ? format(value, item) : value}</td>;
143
156
  })}
144
157
  </tr>;
158
+ }
159
+
160
+ renderListContainer(ctx: Context, content: Templatable): FlatTemplatable {
161
+ return (
162
+ <table class="sealious-list ${toKebabCase(action_name)}-table">
163
+ {this.renderTableHead(ctx, displayFields)}
164
+ <tbody>{content}</tbody>
165
+ </table>
166
+ );
145
167
  }
146
168
 
147
169
  async render(ctx: Context) {
@@ -150,10 +172,7 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
150
172
  "${action_name}",
151
173
  <div class="sealious-list-wrapper ${toKebabCase(action_name)}--wrapper">
152
174
  <h2>${action_name} List</h2>
153
- <table class="sealious-list">
154
- {this.renderTableHead(ctx, displayFields)}
155
- <tbody>{super.render(ctx)}</tbody>
156
- </table>
175
+ {super.render(ctx)}
157
176
  </div>
158
177
  );
159
178
  }
@@ -32,6 +32,7 @@ class RealAppTest {
32
32
  console.log(
33
33
  " starting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
34
34
  );
35
+ await test_app.runSealgenCommand("build");
35
36
  test_app.app_port = await getPort();
36
37
  return test_app;
37
38
  }
@@ -95,6 +96,10 @@ class RealAppTest {
95
96
  await execAsync(
96
97
  `cd ${this.app_path} && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
97
98
  );
99
+ await this.build();
100
+ }
101
+ async build() {
102
+ await execAsync(`cd ${this.app_path} && ${callSealgenBin()} build`);
98
103
  }
99
104
  async throwIfItKillsTheApp(cb) {
100
105
  let finished = false;
@@ -156,6 +161,10 @@ class RealAppTest {
156
161
  "\n\n\n\n=====\n"
157
162
  );
158
163
  await fs.writeFile(path.resolve(this.app_path, target_path), content);
164
+ await this.build();
165
+ }
166
+ fullURL(path2) {
167
+ return `http://localhost:${this.app_port}${path2.startsWith("/") ? "" : "/"}${path2}`;
159
168
  }
160
169
  }
161
170
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.15.30",
3
+ "version": "0.15.32",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -0,0 +1,45 @@
1
+ import { Page } from "playwright";
2
+ import { getBrowser } from "./utils/browser-creator.js";
3
+ import { RealAppTest } from "./test_utils/test-on-real-app.js";
4
+ import { collectionTemplate } from "./templates/collection.js";
5
+ import assert from "node:assert";
6
+
7
+ describe("single-reference.test", () => {
8
+ let page: Page;
9
+ before(async () => {
10
+ const browser = await getBrowser();
11
+ const context = await browser.newContext();
12
+ page = await context.newPage();
13
+ });
14
+
15
+ after(async () => {
16
+ await page.close();
17
+ });
18
+
19
+ it("creates an item list page with working partial string search for text fields", async function () {
20
+ const test_app = await RealAppTest.init();
21
+
22
+ await test_app.addFile(
23
+ "src/back/collections/articles.ts",
24
+ collectionTemplate("articles", `title: new FieldTypes.Text(),`)
25
+ );
26
+ await test_app.runSealgenCommand(
27
+ `add-route --action=ListArticles --mode=list --url=/list-articles --collection=articles`
28
+ );
29
+ await test_app.start();
30
+ await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article ABC" });
31
+ await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article DEF" });
32
+
33
+ await page.goto(test_app.fullURL(`list-articles/`));
34
+ await page.getByLabel("title").click();
35
+ await page.getByLabel("title").fill("ABC");
36
+ await page.getByLabel("title").press("Enter");
37
+ await page.getByRole("cell", { name: "article ABC" }).click();
38
+
39
+ const { response } = await test_app.httpGET(`/list-articles/?page=1&filter%5Btitle%5D=ABC`);
40
+ assert(response.includes("article ABC"));
41
+ assert(!response.includes("article DEF"));
42
+
43
+ await test_app.close();
44
+ }).timeout(100 * 1000);
45
+ });
@@ -47,8 +47,6 @@ describe("single-reference.test", () => {
47
47
  })
48
48
  );
49
49
 
50
- const path = "/sample";
51
- await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
52
50
  await test_app.start();
53
51
  await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 1" });
54
52
  await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 2" });
@@ -36,7 +36,7 @@ export class CollectionField<
36
36
  encoded_value,
37
37
  null,
38
38
  {}
39
- )) as Depromisify<ReturnType<F["encode"]>>;
39
+ )) as ExtractFieldDecoded<F>;
40
40
  return {
41
41
  parsable: true,
42
42
  error: null,
@@ -6,44 +6,56 @@ export type ListFilterRender = (
6
6
  field: Field<any>
7
7
  ) => FlatTemplatable;
8
8
 
9
- export const BooleanListFilter = (value: string, field: Field<any>) => {
10
- return /* HTML */ `<label>
11
- ${field.name}
12
- <select name="filter[${field.name}]" autocomplete="off">
13
- <option value="true" ${value == "true" ? "selected" : ""}>
14
- True
15
- </option>
16
- <option value="false" ${value == "false" ? "selected" : ""}>
17
- False
18
- </option>
19
- <option
20
- value=""
21
- ${value == "null" ||
22
- value == "" ||
23
- value == "undefined" ||
24
- value == undefined
25
- ? "selected"
26
- : ""}
27
- >
28
- -
29
- </option>
30
- </select>
31
- </label>`;
9
+ export type DefaultListFilter = {
10
+ render: ListFilterRender;
11
+ prepareValue?: (filter_value: unknown) => unknown;
32
12
  };
33
13
 
34
- export const SimpleTextFilter = (value: string, field: Field<any>) => {
35
- return /* HTML */ `<label>
36
- ${field.name}
37
- <input
38
- type="text"
39
- name="${field.name}"
40
- ${value ? `value="${value}"` : value}
41
- />
42
- </label>`;
14
+ export const BooleanListFilter = {
15
+ render: (value: string, field: Field<any>) => {
16
+ return /* HTML */ `<label>
17
+ ${field.name}
18
+ <select name="filter[${field.name}]" autocomplete="off">
19
+ <option value="true" ${value == "true" ? "selected" : ""}>
20
+ True
21
+ </option>
22
+ <option value="false" ${value == "false" ? "selected" : ""}>
23
+ False
24
+ </option>
25
+ <option
26
+ value=""
27
+ ${value == "null" ||
28
+ value == "" ||
29
+ value == "undefined" ||
30
+ value == undefined
31
+ ? "selected"
32
+ : ""}
33
+ >
34
+ -
35
+ </option>
36
+ </select>
37
+ </label>`;
38
+ },
39
+ };
40
+
41
+ export const SimpleTextFilter = {
42
+ render: (value: string, field: Field<any>) => {
43
+ return /* HTML */ `<label>
44
+ ${field.name}
45
+ <input
46
+ type="text"
47
+ name="filter[${field.name}]"
48
+ ${value ? `value="${value}"` : value}
49
+ />
50
+ </label>`;
51
+ },
52
+ prepareValue: (value: string) => {
53
+ return { regex: `.*${value}.*` };
54
+ },
43
55
  };
44
56
 
45
57
  export const DefaultListFilters = {
46
58
  boolean: BooleanListFilter,
47
59
  text: SimpleTextFilter,
48
60
  fallback: SimpleTextFilter,
49
- } as Record<string, ListFilterRender>;
61
+ } as Record<string, DefaultListFilter>;
package/src/page/list.ts CHANGED
@@ -112,14 +112,6 @@ export abstract class ListPage<
112
112
  )}`;
113
113
  }
114
114
 
115
- async render(ctx: Context): Promise<FlatTemplatable> {
116
- const values = await this.extractRawValues(ctx);
117
-
118
- return tempstream`${this.renderPagination(ctx, values)}
119
- ${this.renderFilters(ctx)}
120
- ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
121
- }
122
-
123
115
  async renderPagination(
124
116
  ctx: Context,
125
117
  values: Record<string, FormDataValue>
@@ -268,4 +260,12 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
268
260
  </tr>
269
261
  </thead>`;
270
262
  }
263
+
264
+ async render(ctx: Context): Promise<FlatTemplatable> {
265
+ const values = await this.extractRawValues(ctx);
266
+
267
+ return tempstream`${this.renderPagination(ctx, values)}
268
+ ${this.renderFilters(ctx)}
269
+ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
270
+ }
271
271
  }
@@ -0,0 +1,25 @@
1
+ import type { Context } from "koa";
2
+ import { Form } from "../forms/form.js";
3
+ import { FieldTypes } from "sealious";
4
+ import { CollectionField } from "../forms/fields/collection-field.js";
5
+ import { FormData } from "../forms/form-types.js";
6
+
7
+ describe("mountable-with-fields", () => {
8
+ it("properly types the getParsedValues method", () => {
9
+ const fields = { name: new CollectionField(true, new FieldTypes.Text()) };
10
+ new (class extends Form<typeof fields, void> {
11
+ fields = fields;
12
+ controls = [];
13
+ async onSubmit(ctx: Context, form_values: FormData) {
14
+ const data = await this.getParsedValues(ctx);
15
+ // this should not throw a Typescript error
16
+ data.name?.trim();
17
+ const { parsed: data2 } = await this.fields.name.getValue(
18
+ ctx,
19
+ form_values.raw_values
20
+ );
21
+ data2?.trim();
22
+ }
23
+ })();
24
+ });
25
+ });
@@ -18,6 +18,8 @@ export type PageErrorMessage = { type: "access" | "internal"; message: string };
18
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
19
  export type Fields = Record<string, FormField<boolean, any>>;
20
20
 
21
+ type Resolved<T> = T extends Promise<infer X> ? X : never;
22
+
21
23
  export abstract class MountableWithFields<
22
24
  F extends Fields = Fields
23
25
  > extends Mountable {
@@ -134,9 +136,11 @@ export abstract class MountableWithFields<
134
136
  ctx: Context
135
137
  ): Promise<Record<string, FormDataValue>>;
136
138
 
137
- async getParsedValues(
138
- ctx: Context
139
- ): Promise<ShapeToType<FieldsToShape<F>>> {
139
+ async getParsedValues(ctx: Context): Promise<{
140
+ [field in keyof F]: Resolved<
141
+ ReturnType<F[field]["getValue"]>
142
+ >["parsed"];
143
+ }> {
140
144
  const raw_values = await this.extractRawValues(ctx);
141
145
  const result: Record<string, unknown> = {};
142
146
  const promises = Object.entries(this.fields).map(
@@ -80,6 +80,11 @@ export abstract class SealiousItemListPage<
80
80
  )
81
81
  ? _filter_values
82
82
  : {};
83
+ for (const key in filter_values) {
84
+ if (filter_values[key] == "") {
85
+ delete filter_values[key];
86
+ }
87
+ }
83
88
  return filter_values;
84
89
  }
85
90
 
@@ -35,7 +35,7 @@ export const listTemplate = async (
35
35
  ]);
36
36
  const importPath = curryImportPath(newfilefullpath);
37
37
  const result = `import type { Context } from "koa";
38
- import type { CollectionItem } from "sealious";
38
+ import type { CollectionItem, ItemListResult } from "sealious";
39
39
  import type { FlatTemplatable } from "tempstream";
40
40
  import { TempstreamJSX } from "tempstream";
41
41
  import { ${uppercase_collection} } from "${importPath(
@@ -61,7 +61,7 @@ const filterFields = [
61
61
  ${collection_fields
62
62
  .map(
63
63
  (field) =>
64
- ` { field: "${field.name}", render: ${
64
+ ` { field: "${field.name}", ...${
65
65
  DefaultListFilters[field.type]
66
66
  ? `DefaultListFilters["${field.type}"]`
67
67
  : `DefaultListFilters.fallback`
@@ -71,6 +71,7 @@ ${collection_fields
71
71
  ] as {
72
72
  field: keyof (typeof ${uppercase_collection})["fields"];
73
73
  render?: ListFilterRender;
74
+ prepareValue?: (filter_value: unknown)=>unknown; // set this function to change what filter value is passed to Sealious
74
75
  }[];
75
76
 
76
77
  const displayFields = [
@@ -101,6 +102,7 @@ ${collection_fields
101
102
  format: async (value: FilePointer) => {
102
103
  return imageRouter.image(await value.getPath(), {
103
104
  container: { width: 45, height: 45 },
105
+ alt: "",
104
106
  });
105
107
  },
106
108
  }`;
@@ -111,7 +113,7 @@ ${collection_fields
111
113
  ] as {
112
114
  field: string;
113
115
  label: string;
114
- format?: (value: unknown) => FlatTemplatable;
116
+ format?: (value: unknown, item: CollectionItem<typeof Opinions>) => FlatTemplatable;
115
117
  }[];
116
118
 
117
119
  export default new (class ${action_name}Page extends SealiousItemListPage<
@@ -123,23 +125,19 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
123
125
  async renderFilters(ctx: Context): Promise<FlatTemplatable> {
124
126
  const query_params = qs.parse(ctx.search.slice(1));
125
127
  query_params.page = "1";
126
- const filter_values = await this.getFilterValues(ctx);
128
+ const filter_values = await super.getFilterValues(ctx);
127
129
  return <form>
128
130
  {Object.entries(query_params).map(([key, value]) => {
129
131
  if (key == "filter") {
130
132
  return "";
131
133
  }
132
- return <input
133
- type="hidden"
134
- name={key}
135
- value={value}
136
- />;
134
+ // this is necessary to not lose any query params when the user changes the filter values
135
+ return <input type="hidden" name={key} value={value}/>;
137
136
  })}
138
137
  {filterFields.map(({ field, render }) => {
139
138
  if (!render) {
140
139
  render = DefaultListFilters.fallback;
141
140
  }
142
-
143
141
  return (
144
142
  render(filter_values[field] || "", this.collection.fields[field]) ||
145
143
  ""
@@ -147,6 +145,21 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
147
145
  })}
148
146
  <input type="submit" />
149
147
  </form>;
148
+ }
149
+
150
+ async getFilterValues(ctx: Context) {
151
+ // adding opportunity to adjust the values for a given field filter before it's sent to Sealious
152
+ const values = await super.getFilterValues(ctx);
153
+ for (const filterField of filterFields) {
154
+ const key = filterField.field as keyof typeof values;
155
+ if (key in values) {
156
+ const prepare_fn = filterField.prepareValue;
157
+ if (prepare_fn) {
158
+ values[key] = prepare_fn(values[key]) as any;
159
+ }
160
+ }
161
+ }
162
+ return values;
150
163
  }
151
164
 
152
165
  async renderItem(ctx: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
@@ -154,9 +167,18 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
154
167
  {displayFields.map(({ field, format }) => {
155
168
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
156
169
  const value = item.get(field as any);
157
- return <td>{format ? format(value) : value}</td>;
170
+ return <td>{format ? format(value, item) : value}</td>;
158
171
  })}
159
172
  </tr>;
173
+ }
174
+
175
+ renderListContainer(ctx: Context, content: Templatable): FlatTemplatable {
176
+ return (
177
+ <table class="sealious-list ${toKebabCase(action_name)}-table">
178
+ {this.renderTableHead(ctx, displayFields)}
179
+ <tbody>{content}</tbody>
180
+ </table>
181
+ );
160
182
  }
161
183
 
162
184
  async render(ctx: Context) {
@@ -165,10 +187,7 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
165
187
  "${action_name}",
166
188
  <div class="sealious-list-wrapper ${toKebabCase(action_name)}--wrapper">
167
189
  <h2>${action_name} List</h2>
168
- <table class="sealious-list">
169
- {this.renderTableHead(ctx, displayFields)}
170
- <tbody>{super.render(ctx)}</tbody>
171
- </table>
190
+ {super.render(ctx)}
172
191
  </div>
173
192
  );
174
193
  }
@@ -45,6 +45,7 @@ export class RealAppTest {
45
45
  console.log(
46
46
  "\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
47
47
  );
48
+ await test_app.runSealgenCommand("build");
48
49
  test_app.app_port = await getPort();
49
50
  return test_app;
50
51
  }
@@ -112,6 +113,11 @@ export class RealAppTest {
112
113
  this.app_path
113
114
  } && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
114
115
  );
116
+ await this.build();
117
+ }
118
+
119
+ async build() {
120
+ await execAsync(`cd ${this.app_path} && ${callSealgenBin()} build`);
115
121
  }
116
122
 
117
123
  async throwIfItKillsTheApp<T>(cb: () => Promise<T>): Promise<T> {
@@ -182,5 +188,12 @@ export class RealAppTest {
182
188
  "\n\n\n\n=====\n"
183
189
  );
184
190
  await fs.writeFile(path.resolve(this.app_path, target_path), content);
191
+ await this.build();
192
+ }
193
+
194
+ fullURL(path: string): string {
195
+ return `http://localhost:${this.app_port}${
196
+ path.startsWith("/") ? "" : "/"
197
+ }${path}`;
185
198
  }
186
199
  }