feat: add search bar in cdn chooser

This commit is contained in:
2026-03-08 13:00:21 +07:00 Unverified
parent 3f2779c8d2
commit 48cfd4fbc4
3 changed files with 65 additions and 18 deletions
+7
View File
@@ -76,6 +76,7 @@ const columns: ColumnDef<{
export function CdnTable({
files,
onChoose,
onChange,
}: {
files: {
id: string;
@@ -89,6 +90,7 @@ export function CdnTable({
size: string;
} | null,
) => void;
onChange?: () => void;
}) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@@ -108,6 +110,7 @@ export function CdnTable({
toast.promise(cdnify(file), {
loading: `Uploading ${file.name}...`,
success: () => {
onChange?.();
return `${file.name} has been uploaded to CDN.`;
},
error: `Error uploading ${file.name}.`,
@@ -130,6 +133,7 @@ export function CdnTable({
throw "";
}
setRowSelection({});
onChange?.();
return `${selectedIds.length} files has been deleted.`;
},
error: `Error deleting the files.`,
@@ -145,6 +149,7 @@ export function CdnTable({
toast.promise(fetchToCdn(urlList), {
loading: `Fetching ${urlList.length} files...`,
success: () => {
onChange?.();
return `${urlList.length} URLs has been uploaded to CDN.`;
},
error: "Error fetching the files.",
@@ -201,6 +206,7 @@ export function CdnTable({
toast.promise(rename(row.id, newName), {
success() {
onChange?.();
return "เปลี่ยนชื่อสำเร็จ";
},
error() {
@@ -219,6 +225,7 @@ export function CdnTable({
const file = files.find((f) => f.id === res.id);
throw `${file ? file.name || `[${res.id}]` : `[${res.id}]`} ยังถูกใช้โดย ${res.refs.join(", ")}`;
}
onChange?.();
return "ลบสำเร็จ";
},
error(err: { message: string; description: string }) {
+1 -2
View File
@@ -9,6 +9,5 @@ export async function listFiles() {
return await db
.select({ id: cdn.id, name: cdn.name, size: cdn.size, type: cdn.type })
.from(cdn)
.orderBy(cdn.name);
.from(cdn);
}
+57 -16
View File
@@ -1,9 +1,10 @@
/** biome-ignore-all lint/a11y/noStaticElementInteractions: yes maam */
"use client";
import { FileSearch2, X } from "lucide-react";
import { FileSearch2, Search, X } from "lucide-react";
import {
type ComponentProps,
useCallback,
useContext,
useEffect,
useRef,
@@ -15,6 +16,11 @@ import { cn } from "@/lib/utils";
import { SimpleTooltip } from "../tooltip";
import { Button } from "../ui/button";
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "../ui/input-group";
import { Kbd } from "../ui/kbd";
import { Spinner } from "../ui/spinner";
import { listFiles } from "./api";
@@ -25,24 +31,44 @@ export function CdnChooserProvider({
}: {
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(true);
const [files, setFiles] = useState<
Omit<typeof cdn.$inferSelect, "data">[] | null
>(null);
const [options, setOptions] = useState<{ type?: string }>();
const callback = useRef<((value: CdnFile | null) => void) | null>(null);
const [search, setSearch] = useState("");
async function call(options?: { type?: string }): Promise<CdnFile | null> {
setOpen(true);
const list = await listFiles().catch(() => []);
setFiles(
options?.type
? list.filter((file) => file.type.startsWith(options.type!))
: list,
);
return new Promise<CdnFile | null>((resolve) => {
callback.current = resolve;
}).finally(() => setOpen(false));
}
const filtered = search
? files?.filter(
(f) => f.name?.toLowerCase().includes(search) || f.id === search,
)
: files;
const updateList = useCallback(
async (opt = options) => {
const list = await listFiles().catch(() => []);
setFiles(
opt?.type
? list.toReversed().filter((file) => file.type.startsWith(opt.type!))
: list.toReversed(),
);
},
[options],
);
const call = useCallback(
async (options?: { type?: string }): Promise<CdnFile | null> => {
setSearch("");
setOpen(true);
await updateList(options);
setOptions(options);
return new Promise<CdnFile | null>((resolve) => {
callback.current = resolve;
}).finally(() => setOpen(false));
},
[updateList],
);
return (
<CdnChooserContext value={{ call }}>
@@ -55,12 +81,27 @@ export function CdnChooserProvider({
}}
>
<DialogContent className="max-h-[85vh] flex flex-col gap-4 md:max-w-[50svw]">
<DialogTitle className="px-6 pt-6">Choose File</DialogTitle>
<DialogTitle className="px-6 flex justify-between items-center">
Choose File{" "}
<InputGroup className="max-w-xs">
<InputGroupInput
placeholder="Search..."
onChange={(ev) => setSearch(ev.target.value.toLowerCase())}
/>
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
{filtered?.length || 0} results
</InputGroupAddon>
</InputGroup>
</DialogTitle>
{files ? (
<div className="flex-1 min-h-0 overflow-y-auto">
<CdnTable
files={files}
files={filtered || []}
onChoose={(value) => callback.current?.(value)}
onChange={updateList}
/>
</div>
) : (