Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/ArchivedTasks.tsx

tsx201 行查看原文
import * as Dialog from "@radix-ui/react-dialog";
import { Folder, Search, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { SessionRecord } from "../shared/bridge.ts";
import { deleteSessionWithPanes } from "./pane-store.ts";
import { SettingsHelp } from "./SettingsHelp.tsx";
import { refresh, useStore } from "./store.ts";
import { unwrap } from "./unwrap.ts";
import { userErrorMessage } from "./user-error.ts";

type ArchivedRecord = SessionRecord & { projectName: string };
export function ArchivedTasks() {
  const sessions = useStore((s) => s.sessions);
  const [records, setRecords] = useState<ArchivedRecord[]>([]);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [query, setQuery] = useState("");
  const [deleting, setDeleting] = useState<ArchivedRecord[]>();
  const load = useCallback(async () => {
    setRecords(unwrap(await window.zpi.listArchivedSessions()));
  }, []);
  useEffect(() => {
    void load()
      .catch((e) => setError(userErrorMessage(e)))
      .finally(() => setLoading(false));
  }, [load]);
  const running = (r: ArchivedRecord) => (sessions.get(r.id)?.status ?? r.status) === "running";
  const visible = records.filter((r) =>
    `${r.title} ${r.projectName}`.toLowerCase().includes(query.toLowerCase()),
  );
  const groups = new Map<string, { name: string; records: ArchivedRecord[] }>();
  for (const r of visible) {
    const key = r.diagnostic ? "_damaged" : (r.projectId ?? "_unassigned");
    const group = groups.get(key) ?? { name: r.diagnostic ? "无法读取的任务" : r.projectName, records: [] };
    group.records.push(r);
    groups.set(key, group);
  }
  const remove = async (targets: ArchivedRecord[]) => {
    setBusy(true);
    const failures: string[] = [];
    try {
      for (const r of targets) {
        try {
          await deleteSessionWithPanes(r.id);
        } catch (e) {
          failures.push(`${r.title}:${userErrorMessage(e)}`);
        }
      }
      setDeleting(undefined);
      await load();
      await refresh();
      setError(failures.join("\n"));
    } catch (e) {
      setError(userErrorMessage(e));
    } finally {
      setBusy(false);
    }
  };
  return (
    <div className="archived-tasks">
      <div className="archived-heading">
        <div className="settings-heading-title">
          <h1>已归档任务</h1>
          <SettingsHelp
            label="已归档任务"
            text="可恢复任务,或永久删除任务及其会话记录、图片附件和文件变更快照。删除前会再次确认。"
          />
        </div>
        <button
          className="archived-delete-all"
          disabled={busy || !records.length}
          onClick={() => setDeleting([...records])}
        >
          <Trash2 size={14} />
          全部删除
        </button>
      </div>
      <label className="archived-search">
        <Search size={16} />
        <input
          aria-label="搜索已归档任务"
          placeholder="搜索已归档任务"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
      </label>
      {error && (
        <div className="run-error" role="alert">
          {error}
        </div>
      )}
      {loading ? (
        <p role="status">正在读取任务…</p>
      ) : !visible.length ? (
        <p className="settings-intro" role="status">
          {query ? "没有匹配的任务" : "暂无已归档任务"}
        </p>
      ) : null}
      {[...groups]
        .sort(([a], [b]) => (a === "_damaged" ? 1 : b === "_damaged" ? -1 : 0))
        .map(([key, group]) => (
          <section className="archived-group" key={key} aria-label={group.name}>
            <header>
              <Folder size={16} />
              <strong>{group.name}</strong>
              <span>{group.records.length} 个任务</span>
            </header>
            <div className="archived-list">
              {group.records.map((r) => (
                <div className="archived-row" key={r.id} data-session-id={r.id}>
                  <div className="archived-copy">
                    <strong title={r.title}>{r.title}</strong>
                    <small>
                      {r.projectName} ·{" "}
                      {r.archivedAt != null
                        ? new Date(r.archivedAt).toLocaleString("zh-CN")
                        : "归档时间不可用"}
                    </small>
                    {r.diagnostic && (
                      <small className="archived-diagnostic" title={r.diagnostic}>
                        {r.diagnostic}
                      </small>
                    )}
                  </div>
                  <button
                    className="archived-delete"
                    aria-label={`删除任务 ${r.title}`}
                    title={running(r) ? "请先停止运行" : "永久删除"}
                    disabled={busy || running(r)}
                    onClick={() => setDeleting([r])}
                  >
                    <Trash2 size={14} />
                  </button>
                  {!r.diagnostic && (
                    <button
                      className="archived-restore"
                      disabled={busy || running(r)}
                      title={running(r) ? "请先停止运行" : undefined}
                      onClick={() => {
                        setBusy(true);
                        setError("");
                        void window.zpi
                          .restoreSession(r.id)
                          .then(unwrap)
                          .then(load)
                          .then(refresh)
                          .catch((e) => setError(userErrorMessage(e)))
                          .finally(() => setBusy(false));
                      }}
                    >
                      恢复
                    </button>
                  )}
                </div>
              ))}
            </div>
          </section>
        ))}
      <Dialog.Root
        open={Boolean(deleting)}
        onOpenChange={(open) => {
          if (!open && !busy) setDeleting(undefined);
        }}
      >
        <Dialog.Portal>
          <Dialog.Overlay className="modal-backdrop archived-delete-backdrop" />
          <Dialog.Content
            className="modal archived-delete-dialog"
            onEscapeKeyDown={(e) => {
              if (busy) e.preventDefault();
            }}
            onPointerDownOutside={(e) => {
              if (busy) e.preventDefault();
            }}
          >
            <Dialog.Title>永久删除 {deleting?.length ?? 0} 个任务?</Dialog.Title>
            <Dialog.Description>
              会话记录、图片附件和文件变更快照将一并删除,无法恢复。项目源文件会保留。
            </Dialog.Description>
            <div className="modal-actions">
              <button className="secondary" disabled={busy} onClick={() => setDeleting(undefined)}>
                取消
              </button>
              <button
                className="danger-button"
                disabled={busy}
                onClick={() => {
                  if (deleting) void remove(deleting);
                }}
              >
                {busy ? "正在删除…" : "确认删除"}
              </button>
            </div>
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>
    </div>
  );
}