@sealcode/sealgen 0.15.30 → 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/@types/src/add-list-route.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/test_utils/test-on-real-app.d.ts +2 -0
- package/lib/add-list-route.test.js +38 -0
- package/lib/forms/controls/single-reference.test.js +0 -2
- 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/templates/list.js +31 -12
- package/lib/test_utils/test-on-real-app.js +9 -0
- package/package.json +1 -1
- package/src/add-list-route.test.ts +45 -0
- package/src/forms/controls/single-reference.test.ts +0 -2
- package/src/page/default-list-filters.ts +45 -33
- package/src/page/list.ts +8 -8
- package/src/page/sealious-list.ts +5 -0
- package/src/templates/list.ts +31 -12
- package/src/test_utils/test-on-real-app.ts +13 -0
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
}
|
|
@@ -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,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 =
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
@@ -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) {
|
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
|
}
|
|
@@ -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
|
@@ -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" });
|
|
@@ -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
|
}
|
package/src/templates/list.ts
CHANGED
|
@@ -61,7 +61,7 @@ const filterFields = [
|
|
|
61
61
|
${collection_fields
|
|
62
62
|
.map(
|
|
63
63
|
(field) =>
|
|
64
|
-
` { field: "${field.name}",
|
|
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
|
}`;
|
|
@@ -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
|
|
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
|
-
|
|
133
|
-
|
|
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}>) {
|
|
@@ -157,6 +170,15 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
|
|
|
157
170
|
return <td>{format ? format(value) : 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
|
-
|
|
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
|
}
|