mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-14 06:13:49 +08:00
feat: make task global asset sharing opt-in
This commit is contained in:
1 parent
d36e1c3295
commit
080a46eb2d
25 files changed
+838
-328
No files matched your search
@@ -24,6 +24,8 @@ import {
|
||||
ChevronRightIcon,
|
||||
EyeIcon,
|
||||
FolderInputIcon,
|
||||
GlobeIcon,
|
||||
GlobeOffIcon,
|
||||
GripVerticalIcon,
|
||||
Loader2Icon,
|
||||
PaperclipIcon,
|
||||
@@ -795,6 +797,30 @@ export default function TasksPage() {
|
||||
[load],
|
||||
);
|
||||
|
||||
const toggleTaskGlobalAssets = React.useCallback(
|
||||
async (task: Task) => {
|
||||
const enabled = task.share_global_assets === true;
|
||||
try {
|
||||
const updated = await api.updateTask(task.id, {
|
||||
share_global_assets: !enabled,
|
||||
});
|
||||
setTasks((current) =>
|
||||
current.map((entry) =>
|
||||
entry.id === task.id
|
||||
? { ...entry, share_global_assets: updated.share_global_assets }
|
||||
: entry,
|
||||
),
|
||||
);
|
||||
toast.success(`任务 #${task.id} 已${enabled ? "关闭" : "开启"}共享全局资产`);
|
||||
lastRef.current = "";
|
||||
load();
|
||||
} catch (error) {
|
||||
toast.error(`设置失败:${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
[load],
|
||||
);
|
||||
|
||||
const queueTaskArchive = React.useCallback(
|
||||
async (task: Task) => {
|
||||
try {
|
||||
@@ -1284,6 +1310,7 @@ export default function TasksPage() {
|
||||
onRename={renameTask}
|
||||
onRemark={updateTaskRemark}
|
||||
onTogglePinned={toggleTaskPinned}
|
||||
onToggleGlobalAssets={toggleTaskGlobalAssets}
|
||||
onArchive={queueTaskArchive}
|
||||
selected={selectedIds.has(task.id)}
|
||||
onSelectedChange={toggleSelected}
|
||||
@@ -1547,6 +1574,7 @@ const TaskRow = React.memo(function TaskRow({
|
||||
onRename,
|
||||
onRemark,
|
||||
onTogglePinned,
|
||||
onToggleGlobalAssets,
|
||||
onArchive,
|
||||
selected,
|
||||
onSelectedChange,
|
||||
@@ -1559,6 +1587,7 @@ const TaskRow = React.memo(function TaskRow({
|
||||
onRename: (task: Task, name: string) => Promise<void>;
|
||||
onRemark: (task: Task, remark: string) => Promise<void>;
|
||||
onTogglePinned: (task: Task) => Promise<void>;
|
||||
onToggleGlobalAssets: (task: Task) => Promise<void>;
|
||||
onArchive: (task: Task) => Promise<void>;
|
||||
selected: boolean;
|
||||
onSelectedChange: (id: string, checked: boolean) => void;
|
||||
@@ -1756,19 +1785,24 @@ const TaskRow = React.memo(function TaskRow({
|
||||
</TableCell>
|
||||
<TableCell className="sticky right-0 z-10 bg-card text-right shadow-[-1px_0_0_0_hsl(var(--border))] group-hover:bg-muted/50">
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
asChild
|
||||
aria-label="查看任务详情"
|
||||
title="查看任务详情"
|
||||
<TaskGlobalAssetsAction task={task} onToggle={onToggleGlobalAssets} />
|
||||
<TaskActionTooltip
|
||||
label="查看任务详情"
|
||||
description="查看任务进度、探索记录、测试资产和发现的漏洞。"
|
||||
>
|
||||
<Link
|
||||
href={`/function/tasks/detail?id=${encodeURIComponent(task.id)}`}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
asChild
|
||||
aria-label="查看任务详情"
|
||||
>
|
||||
<EyeIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Link
|
||||
href={`/function/tasks/detail?id=${encodeURIComponent(task.id)}`}
|
||||
>
|
||||
<EyeIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
<TaskControlButton task={task} onControl={onControl} />
|
||||
<TaskPinAction task={task} onTogglePinned={onTogglePinned} />
|
||||
<TaskArchiveAction task={task} onArchive={onArchive} />
|
||||
@@ -1779,6 +1813,40 @@ const TaskRow = React.memo(function TaskRow({
|
||||
);
|
||||
});
|
||||
|
||||
function TaskActionTooltip({
|
||||
label,
|
||||
description,
|
||||
children,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
description: string;
|
||||
children: React.ReactElement;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
{disabled ? (
|
||||
<span className="inline-flex" aria-label={label}>
|
||||
{children}
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="block max-w-64 space-y-1 text-left leading-relaxed"
|
||||
>
|
||||
<p className="font-medium">{label}</p>
|
||||
<p>{description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskNameEditor({
|
||||
task,
|
||||
onRename,
|
||||
@@ -1865,20 +1933,24 @@ function TaskNameEditor({
|
||||
<span className="text-muted-foreground">未命名</span>
|
||||
)}
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="shrink-0 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100"
|
||||
aria-label={`重命名任务 #${task.id}`}
|
||||
title="重命名"
|
||||
onClick={() => {
|
||||
setName(task.name ?? "");
|
||||
setEditing(true);
|
||||
}}
|
||||
<TaskActionTooltip
|
||||
label="重命名任务"
|
||||
description="修改任务名称,便于识别和查找。"
|
||||
>
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="shrink-0 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100"
|
||||
aria-label={`重命名任务 #${task.id}`}
|
||||
onClick={() => {
|
||||
setName(task.name ?? "");
|
||||
setEditing(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1900,24 +1972,93 @@ function TaskPinAction({
|
||||
const pinned = taskIsPinned(task);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<TaskActionTooltip
|
||||
label={pinned ? "取消置顶" : "置顶任务"}
|
||||
description={
|
||||
pinned
|
||||
? "取消此任务的置顶,恢复正常排序。"
|
||||
: "将此任务固定在列表顶部,便于快速查看。"
|
||||
}
|
||||
disabled={pinning}
|
||||
aria-label={pinned ? `取消置顶任务 #${task.id}` : `置顶任务 #${task.id}`}
|
||||
title={pinned ? "取消置顶" : "置顶"}
|
||||
onClick={async () => {
|
||||
setPinning(true);
|
||||
try {
|
||||
await onTogglePinned(task);
|
||||
} finally {
|
||||
setPinning(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{taskPinIcon(pinning, pinned)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={pinning}
|
||||
aria-label={
|
||||
pinned ? `取消置顶任务 #${task.id}` : `置顶任务 #${task.id}`
|
||||
}
|
||||
onClick={async () => {
|
||||
setPinning(true);
|
||||
try {
|
||||
await onTogglePinned(task);
|
||||
} finally {
|
||||
setPinning(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{taskPinIcon(pinning, pinned)}
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function taskGlobalAssetsIcon(saving: boolean, enabled: boolean) {
|
||||
if (saving) return <Loader2Icon className="animate-spin" />;
|
||||
if (enabled) return <GlobeIcon />;
|
||||
return <GlobeOffIcon />;
|
||||
}
|
||||
|
||||
function TaskGlobalAssetsAction({
|
||||
task,
|
||||
onToggle,
|
||||
}: {
|
||||
task: Task;
|
||||
onToggle: (task: Task) => Promise<void>;
|
||||
}) {
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const enabled = task.share_global_assets === true;
|
||||
const label = `共享全局资产:${enabled ? "已开启,点击关闭" : "未开启,点击开启"}`;
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={label}
|
||||
aria-pressed={enabled}
|
||||
aria-busy={saving}
|
||||
className={enabled ? "text-foreground" : "text-muted-foreground"}
|
||||
disabled={saving}
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onToggle(task);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{taskGlobalAssetsIcon(saving, enabled)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="block max-w-64 space-y-1 text-left leading-relaxed"
|
||||
>
|
||||
<p className="font-medium">
|
||||
共享全局资产 · {enabled ? "已开启" : "已关闭"}
|
||||
</p>
|
||||
<p>
|
||||
开启后可查询全局资产库;关闭后仅使用本任务已关联的资产及直接关联任务的资产。
|
||||
</p>
|
||||
<p className="opacity-75">
|
||||
{enabled ? "点击关闭共享" : "点击开启共享"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1940,25 +2081,34 @@ function TaskControlButton({
|
||||
const [pending, setPending] = React.useState(false);
|
||||
const action = taskControlAction(task.status);
|
||||
const label = action === "resume" ? "继续任务" : "暂停任务";
|
||||
let description = "暂停当前任务的执行,之后可继续。";
|
||||
if (action === "resume")
|
||||
description = "继续已暂停的任务;等待中的任务将重新进入调度。";
|
||||
if (!action) description = "当前任务状态不支持暂停或继续。";
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={label}
|
||||
title={action ? label : "该状态不可暂停或继续"}
|
||||
<TaskActionTooltip
|
||||
label={label}
|
||||
description={description}
|
||||
disabled={!action || pending}
|
||||
onClick={async () => {
|
||||
if (!action) return;
|
||||
setPending(true);
|
||||
try {
|
||||
await onControl(task.id, action);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{taskControlIcon(pending, action)}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={label}
|
||||
disabled={!action || pending}
|
||||
onClick={async () => {
|
||||
if (!action) return;
|
||||
setPending(true);
|
||||
try {
|
||||
await onControl(task.id, action);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{taskControlIcon(pending, action)}
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2040,20 +2190,27 @@ function TaskArchiveAction({
|
||||
);
|
||||
if (reason) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">{trigger}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{reason}</TooltipContent>
|
||||
</Tooltip>
|
||||
<TaskActionTooltip label="暂不可归档" description={reason} disabled>
|
||||
{trigger}
|
||||
</TaskActionTooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ArchiveConfirmDialog
|
||||
count={1}
|
||||
trigger={trigger}
|
||||
onConfirm={() => onArchive(task)}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<ArchiveConfirmDialog
|
||||
count={1}
|
||||
trigger={<TooltipTrigger asChild>{trigger}</TooltipTrigger>}
|
||||
onConfirm={() => onArchive(task)}
|
||||
/>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="block max-w-64 space-y-1 text-left leading-relaxed"
|
||||
>
|
||||
<p className="font-medium">归档任务</p>
|
||||
<p>将任务及相关记录压缩保存,之后可从“已归档”中还原。点击后需确认。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2545,42 +2702,51 @@ function TaskArchivesPanel({ onChanged }: { onChanged: () => void }) {
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-0.5">
|
||||
{ARCHIVE_FAILED_STATES.has(archive.state) && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => void retry(archive)}
|
||||
aria-label="重试归档操作"
|
||||
title="重试"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</Button>
|
||||
<TaskActionTooltip label="重试归档操作" description="重新执行上次失败的归档、还原或删除操作。">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => void retry(archive)}
|
||||
aria-label="重试归档操作"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
)}
|
||||
{canRestore && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => void restoreMany([archive])}
|
||||
aria-label="还原任务"
|
||||
title="还原任务"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</Button>
|
||||
<TaskActionTooltip label="还原任务" description="将归档中的任务及相关记录恢复到当前任务列表。">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => void restoreMany([archive])}
|
||||
aria-label="还原任务"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
)}
|
||||
{canDelete && (
|
||||
<ArchiveDeleteDialog
|
||||
archives={[archive]}
|
||||
onConfirm={() => deleteMany([archive])}
|
||||
trigger={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="永久删除归档"
|
||||
title="永久删除"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<ArchiveDeleteDialog
|
||||
archives={[archive]}
|
||||
onConfirm={() => deleteMany([archive])}
|
||||
trigger={
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="永久删除归档"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
}
|
||||
/>
|
||||
<TooltipContent side="top" sideOffset={6} className="block max-w-64 space-y-1 text-left leading-relaxed">
|
||||
<p className="font-medium">永久删除归档</p>
|
||||
<p>删除此归档及其保存的数据,删除后无法还原。点击后需确认。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -2795,11 +2961,13 @@ function DeleteTaskDialog({
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline" aria-label="删除任务">
|
||||
<Trash2Icon className="text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<TaskActionTooltip label="删除任务" description="删除任务及执行记录;可在确认窗口选择是否同时清理关联数据。">
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline" aria-label="删除任务">
|
||||
<Trash2Icon className="text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TaskActionTooltip>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除任务 #{task.id}?</AlertDialogTitle>
|
||||
@@ -3415,19 +3583,24 @@ function DraggableCategoryTask({
|
||||
{moving ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
className="touch-none cursor-grab active:cursor-grabbing"
|
||||
<TaskActionTooltip
|
||||
label="移动任务分类"
|
||||
description="按住并拖动此图标,将任务移动到目标分类。"
|
||||
disabled={disabled}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
aria-label={`拖动任务 #${task.id}`}
|
||||
title="拖动任务"
|
||||
>
|
||||
<GripVerticalIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
className="touch-none cursor-grab active:cursor-grabbing"
|
||||
disabled={disabled}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
aria-label={`拖动任务 #${task.id}`}
|
||||
>
|
||||
<GripVerticalIcon />
|
||||
</Button>
|
||||
</TaskActionTooltip>
|
||||
)}
|
||||
</ItemMedia>
|
||||
<ItemContent className="min-w-0">
|
||||
@@ -3877,6 +4050,7 @@ function CreateTaskSheet({
|
||||
const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时
|
||||
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
|
||||
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
|
||||
const [shareGlobalAssets, setShareGlobalAssets] = React.useState(false);
|
||||
const [coverageEnabled, setCoverageEnabled] = React.useState(true); // 资产覆盖度功能;默认开。关闭=不计算/展示覆盖度、不累积范围、隐藏范围类工具(company 关联不受影响)
|
||||
const [workerCount, setWorkerCount] = React.useState(""); // 本任务 Worker 数;空/0=用全局默认
|
||||
const [interceptRules, setInterceptRules] = React.useState<
|
||||
@@ -3972,6 +4146,7 @@ function CreateTaskSheet({
|
||||
seedFirstIntent,
|
||||
planHeartbeatSeconds: heartbeatSec,
|
||||
coverageEnabled,
|
||||
shareGlobalAssets,
|
||||
workerCount: workerCountNum,
|
||||
schedule: scheduleEnabled ? { ...schedule, task_ids: [] } : undefined,
|
||||
interceptRules: interceptRules
|
||||
@@ -3992,6 +4167,7 @@ function CreateTaskSheet({
|
||||
setHeartbeatMin("10");
|
||||
setSeedFirstIntent(false);
|
||||
setCoverageEnabled(true);
|
||||
setShareGlobalAssets(false);
|
||||
setWorkerCount("");
|
||||
setInterceptRules([]);
|
||||
setScheduleEnabled(false);
|
||||
@@ -4286,6 +4462,19 @@ function CreateTaskSheet({
|
||||
onChange={(e) => setGoal(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Field>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="share-global-assets"
|
||||
checked={shareGlobalAssets}
|
||||
onCheckedChange={(checked) => setShareGlobalAssets(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="share-global-assets">共享全局资产</Label>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
默认关闭,仅使用本任务及明确关联任务的资产。开启后可查询全局资产库,测试仍须遵守授权范围。
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="source-tasks">关联任务</FieldLabel>
|
||||
<SourceTaskPicker
|
||||
|
||||
@@ -8,6 +8,7 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { MAX_TASK_REMARK_RUNES } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -165,17 +166,23 @@ function InlineTextEditor({
|
||||
<span className="text-muted-foreground">{emptyLabel}</span>
|
||||
)}
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="shrink-0 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100 [@media(hover:hover)]:group-focus-within/inline-editor:opacity-100 [@media(hover:hover)]:group-hover/inline-editor:opacity-100"
|
||||
aria-label={`编辑${label}的${fieldName}`}
|
||||
title={`编辑${fieldName}`}
|
||||
onClick={startEditing}
|
||||
>
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="shrink-0 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100 [@media(hover:hover)]:group-focus-within/inline-editor:opacity-100 [@media(hover:hover)]:group-hover/inline-editor:opacity-100"
|
||||
aria-label={`编辑${label}的${fieldName}`}
|
||||
onClick={startEditing}
|
||||
>
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
编辑{fieldName}:输入完成后按 Enter 或点击其他位置保存,按 Esc 取消。
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-1
@@ -346,6 +346,7 @@ export const api = {
|
||||
timeoutSeconds?: number;
|
||||
seedFirstIntent?: boolean;
|
||||
planHeartbeatSeconds?: number;
|
||||
shareGlobalAssets?: boolean;
|
||||
coverageEnabled?: boolean;
|
||||
workerCount?: number;
|
||||
interceptRules?: AssetInterceptRuleInput[];
|
||||
@@ -363,6 +364,7 @@ export const api = {
|
||||
timeout_seconds: input.timeoutSeconds ?? 0,
|
||||
seed_first_intent: input.seedFirstIntent ?? false,
|
||||
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
|
||||
share_global_assets: input.shareGlobalAssets ?? false,
|
||||
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
|
||||
worker_count: input.workerCount ?? 0, // 0 = 用全局默认 workers
|
||||
intercept_rules: input.interceptRules ?? [], // 任务级资产拦截规则
|
||||
@@ -380,7 +382,7 @@ export const api = {
|
||||
runScheduleNow: (id: number) =>
|
||||
post<{ ok: boolean; started: number; errors?: string[] }>(`/schedules/${id}/run-now`, {}),
|
||||
taskCategories: () => get<{ categories: TaskCategory[] }>("/task-categories").then((r) => arr(r.categories)),
|
||||
updateTask: (id: string, input: { name?: string; pinned?: boolean; remark?: string }) =>
|
||||
updateTask: (id: string, input: { name?: string; pinned?: boolean; remark?: string; share_global_assets?: boolean }) =>
|
||||
patch<Task>(`/tasks/${id}`, input),
|
||||
renameTask: (id: string, name: string) => patch<Task>(`/tasks/${id}`, { name }),
|
||||
pinTask: (id: string, pinned: boolean) => patch<Task>(`/tasks/${id}`, { pinned }),
|
||||
|
||||
@@ -253,6 +253,7 @@ function mockTaskArchiveBlocker(taskID: string): string | undefined {
|
||||
|
||||
function publicMockTask(task: Task): Task {
|
||||
const item = structuredClone(task);
|
||||
item.share_global_assets = task.share_global_assets ?? false;
|
||||
const blocker = mockTaskArchiveBlocker(task.id);
|
||||
if (blocker) item.archive_blocked_by_task_id = blocker;
|
||||
else delete item.archive_blocked_by_task_id;
|
||||
@@ -1272,6 +1273,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
llm_failover_state: profileIDs.length ? "ready" : "default",
|
||||
source_task_ids: sourceTaskIDs,
|
||||
company_ids: companyIDs,
|
||||
share_global_assets: b.share_global_assets === true,
|
||||
};
|
||||
const numericTaskID = nextMockTaskAssetID++;
|
||||
mockTaskAssetIDs.set(id, numericTaskID);
|
||||
@@ -1460,10 +1462,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
if (has("name") && b.name !== null && typeof b.name !== "string") throw new Error("任务名称必须是字符串");
|
||||
if (has("remark") && b.remark !== null && typeof b.remark !== "string") throw new Error("备注必须是字符串");
|
||||
if (has("pinned") && b.pinned !== null && typeof b.pinned !== "boolean") throw new Error("pinned 必须是布尔值");
|
||||
if (has("share_global_assets") && b.share_global_assets !== null && typeof b.share_global_assets !== "boolean") throw new Error("share_global_assets 必须是布尔值");
|
||||
const sharingPresent = has("share_global_assets") && b.share_global_assets !== null;
|
||||
const namePresent = has("name") && b.name !== null;
|
||||
const remarkPresent = has("remark") && b.remark !== null;
|
||||
const pinnedPresent = has("pinned") && b.pinned !== null;
|
||||
if (!namePresent && !remarkPresent && !pinnedPresent) throw new Error("至少需要提供 name、pinned 或 remark");
|
||||
if (!namePresent && !remarkPresent && !pinnedPresent && !sharingPresent) throw new Error("至少需要提供 name、pinned、remark 或 share_global_assets");
|
||||
let nextName = task.name;
|
||||
let nextRemark = task.remark;
|
||||
let nextPinned = task.pinned;
|
||||
@@ -1480,6 +1484,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
nextPinned = pinned;
|
||||
nextPinnedAt = pinned ? (task.pinned_at ?? new Date().toISOString()) : null;
|
||||
}
|
||||
if (sharingPresent) task.share_global_assets = b.share_global_assets as boolean;
|
||||
task.name = nextName;
|
||||
task.remark = nextRemark;
|
||||
task.pinned = nextPinned;
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface Task {
|
||||
source_task_ids?: string[]; // directly related tasks inherited as read-only context
|
||||
archive_blocked_by_task_id?: string; // live direct dependent that must be archived first
|
||||
company_ids?: number[]; // associated company scopes; current company assets join the task at creation
|
||||
share_global_assets?: boolean; // opt-in global asset visibility; default false
|
||||
coverage_enabled?: boolean; // 资产覆盖度功能开关(创建时定,默认开);false=不计算/不展示覆盖度
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user