114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { Button, Input, Toast } from "@douyinfe/semi-ui-19";
|
||
|
|
import { api, post } from "../api";
|
||
|
|
import { ResearchSelect } from "./ResearchSelect";
|
||
|
|
export type ViewContent = {
|
||
|
|
name: string;
|
||
|
|
filters: Record<string, unknown>;
|
||
|
|
columns: string[];
|
||
|
|
};
|
||
|
|
type View = { id: string; version: number; content: ViewContent };
|
||
|
|
export function SavedViews({
|
||
|
|
filters,
|
||
|
|
columns,
|
||
|
|
onRestore,
|
||
|
|
}: {
|
||
|
|
filters: Record<string, unknown>;
|
||
|
|
columns: string[];
|
||
|
|
onRestore: (view: ViewContent) => void;
|
||
|
|
}) {
|
||
|
|
const [items, setItems] = useState<View[]>([]);
|
||
|
|
const [name, setName] = useState("");
|
||
|
|
const [selected, setSelected] = useState<string>();
|
||
|
|
const [busy, setBusy] = useState(false);
|
||
|
|
const [q, setQ] = useState("");
|
||
|
|
const [version, setVersion] = useState(0);
|
||
|
|
useEffect(() => {
|
||
|
|
const controller = new AbortController();
|
||
|
|
api<{ items: View[] }>(
|
||
|
|
`/research/assets?kind=view&q=${encodeURIComponent(q)}&limit=100`,
|
||
|
|
{ signal: controller.signal },
|
||
|
|
)
|
||
|
|
.then((r) => setItems(r.items))
|
||
|
|
.catch((e) => {
|
||
|
|
if (!controller.signal.aborted) Toast.error(e.message);
|
||
|
|
});
|
||
|
|
return () => controller.abort();
|
||
|
|
}, [q, version]);
|
||
|
|
async function save(replace: boolean) {
|
||
|
|
setBusy(true);
|
||
|
|
try {
|
||
|
|
const old = items.find((i) => i.id === selected);
|
||
|
|
if (replace && !old) return;
|
||
|
|
const content = {
|
||
|
|
name: name.trim() || old?.content.name || "研究队列",
|
||
|
|
filters: Object.fromEntries(
|
||
|
|
Object.entries(filters).filter(
|
||
|
|
([, v]) => v !== "" && v !== null && v !== undefined,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
columns,
|
||
|
|
};
|
||
|
|
const value = replace
|
||
|
|
? await api<View>(`/research/assets/${old!.id}`, {
|
||
|
|
method: "PUT",
|
||
|
|
body: JSON.stringify({
|
||
|
|
kind: "view",
|
||
|
|
version: old!.version,
|
||
|
|
content,
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
: await post<View>("/research/assets", { kind: "view", content });
|
||
|
|
setSelected(value.id);
|
||
|
|
setName(value.content.name);
|
||
|
|
setVersion((n) => n + 1);
|
||
|
|
Toast.success("筛选视图已保存");
|
||
|
|
} catch (e) {
|
||
|
|
Toast.error((e as Error).message);
|
||
|
|
} finally {
|
||
|
|
setBusy(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<div className="research-saved-views inline-actions">
|
||
|
|
<ResearchSelect
|
||
|
|
label="保存的 Alpha 视图"
|
||
|
|
placeholder="搜索并恢复研究队列"
|
||
|
|
filter
|
||
|
|
remote
|
||
|
|
onSearch={setQ}
|
||
|
|
value={selected}
|
||
|
|
optionList={items.map((i) => ({
|
||
|
|
value: i.id,
|
||
|
|
label: `${i.content.name} · v${i.version}`,
|
||
|
|
}))}
|
||
|
|
onChange={(v) => {
|
||
|
|
const item = items.find((i) => i.id === v);
|
||
|
|
if (item) {
|
||
|
|
setSelected(item.id);
|
||
|
|
setName(item.content.name);
|
||
|
|
onRestore(item.content);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
aria-label="视图名称"
|
||
|
|
placeholder="视图名称"
|
||
|
|
value={name}
|
||
|
|
onChange={setName}
|
||
|
|
style={{ width: 160 }}
|
||
|
|
/>
|
||
|
|
<Button size="small" disabled={busy} onClick={() => void save(false)}>
|
||
|
|
保存为新视图
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
disabled={busy || !selected}
|
||
|
|
onClick={() => void save(true)}
|
||
|
|
>
|
||
|
|
更新当前视图
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|