mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-01 03:19:13 +02:00
Compare commits
1 Commits
main
...
agent/j/fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656d4f9a74 |
@@ -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 (
|
||||
<div className="space-y-2">
|
||||
{collaborators.length === 0 ? (
|
||||
<p className="rounded-md border border-dashed px-3 py-3 text-center text-xs text-muted-foreground">
|
||||
{t(($) => $.access.empty)}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{collaborators.map((c) => (
|
||||
<li
|
||||
key={c.user_id}
|
||||
className="flex items-center justify-between rounded-md border bg-background px-2 py-1.5"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<ActorAvatar actorType="member" actorId={c.user_id} size={20} />
|
||||
<span className="truncate text-sm">
|
||||
{getActorName("member", c.user_id)}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRevoke(c.user_id)}
|
||||
disabled={revoke.isPending}
|
||||
className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
|
||||
aria-label={t(($) => $.access.remove_tooltip)}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<PropertyPicker
|
||||
open={pickerOpen}
|
||||
onOpenChange={(v) => {
|
||||
setPickerOpen(v);
|
||||
if (!v) setFilter("");
|
||||
}}
|
||||
width="w-64"
|
||||
align="start"
|
||||
searchable
|
||||
searchPlaceholder={t(($) => $.access.search_placeholder)}
|
||||
onSearchChange={setFilter}
|
||||
trigger={
|
||||
<span className="inline-flex cursor-pointer items-center gap-1 rounded-md border border-dashed px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground">
|
||||
<Plus className="size-3" />
|
||||
{t(($) => $.access.add)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{candidates.length === 0 ? (
|
||||
<PickerEmpty />
|
||||
) : (
|
||||
candidates.map((m) => (
|
||||
<PickerItem
|
||||
key={m.user_id}
|
||||
selected={false}
|
||||
onClick={() => {
|
||||
void handleGrant(m.user_id);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
>
|
||||
<ActorAvatar actorType="member" actorId={m.user_id} size={18} />
|
||||
<span className="truncate">{m.name}</span>
|
||||
</PickerItem>
|
||||
))
|
||||
)}
|
||||
</PropertyPicker>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{t(($) => $.access.owner_note)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
<Button size="sm" variant="outline" onClick={() => setAccessDialogOpen(true)} className="px-2 sm:px-2.5" aria-label={t(($) => $.detail.manage_access)}>
|
||||
<Users className="h-3.5 w-3.5 sm:mr-1" />
|
||||
<span className="hidden sm:inline">{t(($) => $.detail.manage_access)}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={() => setEditDialogOpen(true)} className="px-2 sm:px-2.5" aria-label={t(($) => $.detail.edit)}>
|
||||
<Pencil className="h-3.5 w-3.5 sm:mr-1" />
|
||||
<span className="hidden sm:inline">{t(($) => $.detail.edit)}</span>
|
||||
@@ -980,14 +972,8 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) {
|
||||
.map((s) => s.user_id) ?? [],
|
||||
}}
|
||||
triggers={triggers}
|
||||
/>
|
||||
)}
|
||||
{accessDialogOpen && (
|
||||
<ManageAccessDialog
|
||||
open={accessDialogOpen}
|
||||
onOpenChange={setAccessDialogOpen}
|
||||
autopilotId={autopilot.id}
|
||||
collaborators={collaborators}
|
||||
canManageAccess={canManageAccess}
|
||||
/>
|
||||
)}
|
||||
<AlertDialog
|
||||
|
||||
@@ -51,6 +51,7 @@ import { buildAutopilotWebhookUrl } from "@multica/core/autopilots";
|
||||
import { api } from "@multica/core/api";
|
||||
import type {
|
||||
AutopilotAssigneeType,
|
||||
AutopilotCollaborator,
|
||||
AutopilotExecutionMode,
|
||||
AutopilotTrigger,
|
||||
} from "@multica/core/types";
|
||||
@@ -60,6 +61,7 @@ import { ProjectPicker } from "../../projects/components/project-picker";
|
||||
import { ProjectIcon } from "../../projects/components/project-icon";
|
||||
import { AgentPicker, type AssigneeSelection } from "./pickers/agent-picker";
|
||||
import { SubscriberMultiSelect } from "./subscriber-multi-select";
|
||||
import { AutopilotAccessManager } from "./autopilot-access-manager";
|
||||
import {
|
||||
getDefaultTriggerConfig,
|
||||
getLocalTimezone,
|
||||
@@ -102,6 +104,8 @@ export type AutopilotDialogProps =
|
||||
autopilotId: string;
|
||||
initial: AutopilotInitial;
|
||||
triggers: AutopilotTrigger[];
|
||||
collaborators: AutopilotCollaborator[];
|
||||
canManageAccess: boolean;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -665,6 +669,13 @@ export function AutopilotDialog(props: AutopilotDialogProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isCreate && props.canManageAccess && (
|
||||
<AccessSection
|
||||
autopilotId={props.autopilotId}
|
||||
collaborators={props.collaborators}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCreate && (
|
||||
<TriggerKindSection kind={triggerKind} onChange={setTriggerKind} />
|
||||
)}
|
||||
@@ -912,6 +923,28 @@ function SubscribersSection({
|
||||
);
|
||||
}
|
||||
|
||||
function AccessSection({
|
||||
autopilotId,
|
||||
collaborators,
|
||||
}: {
|
||||
autopilotId: string;
|
||||
collaborators: AutopilotCollaborator[];
|
||||
}) {
|
||||
const { t } = useT("autopilots");
|
||||
return (
|
||||
<div>
|
||||
<SectionLabel>{t(($) => $.dialog.section_access)}</SectionLabel>
|
||||
<p className="mb-2 text-[11px] text-muted-foreground">
|
||||
{t(($) => $.access.description)}
|
||||
</p>
|
||||
<AutopilotAccessManager
|
||||
autopilotId={autopilotId}
|
||||
collaborators={collaborators}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleSection({
|
||||
config,
|
||||
onChange,
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
"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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@multica/ui/components/ui/dialog";
|
||||
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.
|
||||
export function ManageAccessDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
autopilotId,
|
||||
collaborators,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogTitle>{t(($) => $.access.title)}</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(($) => $.access.description)}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{t(($) => $.access.current_label)}
|
||||
</span>
|
||||
<PropertyPicker
|
||||
open={pickerOpen}
|
||||
onOpenChange={(v) => {
|
||||
setPickerOpen(v);
|
||||
if (!v) setFilter("");
|
||||
}}
|
||||
width="w-64"
|
||||
align="start"
|
||||
searchable
|
||||
searchPlaceholder={t(($) => $.access.search_placeholder)}
|
||||
onSearchChange={setFilter}
|
||||
trigger={
|
||||
<span className="inline-flex cursor-pointer items-center gap-1 rounded-md border border-dashed px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground">
|
||||
<Plus className="size-3" />
|
||||
{t(($) => $.access.add)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{candidates.length === 0 ? (
|
||||
<PickerEmpty />
|
||||
) : (
|
||||
candidates.map((m) => (
|
||||
<PickerItem
|
||||
key={m.user_id}
|
||||
selected={false}
|
||||
onClick={() => {
|
||||
void handleGrant(m.user_id);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
>
|
||||
<ActorAvatar actorType="member" actorId={m.user_id} size={18} />
|
||||
<span className="truncate">{m.name}</span>
|
||||
</PickerItem>
|
||||
))
|
||||
)}
|
||||
</PropertyPicker>
|
||||
</div>
|
||||
|
||||
{collaborators.length === 0 ? (
|
||||
<p className="rounded-md border border-dashed px-3 py-4 text-center text-sm text-muted-foreground">
|
||||
{t(($) => $.access.empty)}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{collaborators.map((c) => (
|
||||
<li
|
||||
key={c.user_id}
|
||||
className="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-muted/50"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<ActorAvatar actorType="member" actorId={c.user_id} size={20} />
|
||||
<span className="truncate text-sm">
|
||||
{getActorName("member", c.user_id)}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRevoke(c.user_id)}
|
||||
disabled={revoke.isPending}
|
||||
className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
|
||||
aria-label={t(($) => $.access.remove_tooltip)}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(($) => $.access.owner_note)}
|
||||
</p>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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…",
|
||||
|
||||
@@ -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": "名前でメンバーを検索…",
|
||||
|
||||
@@ -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": "이름으로 멤버 검색…",
|
||||
|
||||
@@ -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": "按名称搜索成员……",
|
||||
|
||||
Reference in New Issue
Block a user