@sealcode/sealgen 0.12.13 → 0.14.0
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/forms/controls/array.d.ts +1 -1
- package/@types/src/forms/controls/derived-frame.d.ts +1 -1
- package/@types/src/forms/controls/form-control.d.ts +1 -1
- package/@types/src/forms/controls/multiple-files.d.ts +11 -11
- package/@types/src/forms/fields/file.d.ts +4 -4
- package/@types/src/forms/form.d.ts +1 -1
- package/@types/src/forms/multiform.d.ts +1 -1
- package/@types/src/index.d.ts +2 -2
- package/@types/src/mount.d.ts +1 -1
- package/@types/src/page/mountable.d.ts +1 -1
- package/@types/src/page/page.d.ts +1 -1
- package/@types/src/page/stateful-page.d.ts +18 -15
- package/dist/forms/controls/multiple-files.js +4 -2
- package/dist/forms/controls/photo.js +2 -2
- package/dist/forms/fields/file.js +5 -5
- package/dist/mount.js +4 -4
- package/dist/page/stateful-page.js +35 -21
- package/package.json +2 -2
- package/src/forms/controls/multiple-files.ts +21 -19
- package/src/forms/controls/photo.ts +2 -2
- package/src/forms/fields/file.ts +7 -7
- package/src/index.ts +2 -2
- package/src/mount.ts +4 -4
- package/src/page/stateful-page.ts +67 -42
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="koa__router"
|
|
2
|
+
/// <reference types="koa__router" />
|
|
3
3
|
import Router from "@koa/router";
|
|
4
4
|
import { Context } from "koa";
|
|
5
5
|
import { Field, FieldsetOutput } from "sealious";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
/// <reference types="koa__router"
|
|
3
|
+
/// <reference types="koa__router" />
|
|
4
4
|
import Router from "@koa/router";
|
|
5
5
|
import { Context } from "koa";
|
|
6
6
|
import { ParsedUrlQuery } from "querystring";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/// <reference types="koa__router"
|
|
1
|
+
/// <reference types="koa__router" />
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
3
|
import Router from "@koa/router";
|
|
4
|
-
import
|
|
4
|
+
import { CollectionItem, File as SealiousFile, Field as SealiousField, Collection } from "sealious";
|
|
5
5
|
import { Context } from "koa";
|
|
6
6
|
import { FlatTemplatable } from "tempstream";
|
|
7
7
|
import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files.js";
|
|
@@ -10,7 +10,7 @@ import { FormControlContext } from "./form-control.js";
|
|
|
10
10
|
export type MultipleFilesOptions = {
|
|
11
11
|
label?: string;
|
|
12
12
|
uploadLabel?: string;
|
|
13
|
-
getAdditionalFields: (ctx: Context, file:
|
|
13
|
+
getAdditionalFields: (ctx: Context, file: SealiousFile) => Promise<Record<string, unknown>>;
|
|
14
14
|
};
|
|
15
15
|
export declare class MultipleFiles extends FormFieldControl {
|
|
16
16
|
field: MultipleFilesField;
|
|
@@ -19,18 +19,18 @@ export declare class MultipleFiles extends FormFieldControl {
|
|
|
19
19
|
getClassModifiers(): string[];
|
|
20
20
|
mount(router: Router): void;
|
|
21
21
|
deleteFileAssociation(ctx: Context, file_item_id: string): Promise<void>;
|
|
22
|
-
addFileAssociation(ctx: Context, file:
|
|
22
|
+
addFileAssociation(ctx: Context, file: SealiousFile): Promise<void>;
|
|
23
23
|
getFrameRelativeURL(): string;
|
|
24
24
|
getFrameID(): string;
|
|
25
25
|
renderFrame(ctx: Context, content?: FlatTemplatable): import("stream").Readable;
|
|
26
26
|
render(fctx: FormControlContext): FlatTemplatable | Promise<FlatTemplatable>;
|
|
27
|
-
getReferencingCollection():
|
|
28
|
-
getFileField():
|
|
29
|
-
extractFileFromItem(item:
|
|
30
|
-
renderFileItemPreview(_fileItem:
|
|
31
|
-
renderFileItem(fileItem:
|
|
32
|
-
renderFileRemoveButton(fileItem:
|
|
33
|
-
getFileItems(ctx: Context): Promise<
|
|
27
|
+
getReferencingCollection(): Collection;
|
|
28
|
+
getFileField(): SealiousField | null;
|
|
29
|
+
extractFileFromItem(item: CollectionItem): Promise<SealiousFile | null>;
|
|
30
|
+
renderFileItemPreview(_fileItem: CollectionItem, file: SealiousFile): Promise<FlatTemplatable>;
|
|
31
|
+
renderFileItem(fileItem: CollectionItem, file: SealiousFile): Promise<FlatTemplatable>;
|
|
32
|
+
renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable;
|
|
33
|
+
getFileItems(ctx: Context): Promise<CollectionItem<any>[]>;
|
|
34
34
|
getInputAttributes(): {
|
|
35
35
|
type: string;
|
|
36
36
|
name: string;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
|
-
import
|
|
2
|
+
import { File as SealiousFile } from "sealious";
|
|
3
3
|
import { FormDataValue } from "../form-types.js";
|
|
4
4
|
import { FieldParseResult, FormField } from "./field.js";
|
|
5
5
|
export type FileContainer = {
|
|
6
|
-
old:
|
|
7
|
-
new:
|
|
6
|
+
old: SealiousFile | null;
|
|
7
|
+
new: SealiousFile | null;
|
|
8
8
|
};
|
|
9
9
|
export declare class File<Required extends boolean> extends FormField<Required, FileContainer> {
|
|
10
10
|
parse(ctx: Context, raw_value: FormDataValue): Promise<FieldParseResult<FileContainer>>;
|
|
11
|
-
getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<
|
|
11
|
+
getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<SealiousFile | undefined>;
|
|
12
12
|
getEmptyValue(): {
|
|
13
13
|
old: null;
|
|
14
14
|
new: null;
|
package/@types/src/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { App, Context as SealiousContext } from "sealious";
|
|
2
2
|
import { FormDataValue } from "./forms/form-types.js";
|
|
3
3
|
declare module "koa" {
|
|
4
4
|
interface BaseContext {
|
|
5
|
-
$context:
|
|
5
|
+
$context: SealiousContext;
|
|
6
6
|
$app: App;
|
|
7
7
|
$body: Record<string, FormDataValue>;
|
|
8
8
|
}
|
package/@types/src/mount.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/// <reference types="koa__router"
|
|
1
|
+
/// <reference types="koa__router" />
|
|
2
2
|
import Router from "@koa/router";
|
|
3
3
|
import { BaseContext } from "koa";
|
|
4
4
|
import { Templatable } from "tempstream";
|
|
@@ -8,9 +8,21 @@ export type StatefulPageActionDescription<ActionName> = {
|
|
|
8
8
|
action: ActionName;
|
|
9
9
|
label?: string;
|
|
10
10
|
} | ActionName;
|
|
11
|
-
export
|
|
11
|
+
export type StateAndMetadata<State, Actions> = {
|
|
12
|
+
state: State;
|
|
13
|
+
inputs: Record<string, string>;
|
|
14
|
+
action: keyof Actions;
|
|
15
|
+
action_args: string;
|
|
16
|
+
} | {
|
|
17
|
+
state: State;
|
|
18
|
+
inputs: Record<string, string>;
|
|
19
|
+
action: null;
|
|
20
|
+
action_args: null;
|
|
21
|
+
$: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
export declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, (state: State, inputs: Record<string, string>, ...args: unknown[]) => State | Promise<State>>> extends Mountable {
|
|
12
24
|
abstract actions: Actions;
|
|
13
|
-
abstract getInitialState(): State
|
|
25
|
+
abstract getInitialState(): State | Promise<State>;
|
|
14
26
|
abstract render(ctx: BaseContext, state: State, inputs: Record<string, string>): Templatable | Promise<Templatable>;
|
|
15
27
|
canAccess(): Promise<{
|
|
16
28
|
readonly canAccess: true;
|
|
@@ -24,22 +36,13 @@ export declare abstract class StatefulPage<State extends Record<string, unknown>
|
|
|
24
36
|
label: string;
|
|
25
37
|
};
|
|
26
38
|
makeActionURL<ActionName extends keyof Actions>(action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
|
|
27
|
-
makeActionButton<ActionName extends keyof Actions>(
|
|
39
|
+
makeActionButton<ActionName extends keyof Actions>(_state: State, action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
|
|
28
40
|
makeActionCallback<ActionName extends keyof Actions>(action_description: {
|
|
29
41
|
action: ActionName;
|
|
30
42
|
label?: string;
|
|
31
43
|
} | ActionName, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
|
|
32
44
|
rerender(): string;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
inputs: Record<string, string>;
|
|
36
|
-
action: keyof Actions;
|
|
37
|
-
action_args: string;
|
|
38
|
-
} | {
|
|
39
|
-
state: State;
|
|
40
|
-
inputs: Record<string, string>;
|
|
41
|
-
action: null;
|
|
42
|
-
action_args: null;
|
|
43
|
-
};
|
|
45
|
+
preprocessRequestBody(values: Record<string, unknown>): Promise<StateAndMetadata<State, Actions>>;
|
|
46
|
+
extractState(ctx: BaseContext): Promise<StateAndMetadata<State, Actions>>;
|
|
44
47
|
mount(router: Router, path: string): void;
|
|
45
48
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
File as SealiousFile
|
|
3
|
+
} from "sealious";
|
|
2
4
|
import { inputWrapper } from "../../utils/input-wrapper.js";
|
|
3
5
|
import { tempstream } from "tempstream";
|
|
4
6
|
import { FormFieldControl } from "./form-field-control.js";
|
|
@@ -105,7 +107,7 @@ class MultipleFiles extends FormFieldControl {
|
|
|
105
107
|
if (!sealious_field) {
|
|
106
108
|
return null;
|
|
107
109
|
}
|
|
108
|
-
return await
|
|
110
|
+
return await SealiousFile.fromID(
|
|
109
111
|
item.collection.app,
|
|
110
112
|
item.get(sealious_field.name).id
|
|
111
113
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tempstream } from "tempstream";
|
|
2
2
|
import { File } from "./file.js";
|
|
3
|
-
import
|
|
3
|
+
import { File as SealiousFile } from "sealious";
|
|
4
4
|
class Photo extends File {
|
|
5
5
|
constructor(field, imageRouter, options) {
|
|
6
6
|
super(field, options);
|
|
@@ -20,7 +20,7 @@ class Photo extends File {
|
|
|
20
20
|
if (!id) {
|
|
21
21
|
return empty_container;
|
|
22
22
|
}
|
|
23
|
-
const file = await
|
|
23
|
+
const file = await SealiousFile.fromID(ctx.$app, id);
|
|
24
24
|
return tempstream`<div class="${class_name}">
|
|
25
25
|
${this.imageRouter.image({
|
|
26
26
|
resolutions: [100],
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
is,
|
|
5
5
|
predicates
|
|
6
6
|
} from "@sealcode/ts-predicates";
|
|
7
|
-
import
|
|
7
|
+
import { File as SealiousFile } from "sealious";
|
|
8
8
|
import { FormField } from "./field.js";
|
|
9
9
|
class File extends FormField {
|
|
10
10
|
async parse(ctx, raw_value) {
|
|
11
11
|
let result;
|
|
12
|
-
if (raw_value instanceof
|
|
12
|
+
if (raw_value instanceof SealiousFile || is(raw_value, predicates.object) && hasShape(
|
|
13
13
|
{
|
|
14
14
|
filename: predicates.string,
|
|
15
15
|
id: predicates.string
|
|
16
16
|
},
|
|
17
17
|
raw_value
|
|
18
18
|
) && raw_value.id) {
|
|
19
|
-
const file = await
|
|
19
|
+
const file = await SealiousFile.fromID(
|
|
20
20
|
ctx.$app,
|
|
21
21
|
raw_value.id
|
|
22
22
|
);
|
|
@@ -35,12 +35,12 @@ class File extends FormField {
|
|
|
35
35
|
const old_file = hasFieldOfType(raw_value, "old", predicates.object) && hasShape(
|
|
36
36
|
{ id: predicates.string, filename: predicates.string },
|
|
37
37
|
raw_value.old
|
|
38
|
-
) && raw_value.old.id ? await
|
|
38
|
+
) && raw_value.old.id ? await SealiousFile.fromID(ctx.$app, raw_value.old.id) : null;
|
|
39
39
|
const new_file = hasFieldOfType(
|
|
40
40
|
raw_value,
|
|
41
41
|
"new",
|
|
42
42
|
predicates.array(predicates.object)
|
|
43
|
-
) && raw_value.new[0] instanceof
|
|
43
|
+
) && raw_value.new[0] instanceof SealiousFile ? raw_value.new[0] : null;
|
|
44
44
|
result = {
|
|
45
45
|
parsable: true,
|
|
46
46
|
error: null,
|
package/dist/mount.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Middlewares as SealiousMiddlewares } from "sealious";
|
|
2
2
|
async function handleHtmlPromise(ctx, next) {
|
|
3
3
|
await next();
|
|
4
4
|
if (ctx.body instanceof Promise) {
|
|
@@ -31,11 +31,11 @@ function mount(router, url, mountable, use_dummy_app = false) {
|
|
|
31
31
|
};
|
|
32
32
|
await next();
|
|
33
33
|
},
|
|
34
|
-
|
|
34
|
+
SealiousMiddlewares.parseBody(),
|
|
35
35
|
handleHtmlPromise
|
|
36
36
|
] : [
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
SealiousMiddlewares.extractContext(),
|
|
38
|
+
SealiousMiddlewares.parseBody(),
|
|
39
39
|
handleHtmlPromise
|
|
40
40
|
];
|
|
41
41
|
router.use(raw_url, ...args);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { predicates, hasShape, is } from "@sealcode/ts-predicates";
|
|
1
|
+
import { predicates, hasShape, is, hasField } from "@sealcode/ts-predicates";
|
|
2
2
|
import deepmerge from "deepmerge";
|
|
3
3
|
import { tempstream } from "tempstream";
|
|
4
4
|
import { from_base64, to_base64 } from "../utils/base64.js";
|
|
@@ -43,16 +43,18 @@ class StatefulPage extends Mountable {
|
|
|
43
43
|
}
|
|
44
44
|
makeActionURL(action_description, ...args) {
|
|
45
45
|
const { action } = this.extractActionAndLabel(action_description);
|
|
46
|
-
return `./?action=${action}&
|
|
46
|
+
return `./?action=${action}&action_args=${to_base64(
|
|
47
|
+
JSON.stringify(args)
|
|
48
|
+
)}`;
|
|
47
49
|
}
|
|
48
|
-
makeActionButton(
|
|
50
|
+
makeActionButton(_state, action_description, ...args) {
|
|
49
51
|
const { label } = this.extractActionAndLabel(action_description);
|
|
50
52
|
return (
|
|
51
53
|
/* HTML */
|
|
52
54
|
`
|
|
53
55
|
<input
|
|
54
56
|
type="submit"
|
|
55
|
-
value
|
|
57
|
+
value="${label}"
|
|
56
58
|
formaction="${this.makeActionURL(action_description, ...args)}"
|
|
57
59
|
/>
|
|
58
60
|
`
|
|
@@ -67,12 +69,15 @@ class StatefulPage extends Mountable {
|
|
|
67
69
|
rerender() {
|
|
68
70
|
return "this.closest('form').requestSubmit()";
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
async preprocessRequestBody(values) {
|
|
73
|
+
return values;
|
|
74
|
+
}
|
|
75
|
+
async extractState(ctx) {
|
|
71
76
|
if (!hasShape(
|
|
72
77
|
{
|
|
73
78
|
action: predicates.maybe(predicates.string),
|
|
74
79
|
state: predicates.string,
|
|
75
|
-
|
|
80
|
+
action_args: predicates.maybe(predicates.string),
|
|
76
81
|
$: predicates.maybe(predicates.object)
|
|
77
82
|
},
|
|
78
83
|
ctx.$body
|
|
@@ -80,43 +85,52 @@ class StatefulPage extends Mountable {
|
|
|
80
85
|
console.error("Wrong data: ", ctx.$body);
|
|
81
86
|
throw new Error("wrong formdata shape");
|
|
82
87
|
}
|
|
88
|
+
const prepared_body = await this.preprocessRequestBody(ctx.$body);
|
|
83
89
|
const inputs = Object.fromEntries(
|
|
84
|
-
Object.entries(
|
|
90
|
+
Object.entries(prepared_body).filter(
|
|
85
91
|
([key]) => !["action", "state", "args", "$"].includes(key)
|
|
86
92
|
)
|
|
87
93
|
);
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const original_state_string = prepared_body.state;
|
|
95
|
+
const original_state = JSON.parse(
|
|
96
|
+
typeof original_state_string == "string" ? from_base64(original_state_string) : "{}"
|
|
97
|
+
);
|
|
98
|
+
const modified_state = deepmerge(
|
|
99
|
+
original_state,
|
|
100
|
+
hasField("$", prepared_body) ? prepared_body.$ : {},
|
|
101
|
+
{
|
|
102
|
+
arrayMerge: (_target, source, _options) => {
|
|
103
|
+
return source;
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
|
-
|
|
94
|
-
if (
|
|
106
|
+
);
|
|
107
|
+
if (prepared_body.action && prepared_body.action_args) {
|
|
95
108
|
return {
|
|
96
109
|
state: modified_state,
|
|
97
|
-
action:
|
|
110
|
+
action: prepared_body.action,
|
|
98
111
|
inputs,
|
|
99
|
-
action_args:
|
|
112
|
+
action_args: prepared_body.action_args
|
|
100
113
|
};
|
|
101
114
|
} else {
|
|
102
115
|
return {
|
|
103
116
|
state: modified_state,
|
|
104
117
|
action: null,
|
|
118
|
+
inputs,
|
|
105
119
|
action_args: null,
|
|
106
|
-
|
|
120
|
+
$: hasField("$", prepared_body) ? prepared_body.$ : {}
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
mount(router, path) {
|
|
111
|
-
router.get(path, (ctx) => {
|
|
112
|
-
ctx.body = this.render(ctx, this.getInitialState(), {});
|
|
125
|
+
router.get(path, async (ctx) => {
|
|
126
|
+
ctx.body = this.render(ctx, await this.getInitialState(), {});
|
|
113
127
|
});
|
|
114
|
-
router.post(path, (ctx) => {
|
|
115
|
-
const { action, state, inputs, action_args } = this.extractState(ctx);
|
|
128
|
+
router.post(path, async (ctx) => {
|
|
129
|
+
const { action, state, inputs, action_args } = await this.extractState(ctx);
|
|
116
130
|
if (action) {
|
|
117
131
|
ctx.body = this.render(
|
|
118
132
|
ctx,
|
|
119
|
-
this.actions[action](
|
|
133
|
+
await this.actions[action](
|
|
120
134
|
state,
|
|
121
135
|
inputs,
|
|
122
136
|
...JSON.parse(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealcode/sealgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Module to automate adding routes and collections to a sealious application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"koa": "^2.13.0",
|
|
73
73
|
"koa-responsive-image-router": "^0.2.6",
|
|
74
|
-
"sealious": "^0.
|
|
74
|
+
"sealious": "^0.18.0"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Router from "@koa/router";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
CollectionItem,
|
|
4
|
+
File as SealiousFile,
|
|
5
|
+
Field as SealiousField,
|
|
6
|
+
Collection,
|
|
7
|
+
} from "sealious";
|
|
3
8
|
import { Context } from "koa";
|
|
4
9
|
import { inputWrapper } from "../../utils/input-wrapper.js";
|
|
5
10
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
@@ -14,7 +19,7 @@ export type MultipleFilesOptions = {
|
|
|
14
19
|
uploadLabel?: string;
|
|
15
20
|
getAdditionalFields: (
|
|
16
21
|
ctx: Context,
|
|
17
|
-
file:
|
|
22
|
+
file: SealiousFile
|
|
18
23
|
) => Promise<Record<string, unknown>>;
|
|
19
24
|
};
|
|
20
25
|
|
|
@@ -52,7 +57,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
52
57
|
);
|
|
53
58
|
|
|
54
59
|
router.post(this.getFrameRelativeURL() + "/add", async (ctx) => {
|
|
55
|
-
let files = ctx.$body.files as
|
|
60
|
+
let files = ctx.$body.files as SealiousFile | SealiousFile[];
|
|
56
61
|
if (!files || !is(files, predicates.object)) {
|
|
57
62
|
ctx.body = "Missing files";
|
|
58
63
|
return;
|
|
@@ -64,7 +69,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
64
69
|
files.map((file) =>
|
|
65
70
|
this.addFileAssociation(
|
|
66
71
|
ctx,
|
|
67
|
-
file as unknown as
|
|
72
|
+
file as unknown as SealiousFile
|
|
68
73
|
)
|
|
69
74
|
)
|
|
70
75
|
);
|
|
@@ -81,7 +86,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
81
86
|
].removeByID(ctx.$context, file_item_id);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
async addFileAssociation(ctx: Context, file:
|
|
89
|
+
async addFileAssociation(ctx: Context, file: SealiousFile) {
|
|
85
90
|
const file_field = this.getFileField();
|
|
86
91
|
if (!file_field) {
|
|
87
92
|
throw new Error("No file field in referencing collection");
|
|
@@ -130,7 +135,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
130
135
|
return result;
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
getFileField():
|
|
138
|
+
getFileField(): SealiousField | null {
|
|
134
139
|
for (const [_, field] of Object.entries(
|
|
135
140
|
this.getReferencingCollection().fields
|
|
136
141
|
)) {
|
|
@@ -142,28 +147,28 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
async extractFileFromItem(
|
|
145
|
-
item:
|
|
146
|
-
): Promise<
|
|
150
|
+
item: CollectionItem
|
|
151
|
+
): Promise<SealiousFile | null> {
|
|
147
152
|
const sealious_field = this.getFileField();
|
|
148
153
|
if (!sealious_field) {
|
|
149
154
|
return null;
|
|
150
155
|
}
|
|
151
|
-
return await
|
|
152
|
-
(item.collection as
|
|
156
|
+
return await SealiousFile.fromID(
|
|
157
|
+
(item.collection as Collection).app,
|
|
153
158
|
item.get(sealious_field.name).id
|
|
154
159
|
);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
async renderFileItemPreview(
|
|
158
|
-
_fileItem:
|
|
159
|
-
file:
|
|
163
|
+
_fileItem: CollectionItem,
|
|
164
|
+
file: SealiousFile
|
|
160
165
|
): Promise<FlatTemplatable> {
|
|
161
166
|
return file.filename || "no filename";
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
async renderFileItem(
|
|
165
|
-
fileItem:
|
|
166
|
-
file:
|
|
170
|
+
fileItem: CollectionItem,
|
|
171
|
+
file: SealiousFile
|
|
167
172
|
): Promise<FlatTemplatable> {
|
|
168
173
|
return tempstream/* HTML */ `<li class="file-list-item">
|
|
169
174
|
<a
|
|
@@ -177,7 +182,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
177
182
|
</li>`;
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
renderFileRemoveButton(fileItem:
|
|
185
|
+
renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable {
|
|
181
186
|
return /* HTML */ ` <form
|
|
182
187
|
data-turbo-frame="${this.getFrameID()}"
|
|
183
188
|
action="${this.getFrameRelativeURL()}/delete/${fileItem.id}"
|
|
@@ -221,10 +226,7 @@ export class MultipleFiles extends FormFieldControl {
|
|
|
221
226
|
await this.extractFileFromItem(item),
|
|
222
227
|
])
|
|
223
228
|
)
|
|
224
|
-
).filter(([_, f]) => f !== null) as [
|
|
225
|
-
Sealious.CollectionItem,
|
|
226
|
-
Sealious.File
|
|
227
|
-
][];
|
|
229
|
+
).filter(([_, f]) => f !== null) as [CollectionItem, SealiousFile][];
|
|
228
230
|
|
|
229
231
|
return inputWrapper(
|
|
230
232
|
["multiple-files", this.field.name, ...this.getClassModifiers()],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
3
|
import { File } from "./file.js";
|
|
4
|
-
import
|
|
4
|
+
import { File as SealiousFile } from "sealious";
|
|
5
5
|
import { FormField } from "../fields/field.js";
|
|
6
6
|
import { SimpleInputOptions } from "./simple-input.js";
|
|
7
7
|
import { FormControlContext } from "./form-control.js";
|
|
@@ -34,7 +34,7 @@ export class Photo extends File {
|
|
|
34
34
|
if (!id) {
|
|
35
35
|
return empty_container;
|
|
36
36
|
}
|
|
37
|
-
const file = await
|
|
37
|
+
const file = await SealiousFile.fromID(ctx.$app, id);
|
|
38
38
|
return tempstream/* HTML */ `<div class="${class_name}">
|
|
39
39
|
${this.imageRouter.image({
|
|
40
40
|
resolutions: [100],
|
package/src/forms/fields/file.ts
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
predicates,
|
|
6
6
|
} from "@sealcode/ts-predicates";
|
|
7
7
|
import { Context } from "koa";
|
|
8
|
-
import
|
|
8
|
+
import { File as SealiousFile } from "sealious";
|
|
9
9
|
import { FormDataValue } from "../form-types.js";
|
|
10
10
|
import { FieldParseResult, FormField } from "./field.js";
|
|
11
11
|
|
|
12
12
|
export type FileContainer = {
|
|
13
|
-
old:
|
|
14
|
-
new:
|
|
13
|
+
old: SealiousFile | null;
|
|
14
|
+
new: SealiousFile | null;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export class File<Required extends boolean> extends FormField<
|
|
@@ -24,7 +24,7 @@ export class File<Required extends boolean> extends FormField<
|
|
|
24
24
|
): Promise<FieldParseResult<FileContainer>> {
|
|
25
25
|
let result;
|
|
26
26
|
if (
|
|
27
|
-
raw_value instanceof
|
|
27
|
+
raw_value instanceof SealiousFile ||
|
|
28
28
|
(is(raw_value, predicates.object) &&
|
|
29
29
|
hasShape(
|
|
30
30
|
{
|
|
@@ -35,7 +35,7 @@ export class File<Required extends boolean> extends FormField<
|
|
|
35
35
|
) &&
|
|
36
36
|
raw_value.id)
|
|
37
37
|
) {
|
|
38
|
-
const file = await
|
|
38
|
+
const file = await SealiousFile.fromID(
|
|
39
39
|
ctx.$app,
|
|
40
40
|
raw_value.id as string
|
|
41
41
|
);
|
|
@@ -58,14 +58,14 @@ export class File<Required extends boolean> extends FormField<
|
|
|
58
58
|
raw_value.old
|
|
59
59
|
) &&
|
|
60
60
|
raw_value.old.id
|
|
61
|
-
? await
|
|
61
|
+
? await SealiousFile.fromID(ctx.$app, raw_value.old.id)
|
|
62
62
|
: null;
|
|
63
63
|
const new_file =
|
|
64
64
|
hasFieldOfType(
|
|
65
65
|
raw_value,
|
|
66
66
|
"new",
|
|
67
67
|
predicates.array(predicates.object)
|
|
68
|
-
) && raw_value.new[0] instanceof
|
|
68
|
+
) && raw_value.new[0] instanceof SealiousFile
|
|
69
69
|
? raw_value.new[0]
|
|
70
70
|
: null;
|
|
71
71
|
result = {
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { App, Context as SealiousContext } from "sealious";
|
|
2
2
|
import { FormDataValue } from "./forms/form-types.js";
|
|
3
3
|
|
|
4
4
|
declare module "koa" {
|
|
5
5
|
interface BaseContext {
|
|
6
|
-
$context:
|
|
6
|
+
$context: SealiousContext;
|
|
7
7
|
$app: App;
|
|
8
8
|
$body: Record<string, FormDataValue>;
|
|
9
9
|
}
|
package/src/mount.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Router from "@koa/router";
|
|
2
2
|
import Koa, { BaseContext, Context } from "koa";
|
|
3
|
-
import
|
|
3
|
+
import { Middlewares as SealiousMiddlewares } from "sealious";
|
|
4
4
|
import { Mountable } from "./page/mountable.js";
|
|
5
5
|
|
|
6
6
|
async function handleHtmlPromise(ctx: Context, next: Koa.Next) {
|
|
@@ -48,12 +48,12 @@ export function mount(
|
|
|
48
48
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
49
49
|
await next();
|
|
50
50
|
},
|
|
51
|
-
|
|
51
|
+
SealiousMiddlewares.parseBody(),
|
|
52
52
|
handleHtmlPromise,
|
|
53
53
|
]
|
|
54
54
|
: [
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
SealiousMiddlewares.extractContext(),
|
|
56
|
+
SealiousMiddlewares.parseBody(),
|
|
57
57
|
handleHtmlPromise,
|
|
58
58
|
];
|
|
59
59
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Router from "@koa/router";
|
|
2
|
-
import { predicates, hasShape, is } from "@sealcode/ts-predicates";
|
|
2
|
+
import { predicates, hasShape, is, hasField } from "@sealcode/ts-predicates";
|
|
3
3
|
import deepmerge, { ArrayMergeOptions } from "deepmerge";
|
|
4
4
|
import { BaseContext } from "koa";
|
|
5
5
|
import { Templatable, tempstream } from "tempstream";
|
|
@@ -14,6 +14,21 @@ export type StatefulPageActionDescription<ActionName> =
|
|
|
14
14
|
}
|
|
15
15
|
| ActionName;
|
|
16
16
|
|
|
17
|
+
export type StateAndMetadata<State, Actions> =
|
|
18
|
+
| {
|
|
19
|
+
state: State;
|
|
20
|
+
inputs: Record<string, string>;
|
|
21
|
+
action: keyof Actions;
|
|
22
|
+
action_args: string;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
state: State;
|
|
26
|
+
inputs: Record<string, string>;
|
|
27
|
+
action: null;
|
|
28
|
+
action_args: null;
|
|
29
|
+
$: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
export abstract class StatefulPage<
|
|
18
33
|
State extends Record<string, unknown>,
|
|
19
34
|
Actions extends Record<
|
|
@@ -22,11 +37,11 @@ export abstract class StatefulPage<
|
|
|
22
37
|
state: State,
|
|
23
38
|
inputs: Record<string, string>,
|
|
24
39
|
...args: unknown[]
|
|
25
|
-
) => State
|
|
40
|
+
) => State | Promise<State>
|
|
26
41
|
>
|
|
27
42
|
> extends Mountable {
|
|
28
43
|
abstract actions: Actions;
|
|
29
|
-
abstract getInitialState(): State
|
|
44
|
+
abstract getInitialState(): State | Promise<State>;
|
|
30
45
|
abstract render(
|
|
31
46
|
ctx: BaseContext,
|
|
32
47
|
state: State,
|
|
@@ -87,11 +102,13 @@ export abstract class StatefulPage<
|
|
|
87
102
|
...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
|
|
88
103
|
) {
|
|
89
104
|
const { action } = this.extractActionAndLabel(action_description);
|
|
90
|
-
return `./?action=${action}&
|
|
105
|
+
return `./?action=${action}&action_args=${to_base64(
|
|
106
|
+
JSON.stringify(args)
|
|
107
|
+
)}`;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
makeActionButton<ActionName extends keyof Actions>(
|
|
94
|
-
|
|
111
|
+
_state: State,
|
|
95
112
|
action_description: StatefulPageActionDescription<ActionName>,
|
|
96
113
|
...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
|
|
97
114
|
) {
|
|
@@ -99,7 +116,7 @@ export abstract class StatefulPage<
|
|
|
99
116
|
return /* HTML */ `
|
|
100
117
|
<input
|
|
101
118
|
type="submit"
|
|
102
|
-
value
|
|
119
|
+
value="${label}"
|
|
103
120
|
formaction="${this.makeActionURL(action_description, ...args)}"
|
|
104
121
|
/>
|
|
105
122
|
`;
|
|
@@ -124,25 +141,21 @@ export abstract class StatefulPage<
|
|
|
124
141
|
return "this.closest('form').requestSubmit()";
|
|
125
142
|
}
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
inputs: Record<string, string>;
|
|
137
|
-
action: null;
|
|
138
|
-
action_args: null;
|
|
139
|
-
} {
|
|
144
|
+
async preprocessRequestBody(
|
|
145
|
+
values: Record<string, unknown>
|
|
146
|
+
): Promise<StateAndMetadata<State, Actions>> {
|
|
147
|
+
return values as StateAndMetadata<State, Actions>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async extractState(
|
|
151
|
+
ctx: BaseContext
|
|
152
|
+
): Promise<StateAndMetadata<State, Actions>> {
|
|
140
153
|
if (
|
|
141
154
|
!hasShape(
|
|
142
155
|
{
|
|
143
156
|
action: predicates.maybe(predicates.string),
|
|
144
157
|
state: predicates.string,
|
|
145
|
-
|
|
158
|
+
action_args: predicates.maybe(predicates.string),
|
|
146
159
|
$: predicates.maybe(predicates.object),
|
|
147
160
|
},
|
|
148
161
|
ctx.$body
|
|
@@ -151,55 +164,67 @@ export abstract class StatefulPage<
|
|
|
151
164
|
console.error("Wrong data: ", ctx.$body);
|
|
152
165
|
throw new Error("wrong formdata shape");
|
|
153
166
|
}
|
|
167
|
+
// giving extending classes a change to modify body before furhter processing
|
|
168
|
+
const prepared_body = await this.preprocessRequestBody(ctx.$body);
|
|
154
169
|
const inputs = Object.fromEntries(
|
|
155
|
-
Object.entries(
|
|
170
|
+
Object.entries(prepared_body).filter(
|
|
156
171
|
([key]) => !["action", "state", "args", "$"].includes(key)
|
|
157
172
|
)
|
|
158
173
|
) as Record<string, string>;
|
|
159
174
|
// the "$" key is parsed as dot notation and overrides the state
|
|
160
175
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
const original_state_string = prepared_body.state;
|
|
177
|
+
const original_state = JSON.parse(
|
|
178
|
+
typeof original_state_string == "string"
|
|
179
|
+
? from_base64(original_state_string)
|
|
180
|
+
: "{}"
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const modified_state = deepmerge(
|
|
184
|
+
original_state,
|
|
185
|
+
hasField("$", prepared_body) ? prepared_body.$ : {},
|
|
186
|
+
{
|
|
187
|
+
arrayMerge: (
|
|
188
|
+
_target: any[],
|
|
189
|
+
source: any[],
|
|
190
|
+
_options: ArrayMergeOptions
|
|
191
|
+
) => {
|
|
192
|
+
// https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
|
|
193
|
+
return source;
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
) as State;
|
|
173
197
|
|
|
174
|
-
if (
|
|
198
|
+
if (prepared_body.action && prepared_body.action_args) {
|
|
175
199
|
return {
|
|
176
200
|
state: modified_state,
|
|
177
|
-
action:
|
|
201
|
+
action: prepared_body.action as string,
|
|
178
202
|
inputs,
|
|
179
|
-
action_args:
|
|
203
|
+
action_args: prepared_body.action_args,
|
|
180
204
|
};
|
|
181
205
|
} else {
|
|
182
206
|
return {
|
|
183
207
|
state: modified_state,
|
|
184
208
|
action: null,
|
|
185
|
-
action_args: null,
|
|
186
209
|
inputs,
|
|
210
|
+
action_args: null,
|
|
211
|
+
$: hasField("$", prepared_body) ? prepared_body.$ : {},
|
|
187
212
|
};
|
|
188
213
|
}
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
mount(router: Router, path: string) {
|
|
192
|
-
router.get(path, (ctx) => {
|
|
193
|
-
ctx.body = this.render(ctx, this.getInitialState(), {});
|
|
217
|
+
router.get(path, async (ctx) => {
|
|
218
|
+
ctx.body = this.render(ctx, await this.getInitialState(), {});
|
|
194
219
|
});
|
|
195
220
|
|
|
196
|
-
router.post(path, (ctx) => {
|
|
221
|
+
router.post(path, async (ctx) => {
|
|
197
222
|
const { action, state, inputs, action_args } =
|
|
198
|
-
this.extractState(ctx);
|
|
223
|
+
await this.extractState(ctx);
|
|
199
224
|
if (action) {
|
|
200
225
|
ctx.body = this.render(
|
|
201
226
|
ctx,
|
|
202
|
-
this.actions[action](
|
|
227
|
+
await this.actions[action](
|
|
203
228
|
state,
|
|
204
229
|
inputs,
|
|
205
230
|
...(JSON.parse(
|