Page MenuHomeSealhub

multiple-files.ts
No OneTemporary

multiple-files.ts

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<Record<string, unknown>>;
};
export class MultipleFiles extends FormFieldControl {
public options: MultipleFilesOptions;
constructor(
public field: MultipleFilesField,
options?: Partial<MultipleFilesOptions>
) {
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 */ `<turbo-frame
${content ? "" : `src="./${this.getFrameRelativeURL()}"`}
id="${this.getFrameID()}"
target="_top"
>
${content || ""}
</turbo-frame>`;
}
render(
fctx: FormControlContext
): FlatTemplatable | Promise<FlatTemplatable> {
return this.renderFrame(fctx.ctx, "");
}
getReferencingCollection() {
const result =
this.field.collection_field.app.collections[
this.field.collection_field.referencing_collection
];
return result;
}
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<Collection>
): Promise<FilePointer | null> {
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<FlatTemplatable> {
return file.getOriginalFilename();
}
async renderFileItem(
fileItem: CollectionItem,
file: FilePointer
): Promise<FlatTemplatable> {
if (!(file instanceof PathFilePointer)) {
return "";
}
return tempstream/* HTML */ `<li class="file-list-item">
<a
class="file-list-item__preview"
href="${file.getURL()}"
data-turbo="false"
>
${this.renderFileItemPreview(fileItem, file)}
</a>
${this.renderFileRemoveButton(fileItem)}
</li>`;
}
renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable {
return /* HTML */ ` <form
data-turbo-frame="${this.getFrameID()}"
action="${this.getFrameRelativeURL()}/delete/${fileItem.id}"
method="POST"
>
<input
type="submit"
value="X"
class="file-list-action file-list-item__button file-list-item__button--delete"
/>
</form>`;
}
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<FlatTemplatable> {
const files = (
await Promise.all(
(
await this.getFileItems(ctx)
)
.filter((item) => item) // filter out undefineds
.map(async (item: CollectionItem<Collection>) => [
item,
await this.extractFileFromItem(item),
])
)
).filter(([_, f]) => f !== null) as [
CollectionItem<Collection>,
FilePointer
][];
return inputWrapper(
["multiple-files", this.field.name, ...this.getClassModifiers()],
tempstream/* HTML */ `
<label>${this.options.label || this.field.name}</label>
<ul class="multiple-files__list">
${files.map(([item, file]) =>
this.renderFileItem(item, file)
)}
</ul>
<form
action="${this.getFrameRelativeURL()}/add"
method="POST"
enctype="multipart/form-data"
data-turbo-frame="${this.getFrameID()}"
>
<input ${renderAttributes(this.getInputAttributes())} />
<input
type="submit"
value="${this.options.uploadLabel || "Upload"}"
class="file-list-action"
/>
</form>
`
);
}
}

File Metadata

Mime Type
text/x-java
Expires
Thu, Jan 23, 19:19 (20 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
601477
Default Alt Text
multiple-files.ts (6 KB)

Event Timeline