feat: add search bar in cdn chooser
This commit is contained in:
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user