mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Enable routine description mentions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e71b27fbd4
commit
b6cf7dd91a
3 files changed
+121
-3
No files matched your search
@@ -20,18 +20,20 @@ import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { LiveRunWidget } from "../components/LiveRunWidget";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { accessApi } from "../api/access";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { buildRoutineTriggerPatch } from "../lib/routine-trigger-patch";
|
||||
import { buildMarkdownMentionOptions } from "../lib/company-members";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor";
|
||||
import {
|
||||
RoutineRunVariablesDialog,
|
||||
type RoutineRunDialogSubmitData,
|
||||
@@ -350,6 +352,11 @@ export function RoutineDetail() {
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const routineDefaults = useMemo(
|
||||
() =>
|
||||
@@ -649,6 +656,13 @@ export function RoutineDetail() {
|
||||
})),
|
||||
[projects],
|
||||
);
|
||||
const mentionOptions = useMemo<MentionOption[]>(() => {
|
||||
return buildMarkdownMentionOptions({
|
||||
agents,
|
||||
projects,
|
||||
members: companyMembers?.users,
|
||||
});
|
||||
}, [agents, companyMembers?.users, projects]);
|
||||
const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null;
|
||||
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
|
||||
|
||||
@@ -882,6 +896,7 @@ export function RoutineDetail() {
|
||||
placeholder="Add instructions..."
|
||||
bordered={false}
|
||||
contentClassName="min-h-[120px] text-[15px] leading-7"
|
||||
mentions={mentionOptions}
|
||||
onSubmit={() => {
|
||||
if (!saveRoutine.isPending && editDraft.title.trim()) {
|
||||
saveRoutine.mutate();
|
||||
|
||||
@@ -12,6 +12,7 @@ let currentSearch = "";
|
||||
const navigateMock = vi.fn();
|
||||
const routinesListMock = vi.fn<(companyId: string) => Promise<RoutineListItem[]>>();
|
||||
const issuesListMock = vi.fn<(companyId: string, filters?: Record<string, unknown>) => Promise<Issue[]>>();
|
||||
const markdownEditorRenderMock = vi.fn((props: { mentions?: Array<{ id: string; name: string }> }) => props);
|
||||
const issuesListRenderMock = vi.fn(({ issues }: { issues: Issue[] }) => (
|
||||
<div data-testid="issues-list">{issues.map((issue) => issue.title).join(", ")}</div>
|
||||
));
|
||||
@@ -158,6 +159,24 @@ vi.mock("../api/projects", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../api/access", () => ({
|
||||
accessApi: {
|
||||
listUserDirectory: vi.fn(async () => ({
|
||||
users: [
|
||||
{
|
||||
principalId: "user-1",
|
||||
status: "active",
|
||||
user: {
|
||||
name: "Taylor",
|
||||
email: "taylor@example.com",
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../api/instanceSettings", () => ({
|
||||
instanceSettingsApi: {
|
||||
getExperimental: vi.fn(async () => ({ enableIsolatedWorkspaces: false })),
|
||||
@@ -186,7 +205,10 @@ vi.mock("@/components/ui/tabs", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../components/MarkdownEditor", () => ({
|
||||
MarkdownEditor: () => <div />,
|
||||
MarkdownEditor: (props: { mentions?: Array<{ id: string; name: string }> }) => {
|
||||
markdownEditorRenderMock(props);
|
||||
return <div data-testid="markdown-editor" />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../components/InlineEntitySelector", () => ({
|
||||
@@ -303,6 +325,7 @@ describe("Routines page", () => {
|
||||
navigateMock.mockReset();
|
||||
routinesListMock.mockReset();
|
||||
issuesListMock.mockReset();
|
||||
markdownEditorRenderMock.mockClear();
|
||||
issuesListRenderMock.mockClear();
|
||||
localStorage.clear();
|
||||
});
|
||||
@@ -425,6 +448,70 @@ describe("Routines page", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes company mention options to the routine description editor", async () => {
|
||||
routinesListMock.mockResolvedValue([]);
|
||||
issuesListMock.mockResolvedValue([]);
|
||||
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routines />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await flush();
|
||||
});
|
||||
|
||||
let createButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Create routine"),
|
||||
);
|
||||
for (let attempts = 0; attempts < 5 && !createButton; attempts += 1) {
|
||||
await act(async () => {
|
||||
await flush();
|
||||
});
|
||||
createButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Create routine"),
|
||||
);
|
||||
}
|
||||
|
||||
expect(createButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
createButton?.click();
|
||||
await flush();
|
||||
});
|
||||
|
||||
for (let attempts = 0; attempts < 5; attempts += 1) {
|
||||
const hasMentionOptions = markdownEditorRenderMock.mock.calls.some(([props]) => (props.mentions ?? []).length > 0);
|
||||
if (hasMentionOptions) break;
|
||||
await act(async () => {
|
||||
await flush();
|
||||
});
|
||||
}
|
||||
|
||||
const callsWithMentions = markdownEditorRenderMock.mock.calls
|
||||
.map(([props]) => props.mentions ?? [])
|
||||
.filter((mentions) => mentions.length > 0);
|
||||
|
||||
expect(callsWithMentions.at(-1)?.map((mention) => mention.id)).toEqual([
|
||||
"user:user-1",
|
||||
"agent:agent-1",
|
||||
"agent:agent-2",
|
||||
"project:project-1",
|
||||
"project:project-2",
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows recent runs through the issues list scoped to routine execution issues", async () => {
|
||||
currentSearch = "tab=runs";
|
||||
routinesListMock.mockResolvedValue([createRoutine({ id: "routine-1" })]);
|
||||
|
||||
@@ -7,9 +7,11 @@ import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { accessApi } from "../api/access";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { buildMarkdownMentionOptions } from "../lib/company-members";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { groupBy } from "../lib/groupBy";
|
||||
import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
||||
@@ -23,7 +25,7 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor";
|
||||
import {
|
||||
RoutineRunVariablesDialog,
|
||||
type RoutineRunDialogSubmitData,
|
||||
@@ -394,6 +396,11 @@ export function Routines() {
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const { data: routineExecutionIssues, isLoading: recentRunsLoading, error: recentRunsError } = useQuery({
|
||||
queryKey: [...queryKeys.issues.list(selectedCompanyId!), "routine-executions"],
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!, { originKind: "routine_execution" }),
|
||||
@@ -410,6 +417,14 @@ export function Routines() {
|
||||
autoResizeTextarea(titleInputRef.current);
|
||||
}, [draft.title, composerOpen]);
|
||||
|
||||
const mentionOptions = useMemo<MentionOption[]>(() => {
|
||||
return buildMarkdownMentionOptions({
|
||||
agents,
|
||||
projects,
|
||||
members: companyMembers?.users,
|
||||
});
|
||||
}, [agents, companyMembers?.users, projects]);
|
||||
|
||||
const createRoutine = useMutation({
|
||||
mutationFn: () =>
|
||||
routinesApi.create(selectedCompanyId!, buildRoutineMutationPayload(draft)),
|
||||
@@ -896,6 +911,7 @@ export function Routines() {
|
||||
placeholder="Add instructions..."
|
||||
bordered={false}
|
||||
contentClassName="min-h-[160px] text-sm text-muted-foreground"
|
||||
mentions={mentionOptions}
|
||||
onSubmit={() => {
|
||||
if (!createRoutine.isPending && draft.title.trim() && draft.projectId && draft.assigneeAgentId) {
|
||||
createRoutine.mutate();
|
||||
|
||||
Reference in new issue
Block a user