@sealcode/sealgen 0.15.29 → 0.15.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.xunit +54 -43
- package/@types/src/add-list-route.test.d.ts +1 -0
- package/@types/src/forms/controls/controls.d.ts +1 -0
- package/@types/src/forms/controls/single-reference.d.ts +13 -0
- package/@types/src/forms/controls/single-reference.test.d.ts +1 -0
- package/@types/src/page/default-list-filters.d.ts +14 -3
- package/@types/src/page/list.d.ts +1 -1
- package/@types/src/templates/collection.d.ts +1 -1
- package/@types/src/templates/form.d.ts +8 -1
- package/@types/src/test_utils/test-on-real-app.d.ts +4 -1
- package/lib/add-collection.js +5 -1
- package/lib/add-list-route.test.js +38 -0
- package/lib/add-route.test.js +1 -1
- package/lib/forms/controls/controls.js +1 -0
- package/lib/forms/controls/single-reference.js +26 -0
- package/lib/forms/controls/single-reference.test.js +51 -0
- package/lib/page/default-list-filters.js +40 -33
- package/lib/page/list.js +6 -6
- package/lib/page/sealious-list.js +5 -0
- package/lib/page/sealious-list.test.js +1 -1
- package/lib/templates/collection.js +4 -3
- package/lib/templates/form.js +32 -22
- package/lib/templates/list.js +31 -12
- package/lib/test_utils/test-on-real-app.js +33 -6
- package/package.json +2 -1
- package/src/add-collection.ts +5 -1
- package/src/add-list-route.test.ts +45 -0
- package/src/add-route.test.ts +1 -1
- package/src/forms/controls/controls.ts +1 -0
- package/src/forms/controls/single-reference.test.ts +62 -0
- package/src/forms/controls/single-reference.ts +36 -0
- package/src/page/default-list-filters.ts +45 -33
- package/src/page/list.ts +8 -8
- package/src/page/sealious-list.test.ts +1 -1
- package/src/page/sealious-list.ts +5 -0
- package/src/templates/collection.ts +8 -3
- package/src/templates/form.ts +34 -22
- package/src/templates/list.ts +31 -12
- package/src/test_utils/test-on-real-app.ts +35 -3
package/lib/templates/form.js
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import Prettier from "prettier";
|
|
2
2
|
import { curryImportPath } from "../utils/import-path.js";
|
|
3
3
|
import { getPrettierConfig } from "../utils/prettier.js";
|
|
4
|
-
|
|
4
|
+
const defaults = {
|
|
5
|
+
postimport: ``,
|
|
6
|
+
fields: `
|
|
7
|
+
email: new Fields.CollectionField(true, Users.fields.email),
|
|
8
|
+
password: new Fields.SimpleFormField(true),`,
|
|
9
|
+
controls: `
|
|
10
|
+
new Controls.SimpleInput(fields.email, { label: "Email:", type: "email" }),
|
|
11
|
+
new Controls.SimpleInput(fields.password, {
|
|
12
|
+
label: "Password:",
|
|
13
|
+
type: "password",
|
|
14
|
+
}),`,
|
|
15
|
+
validate_method: `async validateValues(
|
|
16
|
+
ctx: Context,
|
|
17
|
+
data: Record<string, FormDataValue>
|
|
18
|
+
): Promise<{ valid: boolean; error: string }> {
|
|
19
|
+
// just a silly example, please remove this with better logic
|
|
20
|
+
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
|
21
|
+
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
|
+
if ((password || "").length > ((email as string) || "").length) {
|
|
24
|
+
return { valid: false, error: "Password cannot be longer than email" };
|
|
25
|
+
} else {
|
|
26
|
+
return { valid: true, error: "" };
|
|
27
|
+
}
|
|
28
|
+
}`
|
|
29
|
+
};
|
|
30
|
+
async function formTemplate(action_name, newfilefullpath, _params = defaults) {
|
|
31
|
+
const params = { ...defaults, ..._params };
|
|
5
32
|
const rel = curryImportPath(newfilefullpath);
|
|
6
33
|
const content = `import type { Context } from "koa";
|
|
7
34
|
import type { FormData, FormDataValue } from "@sealcode/sealgen";
|
|
@@ -9,11 +36,12 @@ import { Form, Fields, Controls, fieldsToShape } from "@sealcode/sealgen";
|
|
|
9
36
|
import html from "${rel("src/back/html.js")}";
|
|
10
37
|
import { Users } from "${rel("src/back/collections/collections.js")}";
|
|
11
38
|
|
|
39
|
+
${params.postimport}
|
|
40
|
+
|
|
12
41
|
export const actionName = "${action_name}";
|
|
13
42
|
|
|
14
43
|
const fields = {
|
|
15
|
-
|
|
16
|
-
password: new Fields.SimpleFormField(true),
|
|
44
|
+
${params.fields}
|
|
17
45
|
};
|
|
18
46
|
|
|
19
47
|
export const ${action_name}Shape = fieldsToShape(fields);
|
|
@@ -23,27 +51,9 @@ export default new (class ${action_name}Form extends Form<typeof fields, void> {
|
|
|
23
51
|
fields = fields;
|
|
24
52
|
|
|
25
53
|
controls = [
|
|
26
|
-
|
|
27
|
-
new Controls.SimpleInput(fields.password, {
|
|
28
|
-
label: "Password:",
|
|
29
|
-
type: "password",
|
|
30
|
-
}),
|
|
54
|
+
${params.controls}
|
|
31
55
|
];
|
|
32
56
|
|
|
33
|
-
async validateValues(
|
|
34
|
-
ctx: Context,
|
|
35
|
-
data: Record<string, FormDataValue>
|
|
36
|
-
): Promise<{ valid: boolean; error: string }> {
|
|
37
|
-
// just a silly example, please remove this with better logic
|
|
38
|
-
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
|
39
|
-
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
41
|
-
if ((password || "").length > ((email as string) || "").length) {
|
|
42
|
-
return { valid: false, error: "Password cannot be longer than email" };
|
|
43
|
-
} else {
|
|
44
|
-
return { valid: true, error: "" };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
57
|
|
|
48
58
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
59
|
async canAccess(_: Context) {
|
package/lib/templates/list.js
CHANGED
|
@@ -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}",
|
|
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
|
}`;
|
|
@@ -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
|
|
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
|
-
|
|
118
|
-
|
|
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}>) {
|
|
@@ -142,6 +155,15 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
|
|
|
142
155
|
return <td>{format ? format(value) : 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
|
-
|
|
154
|
-
{this.renderTableHead(ctx, displayFields)}
|
|
155
|
-
<tbody>{super.render(ctx)}</tbody>
|
|
156
|
-
</table>
|
|
175
|
+
{super.render(ctx)}
|
|
157
176
|
</div>
|
|
158
177
|
);
|
|
159
178
|
}
|
|
@@ -3,6 +3,8 @@ import { spawn } from "child_process";
|
|
|
3
3
|
import getPort from "get-port";
|
|
4
4
|
import waitPort from "wait-port";
|
|
5
5
|
import { promises as fs } from "fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import chalk from "chalk";
|
|
6
8
|
import _locreq from "locreq";
|
|
7
9
|
import { module_dirname } from "../utils/module-dirname.js";
|
|
8
10
|
import EventEmitter from "events";
|
|
@@ -30,6 +32,7 @@ class RealAppTest {
|
|
|
30
32
|
console.log(
|
|
31
33
|
" starting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
|
|
32
34
|
);
|
|
35
|
+
await test_app.runSealgenCommand("build");
|
|
33
36
|
test_app.app_port = await getPort();
|
|
34
37
|
return test_app;
|
|
35
38
|
}
|
|
@@ -69,6 +72,11 @@ class RealAppTest {
|
|
|
69
72
|
output: "silent",
|
|
70
73
|
timeout: 6e3
|
|
71
74
|
});
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.yellow(
|
|
77
|
+
`App is listening on http://localhost:${this.app_port}`
|
|
78
|
+
)
|
|
79
|
+
);
|
|
72
80
|
}
|
|
73
81
|
async close() {
|
|
74
82
|
try {
|
|
@@ -88,6 +96,10 @@ class RealAppTest {
|
|
|
88
96
|
await execAsync(
|
|
89
97
|
`cd ${this.app_path} && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
|
|
90
98
|
);
|
|
99
|
+
await this.build();
|
|
100
|
+
}
|
|
101
|
+
async build() {
|
|
102
|
+
await execAsync(`cd ${this.app_path} && ${callSealgenBin()} build`);
|
|
91
103
|
}
|
|
92
104
|
async throwIfItKillsTheApp(cb) {
|
|
93
105
|
let finished = false;
|
|
@@ -114,18 +126,19 @@ class RealAppTest {
|
|
|
114
126
|
this.emitter.removeListener("process_error", error_handler);
|
|
115
127
|
});
|
|
116
128
|
}
|
|
117
|
-
async
|
|
129
|
+
async httpGET(path2) {
|
|
118
130
|
return this.throwIfItKillsTheApp(async () => {
|
|
119
|
-
const ret = await fetch(`http://localhost:${this.app_port}${
|
|
131
|
+
const ret = await fetch(`http://localhost:${this.app_port}${path2}`);
|
|
120
132
|
const status = ret.status;
|
|
121
133
|
const response = await ret.text();
|
|
122
134
|
return { status, response };
|
|
123
135
|
});
|
|
124
136
|
}
|
|
125
|
-
async httpPOST(
|
|
137
|
+
async httpPOST(path2, body) {
|
|
138
|
+
console.debug("POST", path2, body);
|
|
126
139
|
return this.throwIfItKillsTheApp(async () => {
|
|
127
140
|
const ret = await fetch(
|
|
128
|
-
`http://localhost:${this.app_port}${
|
|
141
|
+
`http://localhost:${this.app_port}${path2}`,
|
|
129
142
|
{
|
|
130
143
|
method: "POST",
|
|
131
144
|
headers: { "Content-Type": "application/json" },
|
|
@@ -134,11 +147,25 @@ class RealAppTest {
|
|
|
134
147
|
);
|
|
135
148
|
const status = ret.status;
|
|
136
149
|
const response = await ret.text();
|
|
137
|
-
console.
|
|
138
|
-
console.
|
|
150
|
+
console.debug("request done!");
|
|
151
|
+
console.debug({ response });
|
|
139
152
|
return { status, response };
|
|
140
153
|
});
|
|
141
154
|
}
|
|
155
|
+
async addFile(target_path, content) {
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.yellow("Inserting file at path"),
|
|
158
|
+
chalk.green(target_path),
|
|
159
|
+
"with content\n",
|
|
160
|
+
content,
|
|
161
|
+
"\n\n\n\n=====\n"
|
|
162
|
+
);
|
|
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}`;
|
|
168
|
+
}
|
|
142
169
|
}
|
|
143
170
|
export {
|
|
144
171
|
RealAppTest,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealcode/sealgen",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.31",
|
|
4
4
|
"description": "Module to automate adding routes and collections to a sealious application",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
|
41
41
|
"@typescript-eslint/parser": "^5.58.0",
|
|
42
42
|
"axios": "^1.6.7",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
43
44
|
"eslint": "^7.32.0",
|
|
44
45
|
"eslint-config-prettier": "^8.3.0",
|
|
45
46
|
"eslint-plugin-prettier": "^3.4.1",
|
package/src/add-collection.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { generateCollections } from "./generate-collections.js";
|
|
|
6
6
|
import { toKebabCase, toPascalCase } from "js-convert-case";
|
|
7
7
|
const target_locreq = _locreq(process.cwd());
|
|
8
8
|
import prompts from "prompts";
|
|
9
|
+
import { formatWithPrettier } from "./utils/prettier.js";
|
|
9
10
|
|
|
10
11
|
export async function addCollection(
|
|
11
12
|
params: Record<string, string | boolean> = {}
|
|
@@ -40,7 +41,10 @@ export async function addCollection(
|
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
await fs.mkdir(resolve(file_path, "../"), { recursive: true });
|
|
43
|
-
await fs.writeFile(
|
|
44
|
+
await fs.writeFile(
|
|
45
|
+
file_path,
|
|
46
|
+
await formatWithPrettier(collectionTemplate(collection_name))
|
|
47
|
+
);
|
|
44
48
|
// eslint-disable-next-line no-console
|
|
45
49
|
console.log(`${file_path} created`);
|
|
46
50
|
await generateCollections();
|
|
@@ -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
|
+
});
|
package/src/add-route.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ describe("add route test", () => {
|
|
|
7
7
|
const path = "/sample";
|
|
8
8
|
await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
|
|
9
9
|
await test_app.start();
|
|
10
|
-
const res = await test_app.
|
|
10
|
+
const res = await test_app.httpGET(path);
|
|
11
11
|
if (res.status != 200) {
|
|
12
12
|
throw new Error("url generated by add-route exists but doesn't return 200");
|
|
13
13
|
}
|
|
@@ -4,6 +4,7 @@ export { EditableCollectionSubset } from "./editable-collection-subset.js";
|
|
|
4
4
|
export { Frame } from "./frame.js";
|
|
5
5
|
export { CheckboxedListInput } from "./checkboxed-list-input.js";
|
|
6
6
|
export * from "./dropdown.js";
|
|
7
|
+
export * from "./single-reference.js";
|
|
7
8
|
export { SimpleInput } from "./simple-input.js";
|
|
8
9
|
export { FormHeader } from "./form-header.js";
|
|
9
10
|
export { FormParagraph } from "./form-paragraph.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Page } from "playwright";
|
|
2
|
+
import { collectionTemplate } from "../../templates/collection.js";
|
|
3
|
+
import { formTemplate } from "../../templates/form.js";
|
|
4
|
+
import { RealAppTest } from "../../test_utils/test-on-real-app.js";
|
|
5
|
+
import { getBrowser } from "../../utils/browser-creator.js";
|
|
6
|
+
import { expect as PlaywritghtExpect } from "@playwright/test";
|
|
7
|
+
|
|
8
|
+
describe("single-reference.test", () => {
|
|
9
|
+
let page: Page;
|
|
10
|
+
before(async () => {
|
|
11
|
+
const browser = await getBrowser();
|
|
12
|
+
const context = await browser.newContext();
|
|
13
|
+
page = await context.newPage();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
after(async () => {
|
|
17
|
+
await page.close();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("creates minimal sealious app and adds a simple route that works", async function () {
|
|
21
|
+
const test_app = await RealAppTest.init();
|
|
22
|
+
|
|
23
|
+
// add possiblity to modify templates (form and collection) and create test case
|
|
24
|
+
|
|
25
|
+
await test_app.addFile(
|
|
26
|
+
"src/back/collections/articles.ts",
|
|
27
|
+
collectionTemplate("articles", `title: new FieldTypes.Text(),`)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await test_app.addFile(
|
|
31
|
+
"src/back/collections/comments.ts",
|
|
32
|
+
collectionTemplate(
|
|
33
|
+
"comments",
|
|
34
|
+
`text: new FieldTypes.Text(),
|
|
35
|
+
article: new FieldTypes.SingleReference("articles"),`
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const form_file_path = "src/back/routes/some-form.form.ts";
|
|
40
|
+
await test_app.addFile(
|
|
41
|
+
form_file_path,
|
|
42
|
+
await formTemplate("SomeForm", form_file_path, {
|
|
43
|
+
postimport: `import {Comments} from "../collections/collections.js";`,
|
|
44
|
+
fields: `article: new Fields.CollectionField(true, Comments.fields.article),`,
|
|
45
|
+
controls: `new Controls.SingleReferenceDropdown(fields.article, {getLabel: item=>item.get("title")}),`,
|
|
46
|
+
validate_method: "",
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await test_app.start();
|
|
51
|
+
await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 1" });
|
|
52
|
+
await test_app.httpPOST(`/api/v1/collections/articles`, { title: "article 2" });
|
|
53
|
+
|
|
54
|
+
await page.goto(`http://localhost:${test_app.app_port}/some-form/`);
|
|
55
|
+
await page.locator("html").click();
|
|
56
|
+
await PlaywritghtExpect(page.getByLabel("article")).toBeVisible();
|
|
57
|
+
await page.getByLabel("article").selectOption({ label: "article 2" });
|
|
58
|
+
await page.getByRole("button", { name: "Wyślij" }).click();
|
|
59
|
+
|
|
60
|
+
await test_app.close();
|
|
61
|
+
}).timeout(100 * 1000);
|
|
62
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import SealiousSingleReference from "sealious/@types/src/app/base-chips/field-types/single-reference.js";
|
|
2
|
+
import { CollectionField } from "../fields/collection-field.js";
|
|
3
|
+
import { DropdownOptions, FreeformDropdown } from "./dropdown.js";
|
|
4
|
+
import { Collection, CollectionItem } from "sealious";
|
|
5
|
+
|
|
6
|
+
export class SingleReferenceDropdown<
|
|
7
|
+
F extends SealiousSingleReference,
|
|
8
|
+
Required extends boolean
|
|
9
|
+
> extends FreeformDropdown {
|
|
10
|
+
constructor(
|
|
11
|
+
public field: CollectionField<F, Required>,
|
|
12
|
+
public options: DropdownOptions & {
|
|
13
|
+
getLabel?: (item: CollectionItem<Collection>) => string;
|
|
14
|
+
} = {
|
|
15
|
+
label: field.name,
|
|
16
|
+
autosubmit: false,
|
|
17
|
+
autocomplete: true,
|
|
18
|
+
}
|
|
19
|
+
) {
|
|
20
|
+
super(
|
|
21
|
+
field,
|
|
22
|
+
async (ctx) => {
|
|
23
|
+
const { items } = await ctx.$app.collections[
|
|
24
|
+
field.sealiousField.target_collection
|
|
25
|
+
]
|
|
26
|
+
.list(ctx.$context)
|
|
27
|
+
.fetch();
|
|
28
|
+
return items.map((item) => ({
|
|
29
|
+
value: item.id,
|
|
30
|
+
label: options.getLabel ? options.getLabel(item) : item.id,
|
|
31
|
+
}));
|
|
32
|
+
},
|
|
33
|
+
options
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -6,44 +6,56 @@ export type ListFilterRender = (
|
|
|
6
6
|
field: Field<any>
|
|
7
7
|
) => FlatTemplatable;
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
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
|
}
|
|
@@ -15,7 +15,7 @@ describe("sealious-list test", () => {
|
|
|
15
15
|
content: "some-content",
|
|
16
16
|
});
|
|
17
17
|
assert(res_post.status == 201);
|
|
18
|
-
const res = await test_app.
|
|
18
|
+
const res = await test_app.httpGET(url);
|
|
19
19
|
if (res.status != 200) {
|
|
20
20
|
throw new Error("url generated by add-route exists but doesn't return 200");
|
|
21
21
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { toPascalCase } from "js-convert-case";
|
|
2
|
+
|
|
3
|
+
export function collectionTemplate(
|
|
4
|
+
collection_name: string,
|
|
5
|
+
fields_string = `content: new FieldTypes.Text(),`
|
|
6
|
+
) {
|
|
2
7
|
return `import { Collection, FieldTypes, Policies } from "sealious";
|
|
3
8
|
|
|
4
|
-
export default class ${collection_name} extends Collection {
|
|
9
|
+
export default class ${toPascalCase(collection_name)} extends Collection {
|
|
5
10
|
fields = {
|
|
6
|
-
|
|
11
|
+
${fields_string}
|
|
7
12
|
};
|
|
8
13
|
defaultPolicy = new Policies.Public();
|
|
9
14
|
}
|