@sealcode/sealgen 0.14.16 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/@types/src/forms/controls/file.d.ts +3 -5
  2. package/@types/src/forms/controls/multiple-files.d.ts +9 -8
  3. package/@types/src/forms/controls/multiple-photos.d.ts +3 -2
  4. package/@types/src/forms/controls/photo.d.ts +2 -4
  5. package/@types/src/forms/controls/photo.test.d.ts +1 -0
  6. package/@types/src/forms/controls/simple-input.d.ts +1 -1
  7. package/@types/src/forms/fields/file.d.ts +7 -4
  8. package/@types/src/forms/form-types.d.ts +2 -2
  9. package/@types/src/utils/module-dirname.d.ts +1 -0
  10. package/assets/important.png +0 -0
  11. package/dist/add-route.test.js +2 -1
  12. package/dist/forms/controls/file.js +26 -14
  13. package/dist/forms/controls/multiple-files.js +9 -12
  14. package/dist/forms/controls/multiple-photos.js +8 -2
  15. package/dist/forms/controls/photo.js +3 -9
  16. package/dist/forms/controls/photo.test.js +81 -0
  17. package/dist/forms/controls/simple-input.js +1 -1
  18. package/dist/forms/fields/file.js +11 -23
  19. package/dist/templates/stateful-page.js +0 -1
  20. package/dist/utils/module-dirname.js +8 -0
  21. package/dist/utils/render-attributes.js +3 -1
  22. package/package.json +4 -2
  23. package/src/add-route.test.ts +3 -1
  24. package/src/forms/controls/array.ts +3 -1
  25. package/src/forms/controls/file.ts +27 -14
  26. package/src/forms/controls/multiple-files.ts +17 -22
  27. package/src/forms/controls/multiple-photos.ts +10 -4
  28. package/src/forms/controls/photo.test.ts +90 -0
  29. package/src/forms/controls/photo.ts +4 -9
  30. package/src/forms/controls/simple-input.ts +2 -2
  31. package/src/forms/fields/file.ts +17 -35
  32. package/src/forms/form-types.ts +2 -2
  33. package/src/templates/stateful-page.ts +0 -1
  34. package/src/utils/module-dirname.ts +5 -0
  35. package/src/utils/render-attributes.ts +3 -1
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import { Context } from "koa";
3
+ import { FilePointer } from "@sealcode/file-manager";
3
4
  import { FlatTemplatable } from "tempstream";
4
5
  import { FileContainer } from "../fields/file.js";
5
6
  import { FormDataValue } from "../form-types.js";
@@ -7,10 +8,7 @@ import { FormControlContext } from "./form-control.js";
7
8
  import { SimpleInput } from "./simple-input.js";
8
9
  export declare class File extends SimpleInput<FileContainer> {
9
10
  getType(): string;
10
- renderFilePreview(_ctx: Context, file_data: {
11
- id?: string;
12
- filename?: string;
13
- } | null): Promise<FlatTemplatable>;
14
- preInput(ctx: Context, data: Record<string, FormDataValue>): Promise<import("stream").Readable>;
11
+ renderFilePreview(_ctx: Context, file: FilePointer | null): Promise<FlatTemplatable>;
12
+ preInput(fctx: FormControlContext, data: Record<string, FormDataValue>): Promise<import("stream").Readable>;
15
13
  getInputAttributes(fctx: FormControlContext): Promise<Record<string, string | boolean>>;
16
14
  }
@@ -1,16 +1,17 @@
1
1
  /// <reference types="koa__router" />
2
2
  /// <reference types="node" resolution-mode="require"/>
3
3
  import Router from "@koa/router";
4
- import { CollectionItem, File as SealiousFile, Field as SealiousField, Collection } from "sealious";
4
+ import { CollectionItem, Field as SealiousField } 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";
8
8
  import { FormFieldControl } from "./form-field-control.js";
9
9
  import { FormControlContext } from "./form-control.js";
10
+ import { FilePointer } from "@sealcode/file-manager";
10
11
  export type MultipleFilesOptions = {
11
12
  label?: string;
12
13
  uploadLabel?: string;
13
- getAdditionalFields: (ctx: Context, file: SealiousFile) => Promise<Record<string, unknown>>;
14
+ getAdditionalFields: (ctx: Context, file: FilePointer) => Promise<Record<string, unknown>>;
14
15
  };
15
16
  export declare class MultipleFiles extends FormFieldControl {
16
17
  field: MultipleFilesField;
@@ -19,16 +20,16 @@ export declare class MultipleFiles extends FormFieldControl {
19
20
  getClassModifiers(): string[];
20
21
  mount(router: Router): void;
21
22
  deleteFileAssociation(ctx: Context, file_item_id: string): Promise<void>;
22
- addFileAssociation(ctx: Context, file: SealiousFile): Promise<void>;
23
+ addFileAssociation(ctx: Context, file: FilePointer): Promise<void>;
23
24
  getFrameRelativeURL(): string;
24
25
  getFrameID(): string;
25
- renderFrame(ctx: Context, content?: FlatTemplatable): import("stream").Readable;
26
+ renderFrame(_ctx: Context, content?: FlatTemplatable): import("stream").Readable;
26
27
  render(fctx: FormControlContext): FlatTemplatable | Promise<FlatTemplatable>;
27
- getReferencingCollection(): Collection;
28
+ getReferencingCollection(): import("sealious").Collection;
28
29
  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>;
30
+ extractFileFromItem(item: CollectionItem): Promise<FilePointer | null>;
31
+ renderFileItemPreview(_fileItem: CollectionItem, file: FilePointer): Promise<FlatTemplatable>;
32
+ renderFileItem(fileItem: CollectionItem, file: FilePointer): Promise<FlatTemplatable>;
32
33
  renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable;
33
34
  getFileItems(ctx: Context): Promise<CollectionItem<any>[]>;
34
35
  getInputAttributes(): {
@@ -1,4 +1,5 @@
1
- import { File as SealiousFile, CollectionItem } from "sealious";
1
+ import { CollectionItem } from "sealious";
2
+ import { FilePointer } from "@sealcode/file-manager";
2
3
  import { FlatTemplatable } from "tempstream";
3
4
  import { MultipleFiles, MultipleFilesOptions } from "./multiple-files.js";
4
5
  import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files.js";
@@ -14,5 +15,5 @@ export declare class MultiplePhotos extends MultipleFiles {
14
15
  name: string;
15
16
  multiple: boolean;
16
17
  };
17
- renderFileItem(fileItem: CollectionItem, file: SealiousFile): Promise<FlatTemplatable>;
18
+ renderFileItem(fileItem: CollectionItem, file: FilePointer): Promise<FlatTemplatable>;
18
19
  }
@@ -6,15 +6,13 @@ import { SimpleInputOptions } from "./simple-input.js";
6
6
  import { FormControlContext } from "./form-control.js";
7
7
  import { FileContainer } from "../fields/file.js";
8
8
  import { KoaResponsiveImageRouter } from "koa-responsive-image-router";
9
+ import { FilePointer } from "@sealcode/file-manager";
9
10
  export declare class Photo extends File {
10
11
  field: FormField<boolean, FileContainer>;
11
12
  imageRouter: KoaResponsiveImageRouter;
12
13
  constructor(field: FormField<boolean, FileContainer>, imageRouter: KoaResponsiveImageRouter, options?: SimpleInputOptions);
13
14
  getImgStyle(): Promise<string>;
14
- renderFilePreview(ctx: Context, file_data: {
15
- id?: string;
16
- filename?: string;
17
- } | null): Promise<FlatTemplatable>;
15
+ renderFilePreview(ctx: Context, file: FilePointer | null): Promise<FlatTemplatable>;
18
16
  getWrapperModifiers(): string[];
19
17
  getInputAttributes(fctx: FormControlContext): Promise<{
20
18
  accept: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -20,7 +20,7 @@ export declare class SimpleInput<ParsedValue, Options extends SimpleInputOptions
20
20
  field: FormField<boolean, ParsedValue>;
21
21
  options: SimpleInputOptions;
22
22
  constructor(field: FormField<boolean, ParsedValue>, options?: Options);
23
- preInput(_ctx: Context, _data: Record<string, FormDataValue>): Promise<FlatTemplatable>;
23
+ preInput(_ctx: FormControlContext, _data: Record<string, FormDataValue>): Promise<FlatTemplatable>;
24
24
  getType(): string;
25
25
  getLabel(): string;
26
26
  getID(): string;
@@ -1,14 +1,17 @@
1
1
  import { Context } from "koa";
2
- import { File as SealiousFile } from "sealious";
2
+ import { FileManager, FilePointer } from "@sealcode/file-manager";
3
3
  import { FormDataValue } from "../form-types.js";
4
4
  import { FieldParseResult, FormField } from "./field.js";
5
5
  export type FileContainer = {
6
- old: SealiousFile | null;
7
- new: SealiousFile | null;
6
+ old: FilePointer | null;
7
+ new: FilePointer | null;
8
8
  };
9
9
  export declare class File<Required extends boolean> extends FormField<Required, FileContainer> {
10
+ readonly required: Required;
11
+ file_manager: FileManager;
12
+ constructor(required: Required, file_manager: FileManager);
10
13
  parse(ctx: Context, raw_value: FormDataValue): Promise<FieldParseResult<FileContainer>>;
11
- getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<SealiousFile | undefined>;
14
+ getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<FilePointer | undefined>;
12
15
  getEmptyValue(): {
13
16
  old: null;
14
17
  new: null;
@@ -1,8 +1,8 @@
1
- import type { File as SealiousFile } from "sealious";
1
+ import { FilePointer } from "@sealcode/file-manager";
2
2
  import type { FlatTemplatable } from "tempstream";
3
3
  import type { FormField } from "./fields/field.js";
4
4
  export type FormDataPrimitive = string | string[] | number | undefined;
5
- export type FormDataValue = FormDataPrimitive | Record<string, FormDataPrimitive> | SealiousFile;
5
+ export type FormDataValue = FormDataPrimitive | Record<string, FormDataPrimitive> | FilePointer;
6
6
  export type FieldValueType<F extends FormField> = F extends FormField<infer R> ? R : never;
7
7
  export type FormFieldsToValues<F extends Record<string, FormField<boolean, keyof F>>> = {
8
8
  [Property in keyof F]: FieldValueType<F[Property]>;
@@ -0,0 +1 @@
1
+ export declare function module_dirname(module_url: string): string;
Binary file
@@ -4,7 +4,8 @@ import axios from "axios";
4
4
  import waitPort from "wait-port";
5
5
  import getPort from "get-port";
6
6
  import _locreq from "locreq";
7
- const locreq = _locreq(__dirname);
7
+ import { module_dirname } from "./utils/module-dirname.js";
8
+ const locreq = _locreq(module_dirname(import.meta.url));
8
9
  const execAsync = (cmd) => {
9
10
  return new Promise((resolve, reject) => {
10
11
  const process2 = exec(cmd);
@@ -1,25 +1,37 @@
1
1
  import { tempstream } from "tempstream";
2
2
  import { SimpleInput } from "./simple-input.js";
3
+ import { htmlEscape } from "escape-goat";
3
4
  class File extends SimpleInput {
4
5
  getType() {
5
6
  return "file";
6
7
  }
7
- async renderFilePreview(_ctx, file_data) {
8
- return file_data?.filename || "";
8
+ async renderFilePreview(_ctx, file) {
9
+ const class_name = "file_preview";
10
+ const empty_container = `<div class="${class_name}"></div>`;
11
+ if (!file) {
12
+ return empty_container;
13
+ }
14
+ return (
15
+ /* HTML */
16
+ `<div class="${class_name}">
17
+ ${file.getOriginalFilename()}
18
+ </div>`
19
+ );
9
20
  }
10
- async preInput(ctx, data) {
11
- const { parsed } = await this.field.getValue(ctx, data);
12
- return tempstream`${this.renderFilePreview(ctx, parsed.old)}
21
+ async preInput(fctx, data) {
22
+ const { parsed } = await this.field.getValue(fctx.ctx, data);
23
+ return tempstream`${this.renderFilePreview(
24
+ fctx.ctx,
25
+ parsed.old
26
+ )}
13
27
  <input
14
28
  type="hidden"
15
- name="${this.field.name}[old][id]"
16
- value="${parsed.old?.id || ""}"
17
- />
18
- <input
19
- type="hidden"
20
- name="${this.field.name}[old][filename]"
21
- value="${parsed.old?.filename || ""}"
22
- />`;
29
+ name="${this.field.name}[old]"
30
+ value="${htmlEscape(
31
+ parsed.old?.token || await parsed.old?.save(false) || ""
32
+ )}"
33
+ form="${fctx.form_id}"
34
+ /> `;
23
35
  }
24
36
  async getInputAttributes(fctx) {
25
37
  const original = await super.getInputAttributes(fctx);
@@ -27,7 +39,7 @@ class File extends SimpleInput {
27
39
  return {
28
40
  ...original,
29
41
  name: this.field.name + "[new]",
30
- required: original.required && !parsed.old,
42
+ required: original.required && !parsed.new,
31
43
  value: false
32
44
  };
33
45
  }
@@ -1,11 +1,9 @@
1
- import {
2
- File as SealiousFile
3
- } from "sealious";
4
1
  import { inputWrapper } from "../../utils/input-wrapper.js";
5
2
  import { tempstream } from "tempstream";
6
3
  import { FormFieldControl } from "./form-field-control.js";
7
4
  import { is, predicates } from "@sealcode/ts-predicates";
8
5
  import { renderAttributes } from "../../utils/render-attributes.js";
6
+ import { PathFilePointer } from "@sealcode/file-manager";
9
7
  class MultipleFiles extends FormFieldControl {
10
8
  constructor(field, options) {
11
9
  super([field]);
@@ -43,10 +41,7 @@ class MultipleFiles extends FormFieldControl {
43
41
  }
44
42
  await Promise.all(
45
43
  files.map(
46
- (file) => this.addFileAssociation(
47
- ctx,
48
- file
49
- )
44
+ (file) => this.addFileAssociation(ctx, file)
50
45
  )
51
46
  );
52
47
  ctx.body = this.renderFrame(
@@ -76,7 +71,7 @@ class MultipleFiles extends FormFieldControl {
76
71
  getFrameID() {
77
72
  return `A${this.field.name}__multiple-fields-control`;
78
73
  }
79
- renderFrame(ctx, content) {
74
+ renderFrame(_ctx, content) {
80
75
  return tempstream`<turbo-frame
81
76
  ${content ? "" : `src="./${this.getFrameRelativeURL()}"`}
82
77
  id="${this.getFrameID()}"
@@ -107,15 +102,17 @@ class MultipleFiles extends FormFieldControl {
107
102
  if (!sealious_field) {
108
103
  return null;
109
104
  }
110
- return await SealiousFile.fromID(
111
- item.collection.app,
112
- item.get(sealious_field.name).id
105
+ return await item.collection.app.FileManager.fromToken(
106
+ item.get(sealious_field.name).token
113
107
  );
114
108
  }
115
109
  async renderFileItemPreview(_fileItem, file) {
116
- return file.filename || "no filename";
110
+ return file.getOriginalFilename();
117
111
  }
118
112
  async renderFileItem(fileItem, file) {
113
+ if (!(file instanceof PathFilePointer)) {
114
+ return "";
115
+ }
119
116
  return tempstream`<li class="file-list-item">
120
117
  <a
121
118
  class="file-list-item__preview"
@@ -1,3 +1,4 @@
1
+ import { PathFilePointer } from "@sealcode/file-manager";
1
2
  import { tempstream } from "tempstream";
2
3
  import { MultipleFiles } from "./multiple-files.js";
3
4
  class MultiplePhotos extends MultipleFiles {
@@ -13,7 +14,10 @@ class MultiplePhotos extends MultipleFiles {
13
14
  return { ...super.getInputAttributes(), accept: "image/*" };
14
15
  }
15
16
  async renderFileItem(fileItem, file) {
16
- const path = file.getDataPath();
17
+ if (!(file instanceof PathFilePointer)) {
18
+ return "";
19
+ }
20
+ const path = await file.getPath();
17
21
  if (!path) {
18
22
  return "missing file";
19
23
  }
@@ -24,7 +28,9 @@ class MultiplePhotos extends MultipleFiles {
24
28
  sizesAttr: "150px"
25
29
  })}
26
30
  </a>
27
- <span class="filename"> ${file.filename || "no filename"}</span>
31
+ <span class="filename">
32
+ ${await file.getOriginalFilename() || "no filename"}</span
33
+ >
28
34
  ${this.renderFileRemoveButton(fileItem)}
29
35
  </li>`;
30
36
  }
@@ -1,6 +1,5 @@
1
1
  import { tempstream } from "tempstream";
2
2
  import { File } from "./file.js";
3
- import { File as SealiousFile } from "sealious";
4
3
  class Photo extends File {
5
4
  constructor(field, imageRouter, options) {
6
5
  super(field, options);
@@ -10,19 +9,14 @@ class Photo extends File {
10
9
  async getImgStyle() {
11
10
  return "width: 100px; height: 100px; object-fit: contain";
12
11
  }
13
- async renderFilePreview(ctx, file_data) {
12
+ async renderFilePreview(ctx, file) {
14
13
  const class_name = "file_preview";
15
14
  const empty_container = `<div class="${class_name}"></div>`;
16
- if (!file_data) {
15
+ if (!file) {
17
16
  return empty_container;
18
17
  }
19
- const { id } = file_data;
20
- if (!id) {
21
- return empty_container;
22
- }
23
- const file = await SealiousFile.fromID(ctx.$app, id);
24
18
  return tempstream`<div class="${class_name}">
25
- ${this.imageRouter.image(file.getDataPath(), {
19
+ ${this.imageRouter.image(await file.getPath(), {
26
20
  resolutions: [100],
27
21
  sizesAttr: "100px",
28
22
  imgStyle: await this.getImgStyle()
@@ -0,0 +1,81 @@
1
+ import Router from "@koa/router";
2
+ import getPort from "get-port";
3
+ import Koa from "koa";
4
+ import { mount } from "../../mount.js";
5
+ import { Form } from "../form.js";
6
+ import { getBrowser } from "../../utils/browser-creator.js";
7
+ import { Fields } from "../../index.js";
8
+ import { Photo } from "./photo.js";
9
+ import { KoaResponsiveImageRouter } from "koa-responsive-image-router";
10
+ import _locreq from "locreq";
11
+ import { module_dirname } from "../../utils/module-dirname.js";
12
+ import { FileManager, PathFilePointer } from "@sealcode/file-manager";
13
+ const locreq = _locreq(module_dirname(import.meta.url));
14
+ describe("photo-controll", () => {
15
+ describe("basic tests", async () => {
16
+ let server;
17
+ let browser;
18
+ let page;
19
+ afterAll(() => {
20
+ server.close();
21
+ });
22
+ it("sends the old photo if it wasn't changed and it has an initial value", async () => {
23
+ const port = await getPort();
24
+ console.log(`Using port ${port} for photo.test.ts`);
25
+ const app = new Koa();
26
+ const router = new Router();
27
+ const fields = {
28
+ photo: new Fields.File(false, new FileManager("/tmp", "/uploaded_files"))
29
+ };
30
+ let received_photo;
31
+ const imageRouter = new KoaResponsiveImageRouter({
32
+ staticPath: "/images",
33
+ thumbnailSize: 20,
34
+ cacheManagerResolutionThreshold: 50,
35
+ imageStoragePath: `/tmp/image-cache-${port}`,
36
+ smartCropStoragePath: `/tmp/smartcrop-storage-${port}`
37
+ });
38
+ mount(
39
+ router,
40
+ "/",
41
+ new class extends Form {
42
+ constructor() {
43
+ super(...arguments);
44
+ this.fields = fields;
45
+ this.controls = [new Photo(fields.photo, imageRouter)];
46
+ }
47
+ async onSubmit(ctx) {
48
+ console.log("OnSubmit!");
49
+ const data = await this.getParsedValues(ctx);
50
+ console.log({ data });
51
+ received_photo = data.photo;
52
+ }
53
+ async getInitialValues(ctx) {
54
+ const file = new PathFilePointer(
55
+ new FileManager("/tmp", "/uploaded_files"),
56
+ locreq.resolve("assets/important.png")
57
+ );
58
+ return {
59
+ photo: await file.save(false)
60
+ // to get the token
61
+ };
62
+ }
63
+ async canAccess(_) {
64
+ return { canAccess: true, message: "" };
65
+ }
66
+ }(),
67
+ true
68
+ );
69
+ app.use(router.routes()).use(router.allowedMethods());
70
+ server = app.listen(port);
71
+ browser = await getBrowser();
72
+ const context = await browser.newContext();
73
+ page = await context.newPage();
74
+ await page.goto(`http://localhost:${port}`);
75
+ await page.getByRole("button", { name: "Wy\u015Blij" }).click();
76
+ console.log({ received_photo });
77
+ assert(received_photo !== void 0 && received_photo.old !== null);
78
+ });
79
+ });
80
+ });
81
+ //# sourceMappingURL=photo.test.js.map
@@ -74,7 +74,7 @@ class SimpleInput extends FormFieldControl {
74
74
  this.getWrapperModifiers(),
75
75
  tempstream`
76
76
  ${label ? `<label for="${id}">${label}</label>` : ""}
77
- ${await this.preInput(fctx.ctx, fctx.data)}
77
+ ${await this.preInput(fctx, fctx.data)}
78
78
  ${this.renderInput(
79
79
  fctx.ctx,
80
80
  renderAttributes(await this.getInputAttributes(fctx)),
@@ -1,25 +1,16 @@
1
- import {
2
- hasFieldOfType,
3
- hasShape,
4
- is,
5
- predicates
6
- } from "@sealcode/ts-predicates";
7
- import { File as SealiousFile } from "sealious";
1
+ import { hasFieldOfType, is, predicates } from "@sealcode/ts-predicates";
2
+ import { FilePointer } from "@sealcode/file-manager";
8
3
  import { FormField } from "./field.js";
9
4
  class File extends FormField {
5
+ constructor(required, file_manager) {
6
+ super(required);
7
+ this.required = required;
8
+ this.file_manager = file_manager;
9
+ }
10
10
  async parse(ctx, raw_value) {
11
11
  let result;
12
- if (raw_value instanceof SealiousFile || is(raw_value, predicates.object) && hasShape(
13
- {
14
- filename: predicates.string,
15
- id: predicates.string
16
- },
17
- raw_value
18
- ) && raw_value.id) {
19
- const file = await SealiousFile.fromID(
20
- ctx.$app,
21
- raw_value.id
22
- );
12
+ if (is(raw_value, predicates.string)) {
13
+ const file = await this.file_manager.fromToken(raw_value);
23
14
  result = {
24
15
  parsed_value: { old: file, new: null },
25
16
  error: null,
@@ -32,15 +23,12 @@ class File extends FormField {
32
23
  parsable: false
33
24
  };
34
25
  } else {
35
- const old_file = hasFieldOfType(raw_value, "old", predicates.object) && hasShape(
36
- { id: predicates.string, filename: predicates.string },
37
- raw_value.old
38
- ) && raw_value.old.id ? await SealiousFile.fromID(ctx.$app, raw_value.old.id) : null;
26
+ const old_file = hasFieldOfType(raw_value, "old", predicates.string) ? await this.file_manager.fromToken(raw_value.old) : null;
39
27
  const new_file = hasFieldOfType(
40
28
  raw_value,
41
29
  "new",
42
30
  predicates.array(predicates.object)
43
- ) && raw_value.new[0] instanceof SealiousFile ? raw_value.new[0] : null;
31
+ ) && raw_value.new[0] instanceof FilePointer ? raw_value.new[0] : null;
44
32
  result = {
45
33
  parsable: true,
46
34
  error: null,
@@ -10,7 +10,6 @@ export const actionName = "${action_name}";
10
10
 
11
11
  const actions = {
12
12
  add: (state: State, inputs: Record<string, string>) => {
13
- console.log({ inputs });
14
13
  return {
15
14
  ...state,
16
15
  elements: [...state.elements, inputs.element_to_add || "new element"],
@@ -0,0 +1,8 @@
1
+ import { dirname } from "node:path";
2
+ function module_dirname(module_url) {
3
+ return dirname(module_url).replace(/^file:\/\//, "");
4
+ }
5
+ export {
6
+ module_dirname
7
+ };
8
+ //# sourceMappingURL=module-dirname.js.map
@@ -7,8 +7,10 @@ function renderAttributes(attrs) {
7
7
  } else {
8
8
  return "";
9
9
  }
10
- } else {
10
+ } else if (value) {
11
11
  return `${key}="${attribute(value)}"`;
12
+ } else {
13
+ return "";
12
14
  }
13
15
  }).join(" ");
14
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.14.16",
3
+ "version": "0.15.1",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -52,9 +52,11 @@
52
52
  "types": "./@types/src/index.d.ts",
53
53
  "dependencies": {
54
54
  "@koa/router": "^12.0.1",
55
+ "@sealcode/file-manager": "^1.0.2",
55
56
  "@sealcode/ts-predicates": "^0.4.3",
56
57
  "deepmerge": "^4.3.1",
57
58
  "esbuild": "^0.20.0",
59
+ "escape-goat": "^4.0.0",
58
60
  "google-fonts-helper": "^3.4.1",
59
61
  "js-convert-case": "^4.2.0",
60
62
  "locreq": "^3.0.0",
@@ -71,6 +73,6 @@
71
73
  "peerDependencies": {
72
74
  "koa": "^2.13.0",
73
75
  "koa-responsive-image-router": "^0.2.24",
74
- "sealious": "^0.18.1"
76
+ "sealious": "^0.19.6"
75
77
  }
76
78
  }
@@ -3,8 +3,10 @@ import { exec, spawn } from "child_process";
3
3
  import axios from "axios";
4
4
  import waitPort from "wait-port";
5
5
  import getPort from "get-port";
6
+
6
7
  import _locreq from "locreq";
7
- const locreq = _locreq(__dirname);
8
+ import { module_dirname } from "./utils/module-dirname.js";
9
+ const locreq = _locreq(module_dirname(import.meta.url));
8
10
 
9
11
  const execAsync = (cmd: string) => {
10
12
  return new Promise<void>((resolve, reject) => {
@@ -193,7 +193,9 @@ export class StructuredArray<
193
193
  throw new Error("Missing action param");
194
194
  }
195
195
 
196
- const field_data = ctx.$body[this.field.name] || { data: [] };
196
+ const field_data = (ctx.$body as Record<string, unknown>)[
197
+ this.field.name
198
+ ] || { data: [] };
197
199
  console.log({ action: field_data });
198
200
  if (
199
201
  !hasShape(
@@ -1,9 +1,11 @@
1
1
  import { Context } from "koa";
2
+ import { FilePointer } from "@sealcode/file-manager";
2
3
  import { FlatTemplatable, tempstream } from "tempstream";
3
4
  import { FileContainer } from "../fields/file.js";
4
5
  import { FormDataValue } from "../form-types.js";
5
6
  import { FormControlContext } from "./form-control.js";
6
7
  import { SimpleInput } from "./simple-input.js";
8
+ import { htmlEscape } from "escape-goat";
7
9
 
8
10
  export class File extends SimpleInput<FileContainer> {
9
11
  getType() {
@@ -12,24 +14,35 @@ export class File extends SimpleInput<FileContainer> {
12
14
 
13
15
  async renderFilePreview(
14
16
  _ctx: Context,
15
- file_data: { id?: string; filename?: string } | null
17
+ file: FilePointer | null
16
18
  ): Promise<FlatTemplatable> {
17
- return file_data?.filename || "";
19
+ const class_name = "file_preview";
20
+ const empty_container = `<div class="${class_name}"></div>`;
21
+ if (!file) {
22
+ return empty_container;
23
+ }
24
+ return /* HTML */ `<div class="${class_name}">
25
+ ${file.getOriginalFilename()}
26
+ </div>`;
18
27
  }
19
28
 
20
- async preInput(ctx: Context, data: Record<string, FormDataValue>) {
21
- const { parsed } = await this.field.getValue(ctx, data);
22
- return tempstream/* HTML */ `${this.renderFilePreview(ctx, parsed.old)}
29
+ async preInput(
30
+ fctx: FormControlContext,
31
+ data: Record<string, FormDataValue>
32
+ ) {
33
+ const { parsed } = await this.field.getValue(fctx.ctx, data);
34
+ return tempstream/* HTML */ `${this.renderFilePreview(
35
+ fctx.ctx,
36
+ parsed.old
37
+ )}
23
38
  <input
24
39
  type="hidden"
25
- name="${this.field.name}[old][id]"
26
- value="${parsed.old?.id || ""}"
27
- />
28
- <input
29
- type="hidden"
30
- name="${this.field.name}[old][filename]"
31
- value="${parsed.old?.filename || ""}"
32
- />`;
40
+ name="${this.field.name}[old]"
41
+ value="${htmlEscape(
42
+ parsed.old?.token || (await parsed.old?.save(false)) || ""
43
+ )}"
44
+ form="${fctx.form_id}"
45
+ /> `;
33
46
  }
34
47
 
35
48
  async getInputAttributes(
@@ -40,7 +53,7 @@ export class File extends SimpleInput<FileContainer> {
40
53
  return {
41
54
  ...original,
42
55
  name: this.field.name + "[new]",
43
- required: original.required && !parsed.old,
56
+ required: original.required && !parsed.new,
44
57
  value: false,
45
58
  };
46
59
  }
@@ -1,10 +1,5 @@
1
1
  import Router from "@koa/router";
2
- import {
3
- CollectionItem,
4
- File as SealiousFile,
5
- Field as SealiousField,
6
- Collection,
7
- } from "sealious";
2
+ import { CollectionItem, Field as SealiousField } from "sealious";
8
3
  import { Context } from "koa";
9
4
  import { inputWrapper } from "../../utils/input-wrapper.js";
10
5
  import { FlatTemplatable, tempstream } from "tempstream";
@@ -13,13 +8,14 @@ import { FormFieldControl } from "./form-field-control.js";
13
8
  import { is, predicates } from "@sealcode/ts-predicates";
14
9
  import { renderAttributes } from "../../utils/render-attributes.js";
15
10
  import { FormControlContext } from "./form-control.js";
11
+ import { FilePointer, PathFilePointer } from "@sealcode/file-manager";
16
12
 
17
13
  export type MultipleFilesOptions = {
18
14
  label?: string;
19
15
  uploadLabel?: string;
20
16
  getAdditionalFields: (
21
17
  ctx: Context,
22
- file: SealiousFile
18
+ file: FilePointer
23
19
  ) => Promise<Record<string, unknown>>;
24
20
  };
25
21
 
@@ -57,7 +53,7 @@ export class MultipleFiles extends FormFieldControl {
57
53
  );
58
54
 
59
55
  router.post(this.getFrameRelativeURL() + "/add", async (ctx) => {
60
- let files = ctx.$body.files as SealiousFile | SealiousFile[];
56
+ let files = ctx.$body.files as FilePointer | FilePointer[];
61
57
  if (!files || !is(files, predicates.object)) {
62
58
  ctx.body = "Missing files";
63
59
  return;
@@ -67,10 +63,7 @@ export class MultipleFiles extends FormFieldControl {
67
63
  }
68
64
  await Promise.all(
69
65
  files.map((file) =>
70
- this.addFileAssociation(
71
- ctx,
72
- file as unknown as SealiousFile
73
- )
66
+ this.addFileAssociation(ctx, file as unknown as FilePointer)
74
67
  )
75
68
  );
76
69
  ctx.body = this.renderFrame(
@@ -86,7 +79,7 @@ export class MultipleFiles extends FormFieldControl {
86
79
  ].removeByID(ctx.$context, file_item_id);
87
80
  }
88
81
 
89
- async addFileAssociation(ctx: Context, file: SealiousFile) {
82
+ async addFileAssociation(ctx: Context, file: FilePointer) {
90
83
  const file_field = this.getFileField();
91
84
  if (!file_field) {
92
85
  throw new Error("No file field in referencing collection");
@@ -111,7 +104,7 @@ export class MultipleFiles extends FormFieldControl {
111
104
  return `A${this.field.name}__multiple-fields-control`;
112
105
  }
113
106
 
114
- renderFrame(ctx: Context, content?: FlatTemplatable) {
107
+ renderFrame(_ctx: Context, content?: FlatTemplatable) {
115
108
  return tempstream/* HTML */ `<turbo-frame
116
109
  ${content ? "" : `src="./${this.getFrameRelativeURL()}"`}
117
110
  id="${this.getFrameID()}"
@@ -148,28 +141,30 @@ export class MultipleFiles extends FormFieldControl {
148
141
 
149
142
  async extractFileFromItem(
150
143
  item: CollectionItem
151
- ): Promise<SealiousFile | null> {
144
+ ): Promise<FilePointer | null> {
152
145
  const sealious_field = this.getFileField();
153
146
  if (!sealious_field) {
154
147
  return null;
155
148
  }
156
- return await SealiousFile.fromID(
157
- (item.collection as Collection).app,
158
- item.get(sealious_field.name).id
149
+ return await item.collection.app.FileManager.fromToken(
150
+ item.get(sealious_field.name).token
159
151
  );
160
152
  }
161
153
 
162
154
  async renderFileItemPreview(
163
155
  _fileItem: CollectionItem,
164
- file: SealiousFile
156
+ file: FilePointer
165
157
  ): Promise<FlatTemplatable> {
166
- return file.filename || "no filename";
158
+ return file.getOriginalFilename();
167
159
  }
168
160
 
169
161
  async renderFileItem(
170
162
  fileItem: CollectionItem,
171
- file: SealiousFile
163
+ file: FilePointer
172
164
  ): Promise<FlatTemplatable> {
165
+ if (!(file instanceof PathFilePointer)) {
166
+ return "";
167
+ }
173
168
  return tempstream/* HTML */ `<li class="file-list-item">
174
169
  <a
175
170
  class="file-list-item__preview"
@@ -226,7 +221,7 @@ export class MultipleFiles extends FormFieldControl {
226
221
  await this.extractFileFromItem(item),
227
222
  ])
228
223
  )
229
- ).filter(([_, f]) => f !== null) as [CollectionItem, SealiousFile][];
224
+ ).filter(([_, f]) => f !== null) as [CollectionItem, FilePointer][];
230
225
 
231
226
  return inputWrapper(
232
227
  ["multiple-files", this.field.name, ...this.getClassModifiers()],
@@ -1,4 +1,5 @@
1
- import { File as SealiousFile, CollectionItem } from "sealious";
1
+ import { CollectionItem } from "sealious";
2
+ import { FilePointer, PathFilePointer } from "@sealcode/file-manager";
2
3
  import { FlatTemplatable, tempstream } from "tempstream";
3
4
  import { MultipleFiles, MultipleFilesOptions } from "./multiple-files.js";
4
5
  import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files.js";
@@ -23,9 +24,12 @@ export class MultiplePhotos extends MultipleFiles {
23
24
 
24
25
  async renderFileItem(
25
26
  fileItem: CollectionItem,
26
- file: SealiousFile
27
+ file: FilePointer
27
28
  ): Promise<FlatTemplatable> {
28
- const path = file.getDataPath();
29
+ if (!(file instanceof PathFilePointer)) {
30
+ return "";
31
+ }
32
+ const path = await file.getPath();
29
33
  if (!path) {
30
34
  return "missing file";
31
35
  }
@@ -36,7 +40,9 @@ export class MultiplePhotos extends MultipleFiles {
36
40
  sizesAttr: "150px",
37
41
  })}
38
42
  </a>
39
- <span class="filename"> ${file.filename || "no filename"}</span>
43
+ <span class="filename">
44
+ ${(await file.getOriginalFilename()) || "no filename"}</span
45
+ >
40
46
  ${this.renderFileRemoveButton(fileItem)}
41
47
  </li>`;
42
48
  }
@@ -0,0 +1,90 @@
1
+ import Router from "@koa/router";
2
+ import getPort from "get-port";
3
+ import { Browser, Page } from "playwright";
4
+ import type { Context } from "koa";
5
+ import Koa from "koa";
6
+ import { mount } from "../../mount.js";
7
+ import { Form } from "../form.js";
8
+ import { getBrowser } from "../../utils/browser-creator.js";
9
+ import { Fields } from "../../index.js";
10
+ import { Photo } from "./photo.js";
11
+ import { KoaResponsiveImageRouter } from "koa-responsive-image-router";
12
+
13
+ import _locreq from "locreq";
14
+ import { module_dirname } from "../../utils/module-dirname.js";
15
+ import { FileManager, PathFilePointer } from "@sealcode/file-manager";
16
+ const locreq = _locreq(module_dirname(import.meta.url));
17
+
18
+ describe("photo-controll", () => {
19
+ describe("basic tests", async () => {
20
+ let server: ReturnType<Koa["listen"]>;
21
+ let browser: Browser;
22
+ let page: Page;
23
+
24
+ afterAll(() => {
25
+ server.close();
26
+ });
27
+
28
+ it("sends the old photo if it wasn't changed and it has an initial value", async () => {
29
+ const port = await getPort();
30
+ console.log(`Using port ${port} for photo.test.ts`);
31
+ const app = new Koa();
32
+ const router = new Router();
33
+
34
+ const fields = {
35
+ photo: new Fields.File(false, new FileManager("/tmp", "/uploaded_files")),
36
+ };
37
+
38
+ let received_photo: unknown;
39
+
40
+ const imageRouter = new KoaResponsiveImageRouter({
41
+ staticPath: "/images",
42
+ thumbnailSize: 20,
43
+ cacheManagerResolutionThreshold: 50,
44
+ imageStoragePath: `/tmp/image-cache-${port}`,
45
+ smartCropStoragePath: `/tmp/smartcrop-storage-${port}`,
46
+ });
47
+
48
+ mount(
49
+ router,
50
+ "/",
51
+ new (class extends Form<typeof fields, unknown> {
52
+ fields = fields;
53
+ controls = [new Photo(fields.photo, imageRouter)];
54
+
55
+ async onSubmit(ctx: Context) {
56
+ console.log("OnSubmit!");
57
+ const data = await this.getParsedValues(ctx);
58
+ console.log({ data });
59
+ received_photo = data.photo;
60
+ }
61
+
62
+ async getInitialValues(ctx: Context) {
63
+ const file = new PathFilePointer(
64
+ new FileManager("/tmp", "/uploaded_files"),
65
+ locreq.resolve("assets/important.png")
66
+ );
67
+ return {
68
+ photo: await file.save(false), // to get the token
69
+ };
70
+ }
71
+
72
+ async canAccess(_: Context) {
73
+ return { canAccess: true, message: "" };
74
+ }
75
+ })(),
76
+ true
77
+ );
78
+ app.use(router.routes()).use(router.allowedMethods());
79
+ server = app.listen(port);
80
+
81
+ browser = await getBrowser();
82
+ const context = await browser.newContext();
83
+ page = await context.newPage();
84
+ await page.goto(`http://localhost:${port}`);
85
+ await page.getByRole("button", { name: "Wyślij" }).click();
86
+ console.log({ received_photo });
87
+ assert(received_photo !== undefined && (received_photo as any).old !== null);
88
+ });
89
+ });
90
+ });
@@ -1,12 +1,12 @@
1
1
  import { Context } from "koa";
2
2
  import { FlatTemplatable, tempstream } from "tempstream";
3
3
  import { File } from "./file.js";
4
- import { File as SealiousFile } from "sealious";
5
4
  import { FormField } from "../fields/field.js";
6
5
  import { SimpleInputOptions } from "./simple-input.js";
7
6
  import { FormControlContext } from "./form-control.js";
8
7
  import { FileContainer } from "../fields/file.js";
9
8
  import { KoaResponsiveImageRouter } from "koa-responsive-image-router";
9
+ import { FilePointer } from "@sealcode/file-manager";
10
10
 
11
11
  export class Photo extends File {
12
12
  constructor(
@@ -23,20 +23,15 @@ export class Photo extends File {
23
23
 
24
24
  async renderFilePreview(
25
25
  ctx: Context,
26
- file_data: { id?: string; filename?: string } | null
26
+ file: FilePointer | null
27
27
  ): Promise<FlatTemplatable> {
28
28
  const class_name = "file_preview";
29
29
  const empty_container = `<div class="${class_name}"></div>`;
30
- if (!file_data) {
30
+ if (!file) {
31
31
  return empty_container;
32
32
  }
33
- const { id } = file_data;
34
- if (!id) {
35
- return empty_container;
36
- }
37
- const file = await SealiousFile.fromID(ctx.$app, id);
38
33
  return tempstream/* HTML */ `<div class="${class_name}">
39
- ${this.imageRouter.image(file.getDataPath(), {
34
+ ${this.imageRouter.image(await file.getPath(), {
40
35
  resolutions: [100],
41
36
  sizesAttr: "100px",
42
37
  imgStyle: await this.getImgStyle(),
@@ -47,7 +47,7 @@ export class SimpleInput<
47
47
  }
48
48
 
49
49
  async preInput(
50
- _ctx: Context,
50
+ _ctx: FormControlContext,
51
51
  _data: Record<string, FormDataValue>
52
52
  ): Promise<FlatTemplatable> {
53
53
  return "";
@@ -140,7 +140,7 @@ export class SimpleInput<
140
140
  this.getWrapperModifiers(),
141
141
  tempstream/* HTML */ `
142
142
  ${label ? `<label for="${id}">${label}</label>` : ""}
143
- ${await this.preInput(fctx.ctx, fctx.data)}
143
+ ${await this.preInput(fctx, fctx.data)}
144
144
  ${this.renderInput(
145
145
  fctx.ctx,
146
146
  renderAttributes(await this.getInputAttributes(fctx)),
@@ -1,44 +1,32 @@
1
- import {
2
- hasFieldOfType,
3
- hasShape,
4
- is,
5
- predicates,
6
- } from "@sealcode/ts-predicates";
1
+ import { hasFieldOfType, is, predicates } from "@sealcode/ts-predicates";
7
2
  import { Context } from "koa";
8
- import { File as SealiousFile } from "sealious";
3
+ import { FileManager, FilePointer } from "@sealcode/file-manager";
9
4
  import { FormDataValue } from "../form-types.js";
10
5
  import { FieldParseResult, FormField } from "./field.js";
11
6
 
12
7
  export type FileContainer = {
13
- old: SealiousFile | null;
14
- new: SealiousFile | null;
8
+ old: FilePointer | null;
9
+ new: FilePointer | null;
15
10
  };
16
11
 
17
12
  export class File<Required extends boolean> extends FormField<
18
13
  Required,
19
14
  FileContainer
20
15
  > {
16
+ constructor(
17
+ public readonly required: Required,
18
+ public file_manager: FileManager
19
+ ) {
20
+ super(required);
21
+ }
22
+
21
23
  async parse(
22
24
  ctx: Context,
23
25
  raw_value: FormDataValue
24
26
  ): Promise<FieldParseResult<FileContainer>> {
25
27
  let result;
26
- if (
27
- raw_value instanceof SealiousFile ||
28
- (is(raw_value, predicates.object) &&
29
- hasShape(
30
- {
31
- filename: predicates.string,
32
- id: predicates.string,
33
- },
34
- raw_value
35
- ) &&
36
- raw_value.id)
37
- ) {
38
- const file = await SealiousFile.fromID(
39
- ctx.$app,
40
- raw_value.id as string
41
- );
28
+ if (is(raw_value, predicates.string)) {
29
+ const file = await this.file_manager.fromToken(raw_value);
42
30
  result = {
43
31
  parsed_value: { old: file, new: null },
44
32
  error: null,
@@ -51,21 +39,15 @@ export class File<Required extends boolean> extends FormField<
51
39
  parsable: false as false,
52
40
  };
53
41
  } else {
54
- const old_file =
55
- hasFieldOfType(raw_value, "old", predicates.object) &&
56
- hasShape(
57
- { id: predicates.string, filename: predicates.string },
58
- raw_value.old
59
- ) &&
60
- raw_value.old.id
61
- ? await SealiousFile.fromID(ctx.$app, raw_value.old.id)
62
- : null;
42
+ const old_file = hasFieldOfType(raw_value, "old", predicates.string)
43
+ ? await this.file_manager.fromToken(raw_value.old)
44
+ : null;
63
45
  const new_file =
64
46
  hasFieldOfType(
65
47
  raw_value,
66
48
  "new",
67
49
  predicates.array(predicates.object)
68
- ) && raw_value.new[0] instanceof SealiousFile
50
+ ) && raw_value.new[0] instanceof FilePointer
69
51
  ? raw_value.new[0]
70
52
  : null;
71
53
  result = {
@@ -1,4 +1,4 @@
1
- import type { File as SealiousFile } from "sealious";
1
+ import { FilePointer } from "@sealcode/file-manager";
2
2
  import type { FlatTemplatable } from "tempstream";
3
3
  import type { FormField } from "./fields/field.js";
4
4
 
@@ -6,7 +6,7 @@ export type FormDataPrimitive = string | string[] | number | undefined;
6
6
  export type FormDataValue =
7
7
  | FormDataPrimitive
8
8
  | Record<string, FormDataPrimitive>
9
- | SealiousFile;
9
+ | FilePointer;
10
10
 
11
11
  export type FieldValueType<F extends FormField> = F extends FormField<infer R>
12
12
  ? R
@@ -14,7 +14,6 @@ export const actionName = "${action_name}";
14
14
 
15
15
  const actions = {
16
16
  add: (state: State, inputs: Record<string, string>) => {
17
- console.log({ inputs });
18
17
  return {
19
18
  ...state,
20
19
  elements: [...state.elements, inputs.element_to_add || "new element"],
@@ -0,0 +1,5 @@
1
+ import { dirname } from "node:path";
2
+
3
+ export function module_dirname(module_url: string): string {
4
+ return dirname(module_url).replace(/^file:\/\//, "");
5
+ }
@@ -9,8 +9,10 @@ export function renderAttributes(attrs: Record<string, string | boolean>) {
9
9
  } else {
10
10
  return "";
11
11
  }
12
- } else {
12
+ } else if (value) {
13
13
  return `${key}="${attribute(value)}"`;
14
+ } else {
15
+ return "";
14
16
  }
15
17
  })
16
18
  .join(" ");