@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/src/templates/form.ts
CHANGED
|
@@ -2,10 +2,39 @@ import Prettier from "prettier";
|
|
|
2
2
|
import { curryImportPath } from "../utils/import-path.js";
|
|
3
3
|
import { getPrettierConfig } from "../utils/prettier.js";
|
|
4
4
|
|
|
5
|
+
const defaults = {
|
|
6
|
+
postimport: ``,
|
|
7
|
+
fields: `
|
|
8
|
+
email: new Fields.CollectionField(true, Users.fields.email),
|
|
9
|
+
password: new Fields.SimpleFormField(true),`,
|
|
10
|
+
controls: `
|
|
11
|
+
new Controls.SimpleInput(fields.email, { label: "Email:", type: "email" }),
|
|
12
|
+
new Controls.SimpleInput(fields.password, {
|
|
13
|
+
label: "Password:",
|
|
14
|
+
type: "password",
|
|
15
|
+
}),`,
|
|
16
|
+
validate_method: `async validateValues(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
data: Record<string, FormDataValue>
|
|
19
|
+
): Promise<{ valid: boolean; error: string }> {
|
|
20
|
+
// just a silly example, please remove this with better logic
|
|
21
|
+
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
|
22
|
+
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
24
|
+
if ((password || "").length > ((email as string) || "").length) {
|
|
25
|
+
return { valid: false, error: "Password cannot be longer than email" };
|
|
26
|
+
} else {
|
|
27
|
+
return { valid: true, error: "" };
|
|
28
|
+
}
|
|
29
|
+
}`,
|
|
30
|
+
};
|
|
31
|
+
|
|
5
32
|
export async function formTemplate(
|
|
6
33
|
action_name: string,
|
|
7
|
-
newfilefullpath: string
|
|
34
|
+
newfilefullpath: string,
|
|
35
|
+
_params: Partial<typeof defaults> = defaults
|
|
8
36
|
): Promise<string> {
|
|
37
|
+
const params = { ...defaults, ..._params };
|
|
9
38
|
const rel = curryImportPath(newfilefullpath);
|
|
10
39
|
const content = `import type { Context } from "koa";
|
|
11
40
|
import type { FormData, FormDataValue } from "@sealcode/sealgen";
|
|
@@ -13,11 +42,12 @@ import { Form, Fields, Controls, fieldsToShape } from "@sealcode/sealgen";
|
|
|
13
42
|
import html from "${rel("src/back/html.js")}";
|
|
14
43
|
import { Users } from "${rel("src/back/collections/collections.js")}";
|
|
15
44
|
|
|
45
|
+
${params.postimport}
|
|
46
|
+
|
|
16
47
|
export const actionName = "${action_name}";
|
|
17
48
|
|
|
18
49
|
const fields = {
|
|
19
|
-
|
|
20
|
-
password: new Fields.SimpleFormField(true),
|
|
50
|
+
${params.fields}
|
|
21
51
|
};
|
|
22
52
|
|
|
23
53
|
export const ${action_name}Shape = fieldsToShape(fields);
|
|
@@ -27,27 +57,9 @@ export default new (class ${action_name}Form extends Form<typeof fields, void> {
|
|
|
27
57
|
fields = fields;
|
|
28
58
|
|
|
29
59
|
controls = [
|
|
30
|
-
|
|
31
|
-
new Controls.SimpleInput(fields.password, {
|
|
32
|
-
label: "Password:",
|
|
33
|
-
type: "password",
|
|
34
|
-
}),
|
|
60
|
+
${params.controls}
|
|
35
61
|
];
|
|
36
62
|
|
|
37
|
-
async validateValues(
|
|
38
|
-
ctx: Context,
|
|
39
|
-
data: Record<string, FormDataValue>
|
|
40
|
-
): Promise<{ valid: boolean; error: string }> {
|
|
41
|
-
// just a silly example, please remove this with better logic
|
|
42
|
-
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
|
43
|
-
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
45
|
-
if ((password || "").length > ((email as string) || "").length) {
|
|
46
|
-
return { valid: false, error: "Password cannot be longer than email" };
|
|
47
|
-
} else {
|
|
48
|
-
return { valid: true, error: "" };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
63
|
|
|
52
64
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
53
65
|
async canAccess(_: Context) {
|
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
|
}
|
|
@@ -5,6 +5,8 @@ import { spawn } from "child_process";
|
|
|
5
5
|
import getPort from "get-port";
|
|
6
6
|
import waitPort from "wait-port";
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import chalk from "chalk";
|
|
8
10
|
|
|
9
11
|
import _locreq from "locreq";
|
|
10
12
|
import { module_dirname } from "../utils/module-dirname.js";
|
|
@@ -43,6 +45,7 @@ export class RealAppTest {
|
|
|
43
45
|
console.log(
|
|
44
46
|
"\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
|
|
45
47
|
);
|
|
48
|
+
await test_app.runSealgenCommand("build");
|
|
46
49
|
test_app.app_port = await getPort();
|
|
47
50
|
return test_app;
|
|
48
51
|
}
|
|
@@ -83,6 +86,11 @@ export class RealAppTest {
|
|
|
83
86
|
output: "silent",
|
|
84
87
|
timeout: 6000,
|
|
85
88
|
});
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.yellow(
|
|
91
|
+
`App is listening on http://localhost:${this.app_port}`
|
|
92
|
+
)
|
|
93
|
+
);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
async close() {
|
|
@@ -105,6 +113,11 @@ export class RealAppTest {
|
|
|
105
113
|
this.app_path
|
|
106
114
|
} && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
|
|
107
115
|
);
|
|
116
|
+
await this.build();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async build() {
|
|
120
|
+
await execAsync(`cd ${this.app_path} && ${callSealgenBin()} build`);
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
async throwIfItKillsTheApp<T>(cb: () => Promise<T>): Promise<T> {
|
|
@@ -134,7 +147,7 @@ export class RealAppTest {
|
|
|
134
147
|
});
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
async
|
|
150
|
+
async httpGET(path: string): Promise<{ status: number; response: string }> {
|
|
138
151
|
return this.throwIfItKillsTheApp(async () => {
|
|
139
152
|
const ret = await fetch(`http://localhost:${this.app_port}${path}`);
|
|
140
153
|
const status = ret.status;
|
|
@@ -147,6 +160,7 @@ export class RealAppTest {
|
|
|
147
160
|
path: string,
|
|
148
161
|
body: Record<string, unknown>
|
|
149
162
|
): Promise<{ status: number; response: string }> {
|
|
163
|
+
console.debug("POST", path, body);
|
|
150
164
|
return this.throwIfItKillsTheApp(async () => {
|
|
151
165
|
const ret = await fetch(
|
|
152
166
|
`http://localhost:${this.app_port}${path}`,
|
|
@@ -159,9 +173,27 @@ export class RealAppTest {
|
|
|
159
173
|
const status = ret.status;
|
|
160
174
|
const response = await ret.text();
|
|
161
175
|
|
|
162
|
-
console.
|
|
163
|
-
console.
|
|
176
|
+
console.debug("request done!");
|
|
177
|
+
console.debug({ response });
|
|
164
178
|
return { status, response };
|
|
165
179
|
});
|
|
166
180
|
}
|
|
181
|
+
|
|
182
|
+
async addFile(target_path: string, content: string) {
|
|
183
|
+
console.log(
|
|
184
|
+
chalk.yellow("Inserting file at path"),
|
|
185
|
+
chalk.green(target_path),
|
|
186
|
+
"with content\n",
|
|
187
|
+
content,
|
|
188
|
+
"\n\n\n\n=====\n"
|
|
189
|
+
);
|
|
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}`;
|
|
198
|
+
}
|
|
167
199
|
}
|