From 656d4f9a74bc9ccc2afe47e2a29f65257d1b3e7d Mon Sep 17 00:00:00 2001 From: J Date: Tue, 30 Jun 2026 19:04:42 +0800 Subject: [PATCH] refactor(autopilots): fold access management into the edit dialog (MUL-3893) Remove the standalone 'Manage access' button from the autopilot detail header and surface the grant/revoke list as an 'Access' section inside the Edit dialog's configuration sidebar. Anyone who cannot open Edit already cannot manage access, so the separate affordance was redundant. - Extract the dialog body into a reusable AutopilotAccessManager - Render it in edit mode only, gated on canManageAccess - Drop ManageAccessDialog and its now-dead i18n keys Co-authored-by: multica-agent --- .../components/autopilot-access-manager.tsx | 157 ++++++++++++++++ .../components/autopilot-detail-page.tsx | 18 +- .../components/autopilot-dialog.tsx | 33 ++++ .../components/manage-access-dialog.tsx | 177 ------------------ packages/views/locales/en/autopilots.json | 4 +- packages/views/locales/ja/autopilots.json | 4 +- packages/views/locales/ko/autopilots.json | 4 +- .../views/locales/zh-Hans/autopilots.json | 4 +- 8 files changed, 196 insertions(+), 205 deletions(-) create mode 100644 packages/views/autopilots/components/autopilot-access-manager.tsx delete mode 100644 packages/views/autopilots/components/manage-access-dialog.tsx diff --git a/packages/views/autopilots/components/autopilot-access-manager.tsx b/packages/views/autopilots/components/autopilot-access-manager.tsx new file mode 100644 index 000000000..5c92f4627 --- /dev/null +++ b/packages/views/autopilots/components/autopilot-access-manager.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Plus, X } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { useWorkspaceId } from "@multica/core/hooks"; +import { memberListOptions } from "@multica/core/workspace/queries"; +import { useActorName } from "@multica/core/workspace/hooks"; +import { + useGrantAutopilotAccess, + useRevokeAutopilotAccess, +} from "@multica/core/autopilots/mutations"; +import type { AutopilotCollaborator } from "@multica/core/types"; +import { toast } from "sonner"; +import { ActorAvatar } from "../../common/actor-avatar"; +import { + PropertyPicker, + PickerItem, + PickerEmpty, +} from "../../issues/components/pickers/property-picker"; +import { matchesPinyin } from "../../editor/extensions/pinyin-match"; +import { useT } from "../../i18n"; + +// Grant / revoke explicit write access to an autopilot. Members-only, mirroring +// the subscriber picker. Creators and workspace admins always have access and +// are not listed here — this manages the additional, explicitly-granted set. +// Rendered inline inside the edit dialog's configuration sidebar; access changes +// commit immediately via their own mutations and are independent of the form's +// Save action. +export function AutopilotAccessManager({ + autopilotId, + collaborators, +}: { + autopilotId: string; + collaborators: AutopilotCollaborator[]; +}) { + const { t } = useT("autopilots"); + const wsId = useWorkspaceId(); + const { data: members = [] } = useQuery(memberListOptions(wsId)); + const { getActorName } = useActorName(); + const grant = useGrantAutopilotAccess(); + const revoke = useRevokeAutopilotAccess(); + const [pickerOpen, setPickerOpen] = useState(false); + const [filter, setFilter] = useState(""); + + const grantedIds = useMemo( + () => new Set(collaborators.map((c) => c.user_id)), + [collaborators], + ); + + const query = filter.trim().toLowerCase(); + const candidates = useMemo( + () => + members.filter( + (m) => + !grantedIds.has(m.user_id) && + (query === "" || + m.name.toLowerCase().includes(query) || + matchesPinyin(m.name, query)), + ), + [members, grantedIds, query], + ); + + const handleGrant = async (userId: string) => { + try { + await grant.mutateAsync({ autopilotId, userId }); + toast.success(t(($) => $.access.toast_granted)); + } catch (e: any) { + toast.error(e?.message || t(($) => $.access.toast_failed)); + } + }; + + const handleRevoke = async (userId: string) => { + try { + await revoke.mutateAsync({ autopilotId, userId }); + toast.success(t(($) => $.access.toast_revoked)); + } catch (e: any) { + toast.error(e?.message || t(($) => $.access.toast_failed)); + } + }; + + return ( +
+ {collaborators.length === 0 ? ( +

+ {t(($) => $.access.empty)} +

+ ) : ( +
    + {collaborators.map((c) => ( +
  • + + + + {getActorName("member", c.user_id)} + + + +
  • + ))} +
+ )} + + { + setPickerOpen(v); + if (!v) setFilter(""); + }} + width="w-64" + align="start" + searchable + searchPlaceholder={t(($) => $.access.search_placeholder)} + onSearchChange={setFilter} + trigger={ + + + {t(($) => $.access.add)} + + } + > + {candidates.length === 0 ? ( + + ) : ( + candidates.map((m) => ( + { + void handleGrant(m.user_id); + setPickerOpen(false); + }} + > + + {m.name} + + )) + )} + + +

+ {t(($) => $.access.owner_note)} +

+
+ ); +} diff --git a/packages/views/autopilots/components/autopilot-detail-page.tsx b/packages/views/autopilots/components/autopilot-detail-page.tsx index bcd2dbce9..e65c8ba99 100644 --- a/packages/views/autopilots/components/autopilot-detail-page.tsx +++ b/packages/views/autopilots/components/autopilot-detail-page.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Zap, Play, Clock, Plus, Trash2, CheckCircle2, XCircle, Loader2, Pencil, Ban, ChevronDown, ChevronRight, - Webhook, Copy, Check, RotateCw, Users, + Webhook, Copy, Check, RotateCw, } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { autopilotDetailOptions, autopilotRunsOptions, autopilotRunOptions } from "@multica/core/autopilots/queries"; @@ -62,7 +62,6 @@ import type { AgentTask } from "@multica/core/types/agent"; import { ReadonlyContent } from "../../editor"; import { TranscriptButton } from "../../common/task-transcript"; import { AutopilotDialog } from "./autopilot-dialog"; -import { ManageAccessDialog } from "./manage-access-dialog"; import { WebhookPayloadPreview } from "./webhook-payload-preview"; import { WebhookDeliveriesSection } from "./webhook-deliveries-section"; import { ProjectIcon } from "../../projects/components/project-icon"; @@ -635,7 +634,6 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) { const [triggerDialogOpen, setTriggerDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); - const [accessDialogOpen, setAccessDialogOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleting, setDeleting] = useState(false); @@ -760,12 +758,6 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) { actions={ canWrite ? ( <> - {canManageAccess && ( - - )} - - ))} - - )} - - -

- {t(($) => $.access.owner_note)} -

- - - ); -} diff --git a/packages/views/locales/en/autopilots.json b/packages/views/locales/en/autopilots.json index 2af9aa8c0..a49a58de6 100644 --- a/packages/views/locales/en/autopilots.json +++ b/packages/views/locales/en/autopilots.json @@ -70,7 +70,6 @@ "pause_aria": "Pause autopilot", "activate_aria": "Activate autopilot", "edit": "Edit", - "manage_access": "Manage access", "run_now": "Run now", "running": "Running...", "toast_triggered": "Autopilot triggered", @@ -103,9 +102,7 @@ } }, "access": { - "title": "Manage access", "description": "Members you add can edit, run, and manage this autopilot's triggers and webhooks.", - "current_label": "With access", "add": "Add member", "search_placeholder": "Search members…", "no_results": "No members found", @@ -274,6 +271,7 @@ "section_output_mode": "Output mode", "section_subscribers": "Subscribers", "subscribers_hint": "Auto-subscribed to every issue this autopilot creates", + "section_access": "Access", "subscribers_empty": "No subscribers — add a teammate to notify them on every run", "subscribers_add": "Add subscriber", "subscribers_search_placeholder": "Search members by name…", diff --git a/packages/views/locales/ja/autopilots.json b/packages/views/locales/ja/autopilots.json index 0b1e206d7..978f8e27c 100644 --- a/packages/views/locales/ja/autopilots.json +++ b/packages/views/locales/ja/autopilots.json @@ -70,7 +70,6 @@ "pause_aria": "オートパイロットを一時停止", "activate_aria": "オートパイロットを有効化", "edit": "編集", - "manage_access": "アクセス管理", "run_now": "今すぐ実行", "running": "実行中...", "toast_triggered": "オートパイロットを実行しました", @@ -103,9 +102,7 @@ } }, "access": { - "title": "アクセス管理", "description": "追加したメンバーは、このオートパイロットの編集・実行や、トリガー・Webhook の管理ができます。", - "current_label": "アクセス権あり", "add": "メンバーを追加", "search_placeholder": "メンバーを検索…", "no_results": "メンバーが見つかりません", @@ -273,6 +270,7 @@ "no_project": "プロジェクトなし", "section_subscribers": "購読者", "subscribers_hint": "このオートパイロットが作成するすべてのイシューを自動で購読します", + "section_access": "アクセス", "subscribers_empty": "購読者はいません。チームメイトを追加すると、毎回通知されます", "subscribers_add": "購読者を追加", "subscribers_search_placeholder": "名前でメンバーを検索…", diff --git a/packages/views/locales/ko/autopilots.json b/packages/views/locales/ko/autopilots.json index 4bb1c047b..b0012a959 100644 --- a/packages/views/locales/ko/autopilots.json +++ b/packages/views/locales/ko/autopilots.json @@ -70,7 +70,6 @@ "pause_aria": "오토파일럿 일시 중지", "activate_aria": "오토파일럿 활성화", "edit": "수정", - "manage_access": "접근 권한 관리", "run_now": "지금 실행", "running": "실행 중...", "toast_triggered": "오토파일럿을 실행했습니다", @@ -103,9 +102,7 @@ } }, "access": { - "title": "접근 권한 관리", "description": "추가한 멤버는 이 오토파일럿을 수정·실행하고 트리거와 webhook을 관리할 수 있습니다.", - "current_label": "접근 가능", "add": "멤버 추가", "search_placeholder": "멤버 검색…", "no_results": "멤버를 찾을 수 없습니다", @@ -273,6 +270,7 @@ "no_project": "프로젝트 없음", "section_subscribers": "구독자", "subscribers_hint": "이 오토파일럿이 만드는 모든 이슈를 자동으로 구독합니다", + "section_access": "접근 권한", "subscribers_empty": "구독자가 없습니다. 팀원을 추가하면 매번 알림을 받습니다", "subscribers_add": "구독자 추가", "subscribers_search_placeholder": "이름으로 멤버 검색…", diff --git a/packages/views/locales/zh-Hans/autopilots.json b/packages/views/locales/zh-Hans/autopilots.json index e97a3f381..3a54a15a2 100644 --- a/packages/views/locales/zh-Hans/autopilots.json +++ b/packages/views/locales/zh-Hans/autopilots.json @@ -70,7 +70,6 @@ "pause_aria": "暂停自动化", "activate_aria": "启用自动化", "edit": "编辑", - "manage_access": "管理访问", "run_now": "立即运行", "running": "运行中...", "toast_triggered": "已触发自动化", @@ -103,9 +102,7 @@ } }, "access": { - "title": "管理访问", "description": "你添加的成员可以编辑、运行并管理这个自动化的触发器和 webhook。", - "current_label": "已授权", "add": "添加成员", "search_placeholder": "搜索成员…", "no_results": "未找到成员", @@ -274,6 +271,7 @@ "section_output_mode": "输出模式", "section_subscribers": "订阅者", "subscribers_hint": "每次跑出来的 issue 默认订阅", + "section_access": "访问权限", "subscribers_empty": "暂无订阅者——添加成员后,每次触发都会通知到他", "subscribers_add": "添加订阅者", "subscribers_search_placeholder": "按名称搜索成员……",