@sealcode/sealgen 0.8.52 → 0.8.53

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.
Files changed (90) hide show
  1. package/@types/forms/controls/array.d.ts +1 -1
  2. package/@types/forms/controls/checkbox-group.d.ts +1 -1
  3. package/@types/forms/controls/collapsible-field-group.d.ts +1 -1
  4. package/@types/forms/controls/derived-frame.d.ts +1 -0
  5. package/@types/forms/controls/dropdown.d.ts +2 -2
  6. package/@types/forms/controls/form-field-control.d.ts +1 -1
  7. package/@types/forms/controls/html.d.ts +1 -1
  8. package/@types/forms/controls/multiple-files.d.ts +1 -1
  9. package/@types/forms/controls/number.d.ts +1 -1
  10. package/@types/forms/controls/simple-input.d.ts +1 -1
  11. package/@types/forms/controls/tickable.d.ts +1 -1
  12. package/@types/forms/fields/field.d.ts +6 -6
  13. package/@types/forms/fields/file.d.ts +1 -1
  14. package/@types/forms/form.d.ts +7 -7
  15. package/@types/index.d.ts +1 -0
  16. package/@types/page/list.d.ts +13 -17
  17. package/@types/page/mountable-with-fields.d.ts +2 -2
  18. package/@types/page/props-parser.d.ts +1 -1
  19. package/@types/page/sealious-list.d.ts +18 -0
  20. package/@types/utils/utils.d.ts +3 -3
  21. package/lib/add-collection.js +7 -7
  22. package/lib/add-route.js +7 -7
  23. package/lib/add-route.test.js +4 -4
  24. package/lib/build.js +14 -10
  25. package/lib/components/components.js +5 -1
  26. package/lib/components/tabs.js +2 -2
  27. package/lib/components/toggle-modal.js +2 -2
  28. package/lib/find-scss-includes.js +3 -3
  29. package/lib/forms/controls/array.js +5 -5
  30. package/lib/forms/controls/big-radio-group.js +2 -2
  31. package/lib/forms/controls/checkboxed-list-input.js +3 -3
  32. package/lib/forms/controls/collapsible-field-group.js +1 -1
  33. package/lib/forms/controls/controls.js +5 -1
  34. package/lib/forms/controls/derived-frame.js +3 -3
  35. package/lib/forms/controls/derived.js +2 -2
  36. package/lib/forms/controls/dropdown.js +2 -2
  37. package/lib/forms/controls/editable-collection-subset.js +3 -3
  38. package/lib/forms/controls/field-group.js +1 -1
  39. package/lib/forms/controls/file.js +1 -1
  40. package/lib/forms/controls/hidden-input.js +1 -1
  41. package/lib/forms/controls/multiple-files.js +5 -5
  42. package/lib/forms/controls/multiple-photos.js +1 -1
  43. package/lib/forms/controls/photo.js +1 -1
  44. package/lib/forms/controls/simple-input.js +2 -2
  45. package/lib/forms/controls/tickable.js +3 -3
  46. package/lib/forms/fields/boolean.js +3 -3
  47. package/lib/forms/fields/checkboxed-list.js +3 -3
  48. package/lib/forms/fields/enum-multiple.js +3 -3
  49. package/lib/forms/fields/enum.js +2 -2
  50. package/lib/forms/fields/field.js +6 -2
  51. package/lib/forms/fields/file.js +6 -6
  52. package/lib/forms/fields/number.js +5 -5
  53. package/lib/forms/fields/pick-from-list.js +1 -1
  54. package/lib/forms/fields/regexp.js +2 -2
  55. package/lib/forms/fields/simple-form-field.js +4 -4
  56. package/lib/forms/form.js +9 -9
  57. package/lib/forms/form.test.js +12 -12
  58. package/lib/forms/multiform.js +5 -5
  59. package/lib/forms/multiform.test.js +26 -26
  60. package/lib/generate-collections.js +8 -4
  61. package/lib/generate-routes.js +12 -8
  62. package/lib/index.js +6 -1
  63. package/lib/make-env.js +1 -1
  64. package/lib/make-hidden-inputs.js +3 -3
  65. package/lib/page/list.js +66 -53
  66. package/lib/page/mountable-with-fields.js +2 -2
  67. package/lib/page/page.js +1 -1
  68. package/lib/page/sealious-list.js +53 -0
  69. package/lib/page/stateful-page.js +9 -9
  70. package/lib/templates/form.js +1 -1
  71. package/lib/templates/list.js +9 -8
  72. package/lib/templates/multiform.js +1 -1
  73. package/lib/templates/page.js +1 -1
  74. package/lib/templates/route-test.js +1 -1
  75. package/lib/templates/stateful-page.js +1 -1
  76. package/lib/test_utils/locator_is_visible.js +1 -1
  77. package/lib/utils/exec.js +1 -1
  78. package/lib/utils/import-path.js +2 -2
  79. package/lib/utils/input-wrapper.js +1 -1
  80. package/lib/utils/list-collections.js +2 -2
  81. package/lib/utils/prettier.js +6 -2
  82. package/lib/utils/question.js +1 -1
  83. package/lib/utils/render-attributes.js +1 -1
  84. package/lib/utils/utils.js +5 -1
  85. package/lib/utils/walk.js +1 -1
  86. package/package.json +2 -2
  87. package/src/index.ts +1 -0
  88. package/src/page/list.ts +70 -67
  89. package/src/page/sealious-list.ts +76 -0
  90. package/src/templates/list.ts +7 -6
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SealiousItemListPage = void 0;
4
+ const list_1 = require("./list");
5
+ const access_control_1 = require("./access-control");
6
+ class SealiousItemListPage extends list_1.ListPage {
7
+ constructor(collection) {
8
+ super();
9
+ this.collection = collection;
10
+ this.canAccess = (0, access_control_1.peopleWhoCan)("list", this.collection);
11
+ }
12
+ async getContextForList(ctx) {
13
+ return ctx.$context;
14
+ }
15
+ async getTotalPages(ctx, itemsPerPage) {
16
+ const query = this.collection.list(await this.getContextForList(ctx));
17
+ await this.addFilter(ctx, query);
18
+ const { items } = await query.fetch();
19
+ return Math.ceil(items.length / itemsPerPage);
20
+ }
21
+ async addFilter(ctx, list) {
22
+ const filters = await this.getFilterValues(ctx);
23
+ for (const key in filters) {
24
+ if (filters[key] === undefined || filters[key] == "") {
25
+ delete filters[key];
26
+ }
27
+ }
28
+ if (Object.keys(filters).length) {
29
+ list.filter(filters);
30
+ }
31
+ }
32
+ async makeQuery(ctx) {
33
+ const query = this.collection.list(await this.getContextForList(ctx));
34
+ const sort = await this.getSort(ctx);
35
+ if (sort) {
36
+ query.sort({ [sort.field]: sort.order });
37
+ }
38
+ return query;
39
+ }
40
+ async getItems(ctx, page, itemsPerPage) {
41
+ const query = await this.makeQuery(ctx);
42
+ await this.addFilter(ctx, query);
43
+ const { items } = await query
44
+ .paginate({ items: itemsPerPage, page })
45
+ .fetch();
46
+ return items;
47
+ }
48
+ async renderItem(_, item, index) {
49
+ return `<div>${item.id}</div>`;
50
+ }
51
+ }
52
+ exports.SealiousItemListPage = SealiousItemListPage;
53
+ //# sourceMappingURL=sealious-list.js.map
@@ -21,18 +21,18 @@ class StatefulPage extends mountable_1.Mountable {
21
21
  };
22
22
  }
23
23
  wrapInForm(state, content) {
24
- return tempstream_1.tempstream /* HTML */ `<form action="./" method="POST">
24
+ return (0, tempstream_1.tempstream /* HTML */) `<form action="./" method="POST">
25
25
  <input
26
26
  name="state"
27
27
  type="hidden"
28
- value="${base64_1.to_base64(JSON.stringify(state))}"
28
+ value="${(0, base64_1.to_base64)(JSON.stringify(state))}"
29
29
  />
30
30
  ${content}
31
31
  </form>`;
32
32
  }
33
33
  makeActionButton(state, action_description, ...args) {
34
34
  let label, action;
35
- if (ts_predicates_1.is(action_description, ts_predicates_1.predicates.object)) {
35
+ if ((0, ts_predicates_1.is)(action_description, ts_predicates_1.predicates.object)) {
36
36
  action = action_description.action.toString();
37
37
  label = action_description.label || action;
38
38
  }
@@ -44,12 +44,12 @@ class StatefulPage extends mountable_1.Mountable {
44
44
  <input
45
45
  type="submit"
46
46
  value=${label}
47
- formaction="./?action=${action}&args=${base64_1.to_base64(JSON.stringify(args))}"
47
+ formaction="./?action=${action}&args=${(0, base64_1.to_base64)(JSON.stringify(args))}"
48
48
  />
49
49
  `;
50
50
  }
51
51
  extractState(ctx) {
52
- if (!ts_predicates_1.hasShape({
52
+ if (!(0, ts_predicates_1.hasShape)({
53
53
  action: ts_predicates_1.predicates.maybe(ts_predicates_1.predicates.string),
54
54
  state: ts_predicates_1.predicates.string,
55
55
  args: ts_predicates_1.predicates.maybe(ts_predicates_1.predicates.string),
@@ -60,8 +60,8 @@ class StatefulPage extends mountable_1.Mountable {
60
60
  }
61
61
  const inputs = Object.fromEntries(Object.entries(ctx.$body).filter(([key]) => !["action", "state", "args", "$"].includes(key)));
62
62
  // the "$" key is parsed as dot notation and overrides the state
63
- const original_state = JSON.parse(base64_1.from_base64(ctx.$body.state));
64
- const modified_state = deepmerge_1.default(original_state, ctx.$body.$ || {}, {
63
+ const original_state = JSON.parse((0, base64_1.from_base64)(ctx.$body.state));
64
+ const modified_state = (0, deepmerge_1.default)(original_state, ctx.$body.$ || {}, {
65
65
  arrayMerge: (target, source, options) => {
66
66
  // https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
67
67
  const destination = target.slice();
@@ -71,7 +71,7 @@ class StatefulPage extends mountable_1.Mountable {
71
71
  options.cloneUnlessOtherwiseSpecified(item, options);
72
72
  }
73
73
  else if (options.isMergeableObject(item)) {
74
- destination[index] = deepmerge_1.default(target[index], item, options);
74
+ destination[index] = (0, deepmerge_1.default)(target[index], item, options);
75
75
  }
76
76
  else if (target.indexOf(item) === -1) {
77
77
  destination.push(item);
@@ -104,7 +104,7 @@ class StatefulPage extends mountable_1.Mountable {
104
104
  router.post(path, (ctx) => {
105
105
  const { action, state, inputs, action_args } = this.extractState(ctx);
106
106
  if (action) {
107
- ctx.body = this.render(ctx, this.actions[action](state, inputs, ...JSON.parse(base64_1.from_base64(action_args))), inputs);
107
+ ctx.body = this.render(ctx, this.actions[action](state, inputs, ...JSON.parse((0, base64_1.from_base64)(action_args))), inputs);
108
108
  }
109
109
  else {
110
110
  ctx.body = this.render(ctx, state, inputs);
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formTemplate = void 0;
4
4
  const import_path_1 = require("../utils/import-path");
5
5
  async function formTemplate(action_name, newfilefullpath) {
6
- const rel = import_path_1.curryImportPath(newfilefullpath);
6
+ const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
7
7
  return `import { Context } from "koa";
8
8
  import { Form, FormData, FormDataValue, Fields, Controls } from "@sealcode/sealgen";
9
9
  import html from "${rel("src/back/html")}";
@@ -9,14 +9,14 @@ const listTemplate = async (action_name, newfilefullpath) => {
9
9
  type: "autocomplete",
10
10
  name: "collection",
11
11
  message: "Which sealious collection do you want to list?",
12
- choices: (await list_collections_1.listCollections()).map((collection) => ({
12
+ choices: (await (0, list_collections_1.listCollections)()).map((collection) => ({
13
13
  title: collection,
14
14
  value: collection,
15
15
  })),
16
16
  });
17
17
  const collection = response.collection;
18
18
  const uppercase_collection = collection[0].toLocaleUpperCase() + collection.slice(1);
19
- const rel = import_path_1.curryImportPath(newfilefullpath);
19
+ const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
20
20
  return `import { Context } from "koa";
21
21
  import { CollectionItem } from "sealious";
22
22
  import { tempstream } from "tempstream";
@@ -44,12 +44,13 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
44
44
  tempstream/* HTML */ \`<div>
45
45
  <h2>${uppercase_collection} List</h2>
46
46
  <table>
47
- <thead>
48
- <th>id</th>
49
- \${Object.keys(${uppercase_collection}.fields).map(
50
- (fieldname) => \`<th>\${fieldname}</th>\`
51
- )}
52
- </thead>
47
+ \${this.renderTableHead(ctx, [
48
+ { field: "id" },
49
+ ...Object.keys(Patrons.fields).map((field) => ({
50
+ field,
51
+ label: field,
52
+ })),
53
+ ])}
53
54
  <tbody>
54
55
  \${super.render(ctx)}
55
56
  </tbody>
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.multiformTemplate = void 0;
4
4
  const utils_1 = require("../utils/utils");
5
5
  async function multiformTemplate(action_name, newfilefullpath) {
6
- const rel = utils_1.curryImportPath(newfilefullpath);
6
+ const rel = (0, utils_1.curryImportPath)(newfilefullpath);
7
7
  return `import { Context } from "koa";
8
8
  import html from "${rel("src/back/html")}";
9
9
  import { Controls, Fields, Form, FormMessage, Multiform } from "@sealcode/sealgen";
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.pageTemplate = void 0;
4
4
  const import_path_1 = require("../utils/import-path");
5
5
  async function pageTemplate(action_name, newfilefullpath) {
6
- const rel = import_path_1.curryImportPath(newfilefullpath);
6
+ const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
7
7
  return `import { Context } from "koa";
8
8
  import { tempstream } from "tempstream";
9
9
  import { Page } from "@sealcode/sealgen";
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.testTemplate = void 0;
4
4
  const import_path_1 = require("../utils/import-path");
5
5
  function testTemplate(action_name, new_file_fullpath) {
6
- const rel = import_path_1.curryImportPath(new_file_fullpath);
6
+ const rel = (0, import_path_1.curryImportPath)(new_file_fullpath);
7
7
  return `import { withProdApp } from "${rel("src/back/test_utils/with-prod-app")}";
8
8
  import { LONG_TEST_TIMEOUT, webhintURL } from "${rel("src/back/test_utils/webhint")}";
9
9
  import { ${action_name}URL } from "${rel("src/back/routes/urls")}";
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.statefulPageTemplate = void 0;
4
4
  const import_path_1 = require("../utils/import-path");
5
5
  async function statefulPageTemplate(action_name, newfilefullpath) {
6
- const rel = import_path_1.curryImportPath(newfilefullpath);
6
+ const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
7
7
  return `import { BaseContext } from "koa";
8
8
  import { tempstream, Templatable } from "tempstream";
9
9
  import { StatefulPage } from "@sealcode/sealgen";
@@ -5,7 +5,7 @@ const sleep_1 = require("../utils/sleep");
5
5
  async function locator_is_visible(locator, timeout = 500) {
6
6
  let exists = false;
7
7
  while (timeout > 0 && !exists) {
8
- await sleep_1.sleep(50);
8
+ await (0, sleep_1.sleep)(50);
9
9
  timeout -= 50;
10
10
  exists = await locator.isVisible();
11
11
  }
package/lib/utils/exec.js CHANGED
@@ -7,7 +7,7 @@ async function exec(program, args) {
7
7
  return new Promise((resolve, reject) => {
8
8
  let stdout = "";
9
9
  let stderr = "";
10
- const process = child_process_1.spawn(program, args);
10
+ const process = (0, child_process_1.spawn)(program, args);
11
11
  process.stdout.on("data", (data) => (stdout += data.toString()));
12
12
  process.stderr.on("data", (data) => (stderr += data.toString()));
13
13
  process.on("close", (exitCode) => {
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.curryImportPath = exports.importPath = void 0;
7
7
  const path_1 = require("path");
8
8
  const locreq_1 = __importDefault(require("locreq"));
9
- const target_locreq = locreq_1.default(process.cwd());
9
+ const target_locreq = (0, locreq_1.default)(process.cwd());
10
10
  function importPath(project_module_path, project_target_path) {
11
- let result = path_1.relative(path_1.dirname(target_locreq.resolve(project_module_path)), target_locreq.resolve(project_target_path).replace(/.ts$/, ""));
11
+ let result = (0, path_1.relative)((0, path_1.dirname)(target_locreq.resolve(project_module_path)), target_locreq.resolve(project_target_path).replace(/.ts$/, ""));
12
12
  if (result[0] != ".") {
13
13
  result = "./" + result;
14
14
  }
@@ -10,7 +10,7 @@ function getModifierClasses(modifiers) {
10
10
  return modifiers.map((m) => "form-input__wrapper--" + m).join(" ");
11
11
  }
12
12
  function inputWrapper(modifiers, input) {
13
- return tempstream_1.tempstream /* HTML */ `<div
13
+ return (0, tempstream_1.tempstream /* HTML */) `<div
14
14
  class="form-input form-input__wrapper ${getModifierClasses(modifiers)}"
15
15
  >
16
16
  ${input}
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.listCollections = void 0;
7
7
  const locreq_1 = __importDefault(require("locreq"));
8
8
  const walk_1 = require("./walk");
9
- const target_locreq = locreq_1.default(process.cwd());
9
+ const target_locreq = (0, locreq_1.default)(process.cwd());
10
10
  async function listCollections() {
11
- const files = await walk_1.walkDir(target_locreq.resolve("src/back/collections"));
11
+ const files = await (0, walk_1.walkDir)(target_locreq.resolve("src/back/collections"));
12
12
  return files
13
13
  .filter((f) => f.endsWith(".ts") &&
14
14
  !f.endsWith(".test.ts") &&
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -25,7 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
25
29
  exports.getPrettierConfig = void 0;
26
30
  const prettier = __importStar(require("prettier"));
27
31
  const locreq_1 = __importDefault(require("locreq"));
28
- const target_locreq = locreq_1.default(process.cwd());
32
+ const target_locreq = (0, locreq_1.default)(process.cwd());
29
33
  async function getPrettierConfig() {
30
34
  return {
31
35
  ...(await prettier.resolveConfig(target_locreq.resolve("src/back/routes"))),
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.question = void 0;
4
4
  const readline_1 = require("readline");
5
5
  async function question(text, validator) {
6
- const rl = readline_1.createInterface({
6
+ const rl = (0, readline_1.createInterface)({
7
7
  input: process.stdin,
8
8
  output: process.stdout,
9
9
  });
@@ -14,7 +14,7 @@ function renderAttributes(attrs) {
14
14
  }
15
15
  }
16
16
  else {
17
- return `${key}="${sanitize_1.attribute(value)}"`;
17
+ return `${key}="${(0, sanitize_1.attribute)(value)}"`;
18
18
  }
19
19
  })
20
20
  .join(" ");
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
package/lib/utils/walk.js CHANGED
@@ -10,7 +10,7 @@ async function walkDir(dir) {
10
10
  if (entry.includes(".#")) {
11
11
  continue;
12
12
  }
13
- const fullpath = path_1.resolve(dir, entry);
13
+ const fullpath = (0, path_1.resolve)(dir, entry);
14
14
  // eslint-disable-next-line no-await-in-loop
15
15
  const info = await fs_1.promises.stat(fullpath);
16
16
  if (info.isDirectory()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.8.52",
3
+ "version": "0.8.53",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -40,7 +40,7 @@
40
40
  "playwright": "^1.29.2",
41
41
  "source-map-support": "^0.5.21",
42
42
  "ts-node": "^10.9.1",
43
- "typescript": "^4.3.5",
43
+ "typescript": "^4.4",
44
44
  "wait-port": "^1.0.4"
45
45
  },
46
46
  "types": "./@types/index.d.ts",
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./forms/form";
18
18
  export * from "./page/props-parser";
19
19
  export * from "./utils/utils";
20
20
  export * from "./page/list";
21
+ export * from "./page/sealious-list";
21
22
  export * from "./utils/translate";
22
23
  export * as sanitize from "./sanitize";
23
24
  export * from "./forms/multiform";
package/src/page/list.ts CHANGED
@@ -1,21 +1,13 @@
1
1
  import { Context } from "koa";
2
- import {
3
- Collection,
4
- CollectionItem,
5
- ItemList,
6
- Context as SealiousContext,
7
- CollectionInput,
8
- } from "sealious";
9
-
10
2
  import { Templatable, tempstream, FlatTemplatable } from "tempstream";
11
- import { peopleWhoCan } from "./access-control";
12
3
  import { Page } from "./page";
13
4
  import { ShapeToType } from "@sealcode/ts-predicates";
14
5
  import { FormControl } from "../forms/controls/controls";
15
- import { FormField, NumberField } from "../forms/fields/field";
6
+ import { FormField, NumberField, SimpleFormField } from "../forms/fields/field";
16
7
  import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
17
8
  import { FormDataValue } from "..";
18
9
  import { makeHiddenInputs } from "../make-hidden-inputs";
10
+ import qs from "qs";
19
11
 
20
12
  export const BasePagePropsShape = <const>{};
21
13
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
@@ -23,8 +15,33 @@ export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
23
15
  export const BaseListPageFields = <const>{
24
16
  page: new NumberField(false, 1),
25
17
  itemsPerPage: new NumberField(false, 12),
18
+ sort: new SimpleFormField(false),
26
19
  };
27
20
 
21
+ const SORT_SEPARATOR = ":";
22
+ function decodeSort(
23
+ s: unknown
24
+ ): { field: string; order: "asc" | "desc" } | null {
25
+ if (typeof s !== "string" || !s.includes(SORT_SEPARATOR)) {
26
+ return null;
27
+ }
28
+ let order = s.split(SORT_SEPARATOR)[1];
29
+ const field = s.split(SORT_SEPARATOR)[0];
30
+ if (order !== "asc" && order !== "desc") {
31
+ order = "asc";
32
+ return {
33
+ field,
34
+ order: order as "asc",
35
+ };
36
+ } else {
37
+ return { field, order };
38
+ }
39
+ }
40
+
41
+ function encodeSort(field: string, order: "asc" | "desc") {
42
+ return [field, order].join(SORT_SEPARATOR);
43
+ }
44
+
28
45
  export abstract class ListPage<
29
46
  ItemType,
30
47
  F extends typeof BaseListPageFields
@@ -158,6 +175,21 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
158
175
  return filter;
159
176
  }
160
177
 
178
+ async getSort(
179
+ ctx: Context
180
+ ): Promise<{ field: string; order: "asc" | "desc" } | null> {
181
+ const { sort } = await this.extractRawValues(ctx);
182
+ return decodeSort(sort);
183
+ }
184
+
185
+ makeSortLink(ctx: Context, field: string, order: "asc" | "desc"): string {
186
+ const url = new URL(ctx.url, "https://example.com");
187
+ const params = qs.parse(url.search.slice(1));
188
+ params.sort = encodeSort(field, order);
189
+ url.search = qs.stringify(params);
190
+ return url.pathname + url.search;
191
+ }
192
+
161
193
  async renderFilters(ctx: Context): Promise<FlatTemplatable> {
162
194
  const values = await this.extractRawValues(ctx);
163
195
  return tempstream/* HTML */ `<form method="GET">
@@ -177,64 +209,35 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
177
209
  })}
178
210
  </form>`;
179
211
  }
180
- }
181
-
182
- export abstract class SealiousItemListPage<
183
- C extends Collection,
184
- F extends typeof BaseListPageFields
185
- > extends ListPage<CollectionItem<C>, F> {
186
- constructor(public collection: C) {
187
- super();
188
- }
189
-
190
- async getContextForList(ctx: Context): Promise<SealiousContext> {
191
- return ctx.$context;
192
- }
193
212
 
194
- async getTotalPages(ctx: Context, itemsPerPage: number): Promise<number> {
195
- const query = this.collection.list(await this.getContextForList(ctx));
196
- await this.addFilter(ctx, query);
197
- const { items } = await query.fetch();
198
- return Math.ceil(items.length / itemsPerPage);
199
- }
200
-
201
- protected async addFilter(ctx: Context, list: ItemList<C>): Promise<void> {
202
- const filters = await this.getFilterValues(ctx);
203
-
204
- for (const key in filters) {
205
- if (filters[key] === undefined || filters[key] == "") {
206
- delete filters[key];
207
- }
208
- }
209
- if (Object.keys(filters).length) {
210
- list.filter(filters as Partial<CollectionInput<C>>);
211
- }
212
- }
213
-
214
- async makeQuery(ctx: Context): Promise<ItemList<C>> {
215
- return this.collection.list(await this.getContextForList(ctx));
216
- }
217
-
218
- async getItems(
213
+ renderHeading(ctx: Context, field: string, label = field): FlatTemplatable {
214
+ const current_sort = decodeSort(
215
+ qs.parse(
216
+ new URL(ctx.url, "https://www.example.com").search.slice(1)
217
+ )["sort"]
218
+ );
219
+ const current_order =
220
+ current_sort?.field == field ? current_sort.order : null;
221
+ const order = current_order == "desc" ? "asc" : "desc";
222
+ return /* HTML */ `<th>
223
+ <a href="${this.makeSortLink(ctx, field, order)}"
224
+ >${label}
225
+ ${(current_order && (current_order == "asc" ? "↑" : "↓")) ||
226
+ ""}</a
227
+ >
228
+ </th>`;
229
+ }
230
+
231
+ renderTableHead(
219
232
  ctx: Context,
220
- page: number,
221
- itemsPerPage: number
222
- ): Promise<CollectionItem<C>[]> {
223
- const query = await this.makeQuery(ctx);
224
- await this.addFilter(ctx, query);
225
- const { items } = await query
226
- .paginate({ items: itemsPerPage, page })
227
- .fetch();
228
- return items;
229
- }
230
-
231
- async renderItem(
232
- _: Context,
233
- item: CollectionItem<C>,
234
- index: number
235
- ): Promise<FlatTemplatable> {
236
- return `<div>${item.id}</div>`;
233
+ fields: { field: string; label?: string }[]
234
+ ): FlatTemplatable {
235
+ return tempstream/* HTML */ `<thead>
236
+ <tr>
237
+ ${fields.map(({ label, field }) =>
238
+ this.renderHeading(ctx, field, label)
239
+ )}
240
+ </tr>
241
+ </thead>`;
237
242
  }
238
-
239
- canAccess = peopleWhoCan("list", this.collection);
240
243
  }
@@ -0,0 +1,76 @@
1
+ import { Context } from "koa";
2
+ import { BaseListPageFields, ListPage } from "./list";
3
+ import {
4
+ Collection,
5
+ CollectionItem,
6
+ ItemList,
7
+ Context as SealiousContext,
8
+ CollectionInput,
9
+ } from "sealious";
10
+ import { FlatTemplatable } from "tempstream";
11
+ import { peopleWhoCan } from "./access-control";
12
+
13
+ export abstract class SealiousItemListPage<
14
+ C extends Collection,
15
+ F extends typeof BaseListPageFields
16
+ > extends ListPage<CollectionItem<C>, F> {
17
+ constructor(public collection: C) {
18
+ super();
19
+ }
20
+
21
+ async getContextForList(ctx: Context): Promise<SealiousContext> {
22
+ return ctx.$context;
23
+ }
24
+
25
+ async getTotalPages(ctx: Context, itemsPerPage: number): Promise<number> {
26
+ const query = this.collection.list(await this.getContextForList(ctx));
27
+ await this.addFilter(ctx, query);
28
+ const { items } = await query.fetch();
29
+ return Math.ceil(items.length / itemsPerPage);
30
+ }
31
+
32
+ protected async addFilter(ctx: Context, list: ItemList<C>): Promise<void> {
33
+ const filters = await this.getFilterValues(ctx);
34
+
35
+ for (const key in filters) {
36
+ if (filters[key] === undefined || filters[key] == "") {
37
+ delete filters[key];
38
+ }
39
+ }
40
+ if (Object.keys(filters).length) {
41
+ list.filter(filters as Partial<CollectionInput<C>>);
42
+ }
43
+ }
44
+
45
+ async makeQuery(ctx: Context): Promise<ItemList<C>> {
46
+ const query = this.collection.list(await this.getContextForList(ctx));
47
+ const sort = await this.getSort(ctx);
48
+ if (sort) {
49
+ query.sort({ [sort.field]: sort.order } as any);
50
+ }
51
+ return query;
52
+ }
53
+
54
+ async getItems(
55
+ ctx: Context,
56
+ page: number,
57
+ itemsPerPage: number
58
+ ): Promise<CollectionItem<C>[]> {
59
+ const query = await this.makeQuery(ctx);
60
+ await this.addFilter(ctx, query);
61
+ const { items } = await query
62
+ .paginate({ items: itemsPerPage, page })
63
+ .fetch();
64
+ return items;
65
+ }
66
+
67
+ async renderItem(
68
+ _: Context,
69
+ item: CollectionItem<C>,
70
+ index: number
71
+ ): Promise<FlatTemplatable> {
72
+ return `<div>${item.id}</div>`;
73
+ }
74
+
75
+ canAccess = peopleWhoCan("list", this.collection);
76
+ }
@@ -50,12 +50,13 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
50
50
  tempstream/* HTML */ \`<div>
51
51
  <h2>${uppercase_collection} List</h2>
52
52
  <table>
53
- <thead>
54
- <th>id</th>
55
- \${Object.keys(${uppercase_collection}.fields).map(
56
- (fieldname) => \`<th>\${fieldname}</th>\`
57
- )}
58
- </thead>
53
+ \${this.renderTableHead(ctx, [
54
+ { field: "id" },
55
+ ...Object.keys(Patrons.fields).map((field) => ({
56
+ field,
57
+ label: field,
58
+ })),
59
+ ])}
59
60
  <tbody>
60
61
  \${super.render(ctx)}
61
62
  </tbody>