wcz-layout
Version:
151 lines (122 loc) • 6.06 kB
Markdown
# file
## Client
`import { ... } from "wcz-layout/data/client";`
- Query:
`fileMetasQueryOptions(GetFileMetasSchema)` → `FileMeta[]` // metadata of all files under `subId`
`fileThumbnailQueryOptions(FileSchema)` → `string` // thumbnail as object URL, disabled while `id` is empty
`fileQueryOptions(FileSchema)` → `string` // file content as object URL for preview, disabled while `id` is empty
- Mutation:
`downloadFileMutationOptions()` → `mutate(FileSchema & { fileName, fileExtension })` // downloads and saves as `fileName.fileExtension`
`openFileMutationOptions({ target })` → `mutate(FileSchema)` // opens file in new tab; `target` is a `useRef<Window | null>`, open `about:blank` into it synchronously in the click handler to avoid popup blockers
`updateFileMetaMutationOptions({ queryClient })` → `mutate(UpdateFileMetaSchema)` // update metadata (e.g. rename `fileName`), invalidates metas
`deleteFileMutationOptions({ queryClient })` → `mutate(FileSchema)` // delete single file, invalidates metas
`batchDeleteFilesMutationOptions({ queryClient })` → `mutate(GetFileMetasSchema)` // delete all files under `subId`, invalidates metas
`uploadFileMutationOptions({ queryClient })` → `mutate(UploadFileMetaSchema & { setProgress? })` → `{ id, appName, subId, fileName }` // uploads directly from browser via TUS (1 MB chunks), `setProgress(percent)` for a progress bar, invalidates metas of `subId`
## Server
`import { ... } from "wcz-layout/data/server";`
- Fetch:
`getFileMetas({ data: GetFileMetasSchema })` → `FileMeta[]` // metadata of all files under `subId`
`getFileThumbnail({ data: FileSchema })` → `Response` // thumbnail binary stream
`getFile({ data: FileSchema })` → `Response` // file binary stream, `Content-Disposition: inline`
`downloadFile({ data: FileSchema })` → `Response` // file binary stream for download
- Mutate:
`updateFileMeta({ data: UpdateFileMetaSchema })` → `void` // update metadata
`deleteFile({ data: FileSchema })` → `void` // delete single file
`batchDeleteFiles({ data: GetFileMetasSchema })` → `void` // delete all files under `subId`
`uploadFile({ data: UploadFileMetaSchema })` → `{ id, appName, subId, fileName }` // server-originated uploads only, client uploads go direct via `uploadFileMutationOptions`
## Components
`import { Dropzone, FileViewer } from "wcz-layout/components";`
- `<Dropzone onDrop progress? sx? {...DropzoneOptions} />` // react-dropzone area (`accept`, `maxFiles`, `disabled`, `onDropAccepted`, …), `progress` (0-100) shows upload progress
- `<FileViewer appName subId actions? onDelete{(component) => <component.Grid /> | <component.List />}</FileViewer>` // fetches metas itself, renders nothing when there are no files
- `actions?: { download?: boolean, delete?: boolean }` // both enabled by default
- `onDelete?: ({ remainingFileMetas: FileMeta[], deletedFileMeta: FileMeta }) => void`
- `component.Grid` props: `size?: number` (default 150), `itemBar?: "hidden" | "always" | "onMouseEnter"` (default on hover), `sx?`
- `component.List` props: `sx?`
## Schemas/Models
```ts
const FileMetaSchema = z.object({
id: z.uuid(),
subId: z.uuid(),
appName: z.string().trim().min(1).max(255),
fileName: z.string().trim().min(1).max(255),
fileExtension: z.string().trim().min(1).max(255),
fileSize: z.number().min(0),
mediaSubType: z.string().trim().min(1).max(255),
mediaType: z.string().trim().min(1).max(255),
mimeType: z.string().trim().min(1).max(255),
createdBy: z.string().trim().min(1).max(255),
createdDate: z.date(),
});
const GetFileMetasSchema = z.object({
appName: z.string().min(1).max(255),
subId: z.uuid(),
});
const FileSchema = z.object({
appName: z.string().min(1).max(255),
id: z.uuid(),
});
const UpdateFileMetaSchema = FileMetaSchema.extend({
fileName: z.string().min(1).max(255),
});
const UploadFileMetaSchema = z.object({
id: z.uuid().default(uuidv7),
appName: z.string().min(1).max(255),
subId: z.uuid(),
file: z.instanceof(File),
});
```
## Examples
```tsx
import { useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
deleteFileMutationOptions,
downloadFileMutationOptions,
fileMetasQueryOptions,
fileQueryOptions,
openFileMutationOptions,
uploadFileMutationOptions,
} from "wcz-layout/data/client";
import { Dropzone, FileViewer } from "wcz-layout/components";
import { useInView } from "wcz-layout/hooks";
const { queryClient } = Route.useRouteContext();
// fetch metas (lazily when in view) + fetch first file
const { ref, inView } = useInView({ triggerOnce: true });
const { data: fileMetas = [] } = useQuery({
...fileMetasQueryOptions({ appName: packageJson.name, subId }),
enabled: inView,
});
const { data: file } = useQuery({
...fileQueryOptions({ appName: packageJson.name, id: fileMetas[0]?.id ?? "" }),
enabled: !!fileMetas[0],
});
// upload via Dropzone
const [progress, setProgress] = useState(0);
const { mutateAsync: uploadFile } = useMutation(uploadFileMutationOptions({ queryClient }));
const onDrop = async (acceptedFiles: File[]) => {
await Promise.allSettled(
acceptedFiles.map((file) =>
uploadFile({ appName: packageJson.name, subId, file, setProgress }),
),
);
setProgress(0);
};
<Dropzone onDrop={onDrop} progress={progress} />;
// download / delete
const { mutate: download } = useMutation(downloadFileMutationOptions());
const { mutate: remove } = useMutation(deleteFileMutationOptions({ queryClient }));
download({
appName: packageJson.name,
id: meta.id,
fileName: meta.fileName,
fileExtension: meta.fileExtension,
});
remove({ appName: packageJson.name, id: meta.id });
// open file
const target = useRef<Window | null>(null);
const { mutate: openFile } = useMutation(openFileMutationOptions({ target }));
// File Viewer - no manual fetch of metas needed
<FileViewer appName={packageJson.name} subId={subId}>
{(component) => <component.Grid />} // or component.List
</FileViewer>;
```