import Router from "@koa/router"; import { Collection, CollectionItem, Field as SealiousField } from "sealious"; import { Context } from "koa"; import { inputWrapper } from "../../utils/input-wrapper.js"; import { FlatTemplatable, tempstream } from "tempstream"; import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files.js"; import { FormFieldControl } from "./form-field-control.js"; import { is, predicates } from "@sealcode/ts-predicates"; import { renderAttributes } from "../../utils/render-attributes.js"; import { FormControlContext } from "./form-control.js"; import { FilePointer, PathFilePointer } from "@sealcode/file-manager"; export type MultipleFilesOptions = { label?: string; uploadLabel?: string; getAdditionalFields: ( ctx: Context, file: FilePointer ) => Promise>; }; export class MultipleFiles extends FormFieldControl { public options: MultipleFilesOptions; constructor( public field: MultipleFilesField, options?: Partial ) { super([field]); this.options = { getAdditionalFields: async () => ({}), ...options }; } getClassModifiers(): string[] { return []; } mount(router: Router) { router.get(this.getFrameRelativeURL(), async (ctx) => { ctx.body = this.renderFrame( ctx, await this.renderFrameContent(ctx) ); }); router.post( this.getFrameRelativeURL() + "/delete/:file_item_id", async (ctx) => { await this.deleteFileAssociation(ctx, ctx.params.file_item_id); ctx.body = this.renderFrame( ctx, await this.renderFrameContent(ctx) ); } ); router.post(this.getFrameRelativeURL() + "/add", async (ctx) => { let files = ctx.$body.files as FilePointer | FilePointer[]; if (!files || !is(files, predicates.object)) { ctx.body = "Missing files"; return; } if (!Array.isArray(files)) { files = [files]; } await Promise.all( files.map((file) => this.addFileAssociation(ctx, file as unknown as FilePointer) ) ); ctx.body = this.renderFrame( ctx, await this.renderFrameContent(ctx) ); }); } async deleteFileAssociation(ctx: Context, file_item_id: string) { await ctx.$app.collections[ this.field.collection_field.referencing_collection ].removeByID(ctx.$context, file_item_id); } async addFileAssociation(ctx: Context, file: FilePointer) { const file_field = this.getFileField(); if (!file_field) { throw new Error("No file field in referencing collection"); } const body = { ...(await this.options.getAdditionalFields(ctx, file)), [this.field.collection_field.referencing_field]: await this.field.getItemId(ctx), [file_field.name]: file, }; await ctx.$app.collections[ this.field.collection_field.referencing_collection ].create(ctx.$context, body); } getFrameRelativeURL() { return `${this.field.name}_files`; } getFrameID(): string { // the "A" is necessary here return `A${this.field.name}__multiple-fields-control`; } renderFrame(_ctx: Context, content?: FlatTemplatable) { return tempstream /* HTML */ ` ${content || ""} `; } render( fctx: FormControlContext ): FlatTemplatable | Promise { return this.renderFrame(fctx.ctx, ""); } getReferencingCollection() { const result = this.field.collection_field.app.collections[ this.field.collection_field.referencing_collection ]; return result; } // eslint-disable-next-line @typescript-eslint/no-explicit-any getFileField(): SealiousField | null { for (const [_, field] of Object.entries( this.getReferencingCollection().fields )) { if (field.handles_large_data) { return field; } } return null; } async extractFileFromItem( item: CollectionItem ): Promise { const sealious_field = this.getFileField(); if (!sealious_field) { return null; } const token = (item.get(sealious_field.name) as FilePointer).token; if (!token) { return null; } return await item.collection.app.fileManager.fromToken(token); } async renderFileItemPreview( _fileItem: CollectionItem, file: FilePointer ): Promise { return file.getOriginalFilename(); } async renderFileItem( fileItem: CollectionItem, file: FilePointer ): Promise { if (!(file instanceof PathFilePointer)) { return ""; } return tempstream /* HTML */ `
  • ${this.renderFileItemPreview(fileItem, file)} ${this.renderFileRemoveButton(fileItem)}
  • `; } renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable { return /* HTML */ `
    `; } async getFileItems(ctx: Context) { const item_id = await this.field.getItemId(ctx); const { items: [item], } = await this.field.collection_field.collection .list(ctx.$context) .ids([item_id]) .attach({ [this.field.collection_field.name]: true }) .fetch(); return item .getAttachments(this.field.collection_field.name) .filter((f) => f); } getInputAttributes() { return { type: "file", name: "files", multiple: true }; } async renderFrameContent(ctx: Context): Promise { const files = ( await Promise.all( (await this.getFileItems(ctx)) .filter((item) => item) // filter out undefineds .map(async (item: CollectionItem) => [ item, await this.extractFileFromItem(item), ]) ) ).filter(([_, f]) => f !== null) as [ CollectionItem, FilePointer, ][]; return inputWrapper( "multiple-files", ["multiple-files", this.field.name, ...this.getClassModifiers()], tempstream /* HTML */ `
      ${files.map(([item, file]) => this.renderFileItem(item, file) )}
    ` ); } setLabel(label: string) { this.options.label = label; } setUploadLabel(label: string) { this.options.uploadLabel = label; } setAdditionalFieldsGetter( fn: ( ctx: Context, file: FilePointer ) => Promise> ) { this.options.getAdditionalFields = fn; } }