@sealcode/sealgen 0.10.3 → 0.10.5

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.
@@ -1 +1,2 @@
1
+ export declare function extractCollectionClassname(full_file_path: string): Promise<string>;
1
2
  export declare function generateCollections(): Promise<void>;
package/@types/index.d.ts CHANGED
@@ -22,3 +22,4 @@ export * as sanitize from "./sanitize";
22
22
  export * from "./forms/multiform";
23
23
  export * as Components from "./components/components";
24
24
  export * from "./mount";
25
+ export * from "./page/default-list-filters";
@@ -0,0 +1,6 @@
1
+ import { Field } from "sealious";
2
+ import { FlatTemplatable } from "tempstream";
3
+ export type ListFilterRender = (value: string, field: Field) => FlatTemplatable;
4
+ export declare const BooleanListFilter: (value: string, field: Field) => string;
5
+ export declare const SimpleTextFilter: (value: string, field: Field) => string;
6
+ export declare const DefaultListFilters: Record<string, ListFilterRender>;
@@ -12,6 +12,10 @@ export declare const BaseListPageFields: {
12
12
  readonly itemsPerPage: NumberField<12, false>;
13
13
  readonly sort: SimpleFormField<false>;
14
14
  };
15
+ export type ListSort = {
16
+ field: string;
17
+ order: "asc" | "desc";
18
+ };
15
19
  export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFields> extends Page<F> {
16
20
  abstract getItems(ctx: Context, page: number, itemsPerPage: number, values: Record<string, FormDataValue>): Promise<ItemType[]>;
17
21
  abstract getTotalPages(ctx: Context, itemsPerPage: number, values: Record<string, FormDataValue>): Promise<number>;
@@ -29,13 +33,11 @@ export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFi
29
33
  renderPagination(ctx: Context, values: Record<string, FormDataValue>): Promise<FlatTemplatable>;
30
34
  private renderPageButton;
31
35
  getFilterValues(ctx: Context): Promise<Record<string, unknown>>;
32
- getSort(ctx: Context): Promise<{
33
- field: string;
34
- order: "asc" | "desc";
35
- } | null>;
36
+ getSort(ctx: Context): Promise<ListSort | null>;
36
37
  makeSortLink(ctx: Context, field: string, order: "asc" | "desc"): string;
37
38
  renderFilters(ctx: Context): Promise<FlatTemplatable>;
38
- renderHeading(ctx: Context, field: string, label?: string): FlatTemplatable;
39
+ renderHeading(ctx: Context, field: string, label?: string): Promise<FlatTemplatable>;
40
+ getDefaultSort(_: Context): ListSort | null;
39
41
  renderTableHead(ctx: Context, fields: {
40
42
  field: string;
41
43
  label?: string;
@@ -0,0 +1,4 @@
1
+ export default function extract_fields_from_collection(collection_name: string): Promise<{
2
+ name: string;
3
+ type: string;
4
+ }[]>;
@@ -15,8 +15,13 @@ describe("add route test", () => {
15
15
  const app_path = "/tmp/sealious-minimal";
16
16
  const container_name = "sealgen-test-mongo";
17
17
  afterEach(async function () {
18
- if (node_pid != 0)
19
- await execAsync(`kill -9 ${node_pid}`);
18
+ try {
19
+ if (node_pid != 0)
20
+ await execAsync(`kill -9 ${node_pid}`);
21
+ }
22
+ catch (error) {
23
+ throw new Error("node process exited unexpectedly, please inspect the output");
24
+ }
20
25
  await Promise.all([
21
26
  fs_1.promises.rm(app_path, { recursive: true, force: true }),
22
27
  execAsync(`docker rm -f ${container_name}`),
@@ -25,7 +30,7 @@ describe("add route test", () => {
25
30
  it("creates minimal sealious app", async function () {
26
31
  this.timeout(250000);
27
32
  console.log("\tcloning sealious-minimal...");
28
- await execAsync(`git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout f48991a0a8b426e0d2f4a4b448fdd41d643b0dae`);
33
+ await execAsync(`git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout ce4f33309294`);
29
34
  console.log("\tinstalling dependencies and sealgen...");
30
35
  await execAsync(`cd ${app_path} && npm ci && rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen`);
31
36
  console.log("\trunning add-route and building app...");
@@ -38,6 +43,8 @@ describe("add route test", () => {
38
43
  let app_start = (0, child_process_1.spawn)(`node`, [`${app_path}/dist/back/index.js`], {
39
44
  env,
40
45
  });
46
+ app_start.stdout.on("data", (data) => console.log(data.toString()));
47
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
41
48
  if (app_start.pid == undefined)
42
49
  throw new Error("node failed to start");
43
50
  node_pid = app_start.pid;
@@ -48,8 +55,9 @@ describe("add route test", () => {
48
55
  timeout: 6000,
49
56
  });
50
57
  const res = await axios_1.default.get("http://localhost:8080/sample");
51
- if (res.status != 200)
58
+ if (res.status != 200) {
52
59
  throw new Error("url generated by add-route exists but doesnt return 200");
60
+ }
53
61
  });
54
62
  });
55
63
  //# sourceMappingURL=add-route.test.js.map
package/lib/build.js CHANGED
@@ -71,7 +71,6 @@ async function build_ts() {
71
71
  all_named
72
72
  .match(/[a-zA-Z_$][^,}]+/g)
73
73
  .map((importname) => {
74
- console.log(importname, `importname.startsWith("default as")`, importname.startsWith("default as"));
75
74
  if (importname.startsWith("default as")) {
76
75
  return `const ${importname.replace("default as ", "")} = ${new_name}?.default || ${new_name};`;
77
76
  }
@@ -83,7 +82,6 @@ async function build_ts() {
83
82
  return `const ${importname} = ${new_name}?.default?.${importname} || ${new_name}?.${importname};`;
84
83
  })
85
84
  .join(" ");
86
- console.log({ path, result });
87
85
  return result;
88
86
  });
89
87
  return { contents, loader: "ts" };
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.generateCollections = void 0;
29
+ exports.generateCollections = exports.extractCollectionClassname = void 0;
30
30
  const locreq_1 = __importDefault(require("locreq"));
31
31
  const prettier = __importStar(require("prettier"));
32
32
  const fs_1 = require("fs");
@@ -41,6 +41,7 @@ async function extractCollectionClassname(full_file_path) {
41
41
  }
42
42
  return (0, ts_predicates_1.assertType)(result[1], ts_predicates_1.predicates.string, `Missing export default class in '${full_file_path}'?`);
43
43
  }
44
+ exports.extractCollectionClassname = extractCollectionClassname;
44
45
  async function generateCollections() {
45
46
  const collections_dir = target_locreq.resolve("src/back/collections");
46
47
  const files = (await fs_1.promises.readdir(collections_dir)).filter((f) => !/\.(sub)?test\./.exec(f) &&
package/lib/index.js CHANGED
@@ -42,4 +42,5 @@ exports.sanitize = __importStar(require("./sanitize"));
42
42
  __exportStar(require("./forms/multiform"), exports);
43
43
  exports.Components = __importStar(require("./components/components"));
44
44
  __exportStar(require("./mount"), exports);
45
+ __exportStar(require("./page/default-list-filters"), exports);
45
46
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DefaultListFilters = exports.SimpleTextFilter = exports.BooleanListFilter = void 0;
4
+ const BooleanListFilter = (value, field) => {
5
+ return /* HTML */ `<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" ||
17
+ value == "" ||
18
+ value == "undefined" ||
19
+ value == undefined
20
+ ? "selected"
21
+ : ""}
22
+ >
23
+ -
24
+ </option>
25
+ </select>
26
+ </label>`;
27
+ };
28
+ exports.BooleanListFilter = BooleanListFilter;
29
+ const SimpleTextFilter = (value, field) => {
30
+ return /* HTML */ `<label>
31
+ ${field.name}
32
+ <input
33
+ type="text"
34
+ name="${field.name}"
35
+ ${value ? `value="${value}"` : value}
36
+ />
37
+ </label>`;
38
+ };
39
+ exports.SimpleTextFilter = SimpleTextFilter;
40
+ exports.DefaultListFilters = {
41
+ boolean: exports.BooleanListFilter,
42
+ text: exports.SimpleTextFilter,
43
+ fallback: exports.SimpleTextFilter,
44
+ };
45
+ //# sourceMappingURL=default-list-filters.js.map
package/lib/page/list.js CHANGED
@@ -135,7 +135,13 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
135
135
  }
136
136
  async getSort(ctx) {
137
137
  const { sort } = await this.extractRawValues(ctx);
138
- return decodeSort(sort);
138
+ const decoded = decodeSort(sort);
139
+ if (decoded === null) {
140
+ return this.getDefaultSort(ctx);
141
+ }
142
+ else {
143
+ return decoded;
144
+ }
139
145
  }
140
146
  makeSortLink(ctx, field, order) {
141
147
  const url = new URL(ctx.url, "https://example.com");
@@ -157,8 +163,8 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
157
163
  })}
158
164
  </form>`;
159
165
  }
160
- renderHeading(ctx, field, label = field) {
161
- const current_sort = decodeSort(qs_1.default.parse(new URL(ctx.url, "https://www.example.com").search.slice(1))["sort"]);
166
+ async renderHeading(ctx, field, label = field) {
167
+ const current_sort = await this.getSort(ctx);
162
168
  const current_order = current_sort?.field == field ? current_sort.order : null;
163
169
  const order = current_order == "desc" ? "asc" : "desc";
164
170
  return /* HTML */ `<th>
@@ -169,6 +175,9 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
169
175
  >
170
176
  </th>`;
171
177
  }
178
+ getDefaultSort(_) {
179
+ return null;
180
+ }
172
181
  renderTableHead(ctx, fields) {
173
182
  return (0, tempstream_1.tempstream /* HTML */) `<thead>
174
183
  <tr>
@@ -1,9 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.listTemplate = void 0;
7
+ const locreq_1 = __importDefault(require("locreq"));
4
8
  const import_path_1 = require("../utils/import-path");
5
9
  const prompts = require("prompts");
6
10
  const list_collections_1 = require("../utils/list-collections");
11
+ const generate_collections_1 = require("../generate-collections");
12
+ const extract_fields_from_collection_1 = __importDefault(require("../utils/extract-fields-from-collection"));
13
+ const default_list_filters_1 = require("../page/default-list-filters");
14
+ const target_locreq = (0, locreq_1.default)(process.cwd());
7
15
  const listTemplate = async (action_name, newfilefullpath) => {
8
16
  const response = await prompts({
9
17
  type: "autocomplete",
@@ -14,19 +22,64 @@ const listTemplate = async (action_name, newfilefullpath) => {
14
22
  value: collection,
15
23
  })),
16
24
  });
17
- const collection = response.collection;
18
- const uppercase_collection = collection[0].toLocaleUpperCase() + collection.slice(1);
25
+ const collection_name = response.collection;
26
+ const [uppercase_collection, collection_fields] = await Promise.all([
27
+ (0, generate_collections_1.extractCollectionClassname)(target_locreq.resolve("src/back/collections/" + collection_name + ".ts")),
28
+ (0, extract_fields_from_collection_1.default)(collection_name),
29
+ ]);
19
30
  const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
20
31
  return `import { Context } from "koa";
21
- import { CollectionItem } from "sealious";
22
- import { tempstream } from "tempstream";
32
+ import { CollectionItem, Field } from "sealious";
33
+ import { FlatTemplatable, tempstream } from "tempstream";
23
34
  import { ${uppercase_collection} } from "${rel("src/back/collections/collections.js")}";
24
35
  import html from "${rel("src/back/html.js")}";
25
- import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen";
36
+ import {
37
+ SealiousItemListPage,
38
+ BaseListPageFields,
39
+ DefaultListFilters,
40
+ ListFilterRender,
41
+ } from "@sealcode/sealgen";;
42
+ import qs from "qs";
43
+ import { is, predicates } from "@sealcode/ts-predicates";
26
44
 
27
45
  export const actionName = "${action_name}";
28
46
 
29
- const filterFields = {};
47
+ const filterFields = [
48
+ ${collection_fields
49
+ .map((field) => ` { field: "${field.name}", render: ${default_list_filters_1.DefaultListFilters[field.type]
50
+ ? `DefaultListFilters["${field.type}"]`
51
+ : `DefaultListFilters.fallback`} }`)
52
+ .join(",\n")}
53
+ ] as {
54
+ field: keyof (typeof PatronEvents)["fields"];
55
+ render?: ListFilterRender;
56
+ }[];
57
+
58
+ const displayFields = [
59
+ ${collection_fields
60
+ .map((field) => {
61
+ if (field.type == "boolean") {
62
+ return ` {
63
+ field: "${field.name}",
64
+ label: "${field.name}",
65
+ format: (v: boolean) => (v ? "YES" : "NO"),
66
+ }`;
67
+ }
68
+ if (field.type == "datetime") {
69
+ return ` {
70
+ field: "${field.name}",
71
+ label: "${field.name}",
72
+ format: (v: number) => {
73
+ const d = new Date();
74
+ d.setTime(v);
75
+ return d.toString().split(" ").slice(1, 5).join(" ");
76
+ },
77
+ }`;
78
+ }
79
+ return ` { field: "${field.name}", label: "${field.name}" }`;
80
+ })
81
+ .join(",\n")}
82
+ ];
30
83
 
31
84
  export default new (class ${action_name}Page extends SealiousItemListPage<
32
85
  typeof ${uppercase_collection},
@@ -34,23 +87,14 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
34
87
  > {
35
88
  fields = BaseListPageFields;
36
89
 
37
- filterFields = filterFields;
38
- filterControls = [];
39
-
40
90
  async render(ctx: Context) {
41
91
  return html(
42
92
  ctx,
43
- "${uppercase_collection}",
93
+ "${action_name}",
44
94
  tempstream/* HTML */ \`<div>
45
- <h2>${uppercase_collection} List</h2>
95
+ <h2>${action_name}</h2>
46
96
  <table class="sealious-list">
47
- \${this.renderTableHead(ctx, [
48
- { field: "id" },
49
- ...Object.keys(Patrons.fields).map((field) => ({
50
- field,
51
- label: field,
52
- })),
53
- ])}
97
+ \${this.renderTableHead(ctx, displayFields)}
54
98
  <tbody>
55
99
  \${super.render(ctx)}
56
100
  </tbody>
@@ -59,11 +103,54 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
59
103
  );
60
104
  }
61
105
 
106
+ async renderFilters(ctx: Context): Promise<FlatTemplatable> {
107
+ const query_params = qs.parse(ctx.search.slice(1));
108
+ query_params.page = "1";
109
+ const filter_values = await this.getFilterValues(ctx);
110
+ return tempstream/* HTML */ \`<form>
111
+ \${Object.entries(query_params).map(([key, value]) => {
112
+ if (key == "filter") {
113
+ return "";
114
+ }
115
+ return /* HTML */ \`<input
116
+ type="hidden"
117
+ name="\${key}"
118
+ value="\${value}"
119
+ />\`;
120
+ })}
121
+ \${filterFields.map(({ field, render }) => {
122
+ if (!render) {
123
+ render = DefaultListFilters.fallback;
124
+ }
125
+
126
+ return (
127
+ render(filter_values[field] || "", this.collection.fields[field]) ||
128
+ ""
129
+ );
130
+ })}
131
+ <input type="submit" />
132
+ </form>\`;
133
+ }
134
+
135
+ async getFilterValues(ctx: Context) {
136
+ const query_params = qs.parse(ctx.search.slice(1));
137
+ const _filter_values = query_params?.filter || {};
138
+ let filter_values: Record<string, string | undefined> = is(
139
+ _filter_values,
140
+ predicates.record(predicates.string, predicates.string)
141
+ )
142
+ ? _filter_values
143
+ : {};
144
+ return filter_values;
145
+ }
146
+
62
147
  async renderItem(_: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
63
- return tempstream\`<tr><td>\${item.id}</td>\${Object.keys(${uppercase_collection}.fields).map(
64
- (fieldname: keyof typeof ${uppercase_collection}["fields"]) =>
65
- tempstream\`<td>\${item.get(fieldname)}</td>\`
66
- )}</tr>\`;
148
+ return tempstream/* HTML */ \`<tr>
149
+ \${displayFields.map(({ field, format }) => {
150
+ const value = item.get(field as any);
151
+ return tempstream\`<td>\${format ? format(value) : value}</td>\`;
152
+ })}
153
+ </tr>\`;
67
154
  }
68
155
  })(${uppercase_collection});
69
156
  `;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const locreq_1 = __importDefault(require("locreq"));
7
+ const utils_1 = require("./utils");
8
+ const target_locreq = (0, locreq_1.default)(process.cwd());
9
+ const fs_1 = require("fs");
10
+ async function extract_fields_from_collection(collection_name) {
11
+ const rel = (0, utils_1.curryImportPath)(target_locreq.resolve("dist/back"));
12
+ const extractor_code = `import {default as Collection} from "${rel(`./back/collections/${collection_name}.js`)}";
13
+ const c = new Collection();
14
+ const fields = [];
15
+ for (const field_name in c.fields){
16
+ const field = c.fields[field_name];
17
+ let type = field.typeName;
18
+ if(["derived-value", "cached-value"].includes(type)){
19
+ type = field.virtual_field.typeName
20
+ }
21
+ fields.push({name: field_name, type})
22
+ }
23
+ console.log(JSON.stringify(fields));
24
+ `;
25
+ const extractor_code_path = target_locreq.resolve("dist/back/___extract_fields.js");
26
+ await fs_1.promises.writeFile(extractor_code_path, extractor_code);
27
+ const { stdout } = await (0, utils_1.exec)("node", [extractor_code_path]);
28
+ await fs_1.promises.unlink(extractor_code_path);
29
+ return JSON.parse(stdout);
30
+ }
31
+ exports.default = extract_fields_from_collection;
32
+ //# sourceMappingURL=extract-fields-from-collection.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -12,7 +12,11 @@ describe("add route test", () => {
12
12
  const app_path = "/tmp/sealious-minimal";
13
13
  const container_name = "sealgen-test-mongo";
14
14
  afterEach(async function () {
15
- if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
15
+ try {
16
+ if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
17
+ } catch (error) {
18
+ throw new Error("node process exited unexpectedly, please inspect the output");
19
+ }
16
20
  await Promise.all([
17
21
  fs.rm(app_path, { recursive: true, force: true }),
18
22
  execAsync(`docker rm -f ${container_name}`),
@@ -23,7 +27,7 @@ describe("add route test", () => {
23
27
  this.timeout(250000);
24
28
  console.log("\tcloning sealious-minimal...");
25
29
  await execAsync(
26
- `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout f48991a0a8b426e0d2f4a4b448fdd41d643b0dae`
30
+ `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout ce4f33309294`
27
31
  );
28
32
  console.log("\tinstalling dependencies and sealgen...");
29
33
  await execAsync(
@@ -43,6 +47,8 @@ describe("add route test", () => {
43
47
  let app_start = spawn(`node`, [`${app_path}/dist/back/index.js`], {
44
48
  env,
45
49
  });
50
+ app_start.stdout.on("data", (data) => console.log(data.toString()));
51
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
46
52
  if (app_start.pid == undefined) throw new Error("node failed to start");
47
53
  node_pid = app_start.pid;
48
54
  await waitPort({
@@ -52,7 +58,8 @@ describe("add route test", () => {
52
58
  timeout: 6000,
53
59
  });
54
60
  const res = await axios.get("http://localhost:8080/sample");
55
- if (res.status != 200)
61
+ if (res.status != 200) {
56
62
  throw new Error("url generated by add-route exists but doesnt return 200");
63
+ }
57
64
  });
58
65
  });
package/src/build.ts CHANGED
@@ -66,13 +66,6 @@ async function build_ts() {
66
66
  all_named
67
67
  .match(/[a-zA-Z_$][^,}]+/g)
68
68
  .map((importname: string) => {
69
- console.log(
70
- importname,
71
- `importname.startsWith("default as")`,
72
- importname.startsWith(
73
- "default as"
74
- )
75
- );
76
69
  if (
77
70
  importname.startsWith(
78
71
  "default as"
@@ -98,7 +91,6 @@ async function build_ts() {
98
91
  return `const ${importname} = ${new_name}?.default?.${importname} || ${new_name}?.${importname};`;
99
92
  })
100
93
  .join(" ");
101
- console.log({ path, result });
102
94
  return result;
103
95
  }
104
96
  );
@@ -6,7 +6,7 @@ import { getPrettierConfig } from "./utils/prettier";
6
6
 
7
7
  const target_locreq = _locreq(process.cwd());
8
8
 
9
- async function extractCollectionClassname(
9
+ export async function extractCollectionClassname(
10
10
  full_file_path: string
11
11
  ): Promise<string> {
12
12
  const file_content = await fs.readFile(full_file_path, "utf-8");
package/src/index.ts CHANGED
@@ -24,3 +24,4 @@ export * as sanitize from "./sanitize";
24
24
  export * from "./forms/multiform";
25
25
  export * as Components from "./components/components";
26
26
  export * from "./mount";
27
+ export * from "./page/default-list-filters";
@@ -0,0 +1,46 @@
1
+ import { Field } from "sealious";
2
+ import { FlatTemplatable } from "tempstream";
3
+
4
+ export type ListFilterRender = (value: string, field: Field) => FlatTemplatable;
5
+
6
+ export const BooleanListFilter = (value: string, field: Field) => {
7
+ return /* HTML */ `<label>
8
+ ${field.name}
9
+ <select name="filter[${field.name}]" autocomplete="off">
10
+ <option value="true" ${value == "true" ? "selected" : ""}>
11
+ True
12
+ </option>
13
+ <option value="false" ${value == "false" ? "selected" : ""}>
14
+ False
15
+ </option>
16
+ <option
17
+ value=""
18
+ ${value == "null" ||
19
+ value == "" ||
20
+ value == "undefined" ||
21
+ value == undefined
22
+ ? "selected"
23
+ : ""}
24
+ >
25
+ -
26
+ </option>
27
+ </select>
28
+ </label>`;
29
+ };
30
+
31
+ export const SimpleTextFilter = (value: string, field: Field) => {
32
+ return /* HTML */ `<label>
33
+ ${field.name}
34
+ <input
35
+ type="text"
36
+ name="${field.name}"
37
+ ${value ? `value="${value}"` : value}
38
+ />
39
+ </label>`;
40
+ };
41
+
42
+ export const DefaultListFilters = {
43
+ boolean: BooleanListFilter,
44
+ text: SimpleTextFilter,
45
+ fallback: SimpleTextFilter,
46
+ } as Record<string, ListFilterRender>;
package/src/page/list.ts CHANGED
@@ -18,10 +18,10 @@ export const BaseListPageFields = <const>{
18
18
  sort: new SimpleFormField(false),
19
19
  };
20
20
 
21
+ export type ListSort = { field: string; order: "asc" | "desc" };
22
+
21
23
  const SORT_SEPARATOR = ":";
22
- function decodeSort(
23
- s: unknown
24
- ): { field: string; order: "asc" | "desc" } | null {
24
+ function decodeSort(s: unknown): ListSort | null {
25
25
  if (typeof s !== "string" || !s.includes(SORT_SEPARATOR)) {
26
26
  return null;
27
27
  }
@@ -38,7 +38,7 @@ function decodeSort(
38
38
  }
39
39
  }
40
40
 
41
- function encodeSort(field: string, order: "asc" | "desc") {
41
+ function encodeSort(field: string, order: "asc" | "desc"): string {
42
42
  return [field, order].join(SORT_SEPARATOR);
43
43
  }
44
44
 
@@ -185,11 +185,14 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
185
185
  return filter;
186
186
  }
187
187
 
188
- async getSort(
189
- ctx: Context
190
- ): Promise<{ field: string; order: "asc" | "desc" } | null> {
188
+ async getSort(ctx: Context): Promise<ListSort | null> {
191
189
  const { sort } = await this.extractRawValues(ctx);
192
- return decodeSort(sort);
190
+ const decoded = decodeSort(sort);
191
+ if (decoded === null) {
192
+ return this.getDefaultSort(ctx);
193
+ } else {
194
+ return decoded;
195
+ }
193
196
  }
194
197
 
195
198
  makeSortLink(ctx: Context, field: string, order: "asc" | "desc"): string {
@@ -220,12 +223,12 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
220
223
  </form>`;
221
224
  }
222
225
 
223
- renderHeading(ctx: Context, field: string, label = field): FlatTemplatable {
224
- const current_sort = decodeSort(
225
- qs.parse(
226
- new URL(ctx.url, "https://www.example.com").search.slice(1)
227
- )["sort"]
228
- );
226
+ async renderHeading(
227
+ ctx: Context,
228
+ field: string,
229
+ label = field
230
+ ): Promise<FlatTemplatable> {
231
+ const current_sort = await this.getSort(ctx);
229
232
  const current_order =
230
233
  current_sort?.field == field ? current_sort.order : null;
231
234
  const order = current_order == "desc" ? "asc" : "desc";
@@ -238,6 +241,10 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
238
241
  </th>`;
239
242
  }
240
243
 
244
+ getDefaultSort(_: Context): ListSort | null {
245
+ return null;
246
+ }
247
+
241
248
  renderTableHead(
242
249
  ctx: Context,
243
250
  fields: { field: string; label?: string }[]
@@ -1,6 +1,11 @@
1
+ import _locreq from "locreq";
1
2
  import { curryImportPath } from "../utils/import-path";
2
3
  import prompts = require("prompts");
3
4
  import { listCollections } from "../utils/list-collections";
5
+ import { extractCollectionClassname } from "../generate-collections";
6
+ import extract_fields_from_collection from "../utils/extract-fields-from-collection";
7
+ import { DefaultListFilters } from "../page/default-list-filters";
8
+ const target_locreq = _locreq(process.cwd());
4
9
 
5
10
  export const listTemplate = async (
6
11
  action_name: string,
@@ -17,22 +22,75 @@ export const listTemplate = async (
17
22
  value: collection,
18
23
  })),
19
24
  });
20
- const collection = response.collection as string;
21
- const uppercase_collection =
22
- collection[0].toLocaleUpperCase() + collection.slice(1);
25
+ const collection_name = response.collection as string;
26
+ const [uppercase_collection, collection_fields] = await Promise.all([
27
+ extractCollectionClassname(
28
+ target_locreq.resolve(
29
+ "src/back/collections/" + collection_name + ".ts"
30
+ )
31
+ ),
32
+ extract_fields_from_collection(collection_name),
33
+ ]);
23
34
  const rel = curryImportPath(newfilefullpath);
24
35
  return `import { Context } from "koa";
25
- import { CollectionItem } from "sealious";
26
- import { tempstream } from "tempstream";
36
+ import { CollectionItem, Field } from "sealious";
37
+ import { FlatTemplatable, tempstream } from "tempstream";
27
38
  import { ${uppercase_collection} } from "${rel(
28
39
  "src/back/collections/collections.js"
29
40
  )}";
30
41
  import html from "${rel("src/back/html.js")}";
31
- import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen";
42
+ import {
43
+ SealiousItemListPage,
44
+ BaseListPageFields,
45
+ DefaultListFilters,
46
+ ListFilterRender,
47
+ } from "@sealcode/sealgen";;
48
+ import qs from "qs";
49
+ import { is, predicates } from "@sealcode/ts-predicates";
32
50
 
33
51
  export const actionName = "${action_name}";
34
52
 
35
- const filterFields = {};
53
+ const filterFields = [
54
+ ${collection_fields
55
+ .map(
56
+ (field) =>
57
+ ` { field: "${field.name}", render: ${
58
+ DefaultListFilters[field.type]
59
+ ? `DefaultListFilters["${field.type}"]`
60
+ : `DefaultListFilters.fallback`
61
+ } }`
62
+ )
63
+ .join(",\n")}
64
+ ] as {
65
+ field: keyof (typeof PatronEvents)["fields"];
66
+ render?: ListFilterRender;
67
+ }[];
68
+
69
+ const displayFields = [
70
+ ${collection_fields
71
+ .map((field) => {
72
+ if (field.type == "boolean") {
73
+ return ` {
74
+ field: "${field.name}",
75
+ label: "${field.name}",
76
+ format: (v: boolean) => (v ? "YES" : "NO"),
77
+ }`;
78
+ }
79
+ if (field.type == "datetime") {
80
+ return ` {
81
+ field: "${field.name}",
82
+ label: "${field.name}",
83
+ format: (v: number) => {
84
+ const d = new Date();
85
+ d.setTime(v);
86
+ return d.toString().split(" ").slice(1, 5).join(" ");
87
+ },
88
+ }`;
89
+ }
90
+ return ` { field: "${field.name}", label: "${field.name}" }`;
91
+ })
92
+ .join(",\n")}
93
+ ];
36
94
 
37
95
  export default new (class ${action_name}Page extends SealiousItemListPage<
38
96
  typeof ${uppercase_collection},
@@ -40,23 +98,14 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
40
98
  > {
41
99
  fields = BaseListPageFields;
42
100
 
43
- filterFields = filterFields;
44
- filterControls = [];
45
-
46
101
  async render(ctx: Context) {
47
102
  return html(
48
103
  ctx,
49
- "${uppercase_collection}",
104
+ "${action_name}",
50
105
  tempstream/* HTML */ \`<div>
51
- <h2>${uppercase_collection} List</h2>
106
+ <h2>${action_name}</h2>
52
107
  <table class="sealious-list">
53
- \${this.renderTableHead(ctx, [
54
- { field: "id" },
55
- ...Object.keys(Patrons.fields).map((field) => ({
56
- field,
57
- label: field,
58
- })),
59
- ])}
108
+ \${this.renderTableHead(ctx, displayFields)}
60
109
  <tbody>
61
110
  \${super.render(ctx)}
62
111
  </tbody>
@@ -65,11 +114,54 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
65
114
  );
66
115
  }
67
116
 
117
+ async renderFilters(ctx: Context): Promise<FlatTemplatable> {
118
+ const query_params = qs.parse(ctx.search.slice(1));
119
+ query_params.page = "1";
120
+ const filter_values = await this.getFilterValues(ctx);
121
+ return tempstream/* HTML */ \`<form>
122
+ \${Object.entries(query_params).map(([key, value]) => {
123
+ if (key == "filter") {
124
+ return "";
125
+ }
126
+ return /* HTML */ \`<input
127
+ type="hidden"
128
+ name="\${key}"
129
+ value="\${value}"
130
+ />\`;
131
+ })}
132
+ \${filterFields.map(({ field, render }) => {
133
+ if (!render) {
134
+ render = DefaultListFilters.fallback;
135
+ }
136
+
137
+ return (
138
+ render(filter_values[field] || "", this.collection.fields[field]) ||
139
+ ""
140
+ );
141
+ })}
142
+ <input type="submit" />
143
+ </form>\`;
144
+ }
145
+
146
+ async getFilterValues(ctx: Context) {
147
+ const query_params = qs.parse(ctx.search.slice(1));
148
+ const _filter_values = query_params?.filter || {};
149
+ let filter_values: Record<string, string | undefined> = is(
150
+ _filter_values,
151
+ predicates.record(predicates.string, predicates.string)
152
+ )
153
+ ? _filter_values
154
+ : {};
155
+ return filter_values;
156
+ }
157
+
68
158
  async renderItem(_: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
69
- return tempstream\`<tr><td>\${item.id}</td>\${Object.keys(${uppercase_collection}.fields).map(
70
- (fieldname: keyof typeof ${uppercase_collection}["fields"]) =>
71
- tempstream\`<td>\${item.get(fieldname)}</td>\`
72
- )}</tr>\`;
159
+ return tempstream/* HTML */ \`<tr>
160
+ \${displayFields.map(({ field, format }) => {
161
+ const value = item.get(field as any);
162
+ return tempstream\`<td>\${format ? format(value) : value}</td>\`;
163
+ })}
164
+ </tr>\`;
73
165
  }
74
166
  })(${uppercase_collection});
75
167
  `;
@@ -0,0 +1,32 @@
1
+ import _locreq from "locreq";
2
+ import { curryImportPath, exec } from "./utils";
3
+ const target_locreq = _locreq(process.cwd());
4
+ import { promises as fs } from "fs";
5
+
6
+ export default async function extract_fields_from_collection(
7
+ collection_name: string
8
+ ): Promise<{ name: string; type: string }[]> {
9
+ const rel = curryImportPath(target_locreq.resolve("dist/back"));
10
+ const extractor_code = `import {default as Collection} from "${rel(
11
+ `./back/collections/${collection_name}.js`
12
+ )}";
13
+ const c = new Collection();
14
+ const fields = [];
15
+ for (const field_name in c.fields){
16
+ const field = c.fields[field_name];
17
+ let type = field.typeName;
18
+ if(["derived-value", "cached-value"].includes(type)){
19
+ type = field.virtual_field.typeName
20
+ }
21
+ fields.push({name: field_name, type})
22
+ }
23
+ console.log(JSON.stringify(fields));
24
+ `;
25
+ const extractor_code_path = target_locreq.resolve(
26
+ "dist/back/___extract_fields.js"
27
+ );
28
+ await fs.writeFile(extractor_code_path, extractor_code);
29
+ const { stdout } = await exec("node", [extractor_code_path]);
30
+ await fs.unlink(extractor_code_path);
31
+ return JSON.parse(stdout);
32
+ }