mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
Compare commits
1 Commits
agent/lamb
...
agent/naiy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c88cd974 |
@@ -26,7 +26,10 @@ vi.mock("@/features/workspace", () => ({
|
||||
useWorkspaceStore: (selector: (s: any) => any) =>
|
||||
selector({
|
||||
hydrateWorkspace: mockHydrateWorkspace,
|
||||
workspace: { slug: "test" },
|
||||
}),
|
||||
useWorkspacePath: () => (path: string) => `/test${path}`,
|
||||
stripWorkspaceSlug: (pathname: string) => pathname.replace(/^\/[^/]+/, "") || "/",
|
||||
}));
|
||||
|
||||
// Mock api
|
||||
|
||||
@@ -51,14 +51,15 @@ function LoginPageContent() {
|
||||
const sendCode = useAuthStore((s) => s.sendCode);
|
||||
const verifyCode = useAuthStore((s) => s.verifyCode);
|
||||
const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Already authenticated — redirect to dashboard
|
||||
useEffect(() => {
|
||||
if (!isLoading && user && !searchParams.get("cli_callback")) {
|
||||
router.replace(searchParams.get("next") || "/issues");
|
||||
if (!isLoading && user && workspace && !searchParams.get("cli_callback")) {
|
||||
router.replace(searchParams.get("next") || `/${workspace.slug}/issues`);
|
||||
}
|
||||
}, [isLoading, user, router, searchParams]);
|
||||
}, [isLoading, user, workspace, router, searchParams]);
|
||||
|
||||
const [step, setStep] = useState<"email" | "code" | "cli_confirm">("email");
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -153,8 +154,9 @@ function LoginPageContent() {
|
||||
|
||||
await verifyCode(email, value);
|
||||
const wsList = await api.listWorkspaces();
|
||||
await hydrateWorkspace(wsList);
|
||||
router.push(searchParams.get("next") || "/issues");
|
||||
const ws = await hydrateWorkspace(wsList);
|
||||
const defaultPath = ws ? `/${ws.slug}/issues` : "/issues";
|
||||
router.push(searchParams.get("next") || defaultPath);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Invalid or expired code"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDefaultLayout } from "react-resizable-panels";
|
||||
import { useInboxStore } from "@/features/inbox";
|
||||
import { IssueDetail, StatusIcon, PriorityIcon } from "@/features/issues/components";
|
||||
import { STATUS_CONFIG, PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import { useActorName } from "@/features/workspace";
|
||||
import { useActorName, useWorkspaceStore } from "@/features/workspace";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -219,9 +219,11 @@ function InboxListItem({
|
||||
|
||||
export default function InboxPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const workspaceSlug = useWorkspaceStore((s) => s.workspace?.slug);
|
||||
const selectedKey = searchParams.get("issue") ?? "";
|
||||
const setSelectedKey = (key: string) => {
|
||||
const url = key ? `/inbox?issue=${key}` : "/inbox";
|
||||
const base = workspaceSlug ? `/${workspaceSlug}/inbox` : "/inbox";
|
||||
const url = key ? `${base}?issue=${key}` : base;
|
||||
window.history.replaceState(null, "", url);
|
||||
};
|
||||
|
||||
@@ -60,6 +60,8 @@ vi.mock("@/features/workspace", () => ({
|
||||
},
|
||||
getActorAvatarUrl: () => null,
|
||||
}),
|
||||
useWorkspacePath: () => (path: string) => `/test${path}`,
|
||||
stripWorkspaceSlug: (pathname: string) => pathname.replace(/^\/[^/]+/, "") || "/",
|
||||
}));
|
||||
|
||||
// Mock issue store — supply a stable full issue object so storeIssue
|
||||
@@ -357,6 +359,6 @@ describe("IssueDetailPage", () => {
|
||||
});
|
||||
|
||||
const wsLink = screen.getByText("Test WS");
|
||||
expect(wsLink.closest("a")).toHaveAttribute("href", "/issues");
|
||||
expect(wsLink.closest("a")).toHaveAttribute("href", "/test/issues");
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,8 @@ vi.mock("@/features/workspace", () => ({
|
||||
{ getState: () => ({ workspace: { id: "ws-1", name: "Test", slug: "test" }, agents: [], members: [] }) },
|
||||
),
|
||||
WorkspaceAvatar: ({ name }: { name: string }) => <span>{name.charAt(0)}</span>,
|
||||
useWorkspacePath: () => (path: string) => `/test${path}`,
|
||||
stripWorkspaceSlug: (pathname: string) => pathname.replace(/^\/[^/]+/, "") || "/",
|
||||
}));
|
||||
|
||||
// Mock WebSocket context
|
||||
42
apps/web/app/(dashboard)/[workspaceSlug]/layout.tsx
Normal file
42
apps/web/app/(dashboard)/[workspaceSlug]/layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { use, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { MulticaIcon } from "@/components/multica-icon";
|
||||
|
||||
export default function WorkspaceSlugLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ workspaceSlug: string }>;
|
||||
}) {
|
||||
const { workspaceSlug } = use(params);
|
||||
const router = useRouter();
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const workspaces = useWorkspaceStore((s) => s.workspaces);
|
||||
const switchWorkspace = useWorkspaceStore((s) => s.switchWorkspace);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) return;
|
||||
if (workspace.slug === workspaceSlug) return;
|
||||
|
||||
const target = workspaces.find((ws) => ws.slug === workspaceSlug);
|
||||
if (target) {
|
||||
switchWorkspace(target.id);
|
||||
} else {
|
||||
router.replace(`/${workspace.slug}/issues`);
|
||||
}
|
||||
}, [workspaceSlug, workspace, workspaces, switchWorkspace, router]);
|
||||
|
||||
if (!workspace || workspace.slug !== workspaceSlug) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<MulticaIcon className="size-6 animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SquarePen,
|
||||
CircleUser,
|
||||
} from "lucide-react";
|
||||
import { WorkspaceAvatar } from "@/features/workspace";
|
||||
import { WorkspaceAvatar, useWorkspacePath, stripWorkspaceSlug } from "@/features/workspace";
|
||||
import { useIssueDraftStore } from "@/features/issues/stores/draft-store";
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -71,7 +71,7 @@ export function AppSidebar() {
|
||||
const authLogout = useAuthStore((s) => s.logout);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const workspaces = useWorkspaceStore((s) => s.workspaces);
|
||||
const switchWorkspace = useWorkspaceStore((s) => s.switchWorkspace);
|
||||
const wp = useWorkspacePath();
|
||||
|
||||
const unreadCount = useInboxStore((s) => s.unreadCount());
|
||||
|
||||
@@ -132,7 +132,7 @@ export function AppSidebar() {
|
||||
key={ws.id}
|
||||
onClick={() => {
|
||||
if (ws.id !== workspace?.id) {
|
||||
switchWorkspace(ws.id);
|
||||
router.push(`/${ws.slug}/issues`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -174,12 +174,13 @@ export function AppSidebar() {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{primaryNav.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
const subPath = stripWorkspaceSlug(pathname);
|
||||
const isActive = subPath === item.href;
|
||||
return (
|
||||
<SidebarMenuItem key={item.href}>
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
render={<Link href={item.href} />}
|
||||
render={<Link href={wp(item.href)} />}
|
||||
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
|
||||
>
|
||||
<item.icon />
|
||||
@@ -201,12 +202,13 @@ export function AppSidebar() {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{workspaceNav.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
const subPath = stripWorkspaceSlug(pathname);
|
||||
const isActive = subPath === item.href;
|
||||
return (
|
||||
<SidebarMenuItem key={item.href}>
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
render={<Link href={item.href} />}
|
||||
render={<Link href={wp(item.href)} />}
|
||||
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
|
||||
>
|
||||
<item.icon />
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PriorityPicker, AssigneePicker, DueDatePicker } from "./pickers";
|
||||
import { PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import type { CardProperties } from "@/features/issues/stores/view-store";
|
||||
import { useViewStore } from "@/features/issues/stores/view-store-context";
|
||||
import { useWorkspacePath } from "@/features/workspace";
|
||||
|
||||
function formatDate(date: string): string {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
@@ -169,6 +170,7 @@ export const BoardCardContent = memo(function BoardCardContent({
|
||||
});
|
||||
|
||||
export const DraggableBoardCard = memo(function DraggableBoardCard({ issue }: { issue: Issue }) {
|
||||
const wp = useWorkspacePath();
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -195,7 +197,7 @@ export const DraggableBoardCard = memo(function DraggableBoardCard({ issue }: {
|
||||
className={isDragging ? "opacity-30" : ""}
|
||||
>
|
||||
<Link
|
||||
href={`/issues/${issue.id}`}
|
||||
href={wp(`/issues/${issue.id}`)}
|
||||
className={`group block transition-colors ${isDragging ? "pointer-events-none" : ""}`}
|
||||
>
|
||||
<BoardCardContent issue={issue} editable />
|
||||
|
||||
@@ -63,7 +63,7 @@ import { CommentInput } from "./comment-input";
|
||||
import { AgentLiveCard, TaskRunHistory } from "./agent-live-card";
|
||||
import { api } from "@/shared/api";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useWorkspaceStore, useActorName, useWorkspacePath } from "@/features/workspace";
|
||||
import { useIssueStore } from "@/features/issues";
|
||||
import { useIssueTimeline } from "@/features/issues/hooks/use-issue-timeline";
|
||||
import { useIssueReactions } from "@/features/issues/hooks/use-issue-reactions";
|
||||
@@ -169,6 +169,7 @@ interface IssueDetailProps {
|
||||
export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layoutId = "multica_issue_detail_layout" }: IssueDetailProps) {
|
||||
const id = issueId;
|
||||
const router = useRouter();
|
||||
const wp = useWorkspacePath();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
@@ -256,7 +257,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
useIssueStore.getState().removeIssue(issue!.id);
|
||||
toast.success("Issue deleted");
|
||||
if (onDelete) onDelete();
|
||||
else router.push("/issues");
|
||||
else router.push(wp("/issues"));
|
||||
} catch {
|
||||
toast.error("Failed to delete issue");
|
||||
setDeleting(false);
|
||||
@@ -276,7 +277,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
<div className="flex flex-1 min-h-0 flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
<p>This issue does not exist or has been deleted in this workspace.</p>
|
||||
{!onDelete && (
|
||||
<Button variant="outline" size="sm" onClick={() => router.push("/issues")}>
|
||||
<Button variant="outline" size="sm" onClick={() => router.push(wp("/issues"))}>
|
||||
<ChevronLeft className="mr-1 h-3.5 w-3.5" />
|
||||
Back to Issues
|
||||
</Button>
|
||||
@@ -296,7 +297,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
{workspace && (
|
||||
<>
|
||||
<Link
|
||||
href="/issues"
|
||||
href={wp("/issues")}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors truncate shrink-0"
|
||||
>
|
||||
{workspace.name}
|
||||
@@ -322,7 +323,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
disabled={!prevIssue}
|
||||
onClick={() => prevIssue && router.push(`/issues/${prevIssue.id}`)}
|
||||
onClick={() => prevIssue && router.push(wp(`/issues/${prevIssue.id}`))}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -341,7 +342,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
disabled={!nextIssue}
|
||||
onClick={() => nextIssue && router.push(`/issues/${nextIssue.id}`)}
|
||||
onClick={() => nextIssue && router.push(wp(`/issues/${nextIssue.id}`))}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useIssueStore } from "@/features/issues/store";
|
||||
import { useWorkspacePath } from "@/features/workspace";
|
||||
import { StatusIcon } from "./status-icon";
|
||||
|
||||
interface IssueMentionCardProps {
|
||||
@@ -11,12 +12,13 @@ interface IssueMentionCardProps {
|
||||
}
|
||||
|
||||
export function IssueMentionCard({ issueId, fallbackLabel }: IssueMentionCardProps) {
|
||||
const wp = useWorkspacePath();
|
||||
const issue = useIssueStore((s) => s.issues.find((i) => i.id === issueId));
|
||||
|
||||
if (!issue) {
|
||||
return (
|
||||
<Link
|
||||
href={`/issues/${issueId}`}
|
||||
href={wp(`/issues/${issueId}`)}
|
||||
className="text-primary font-medium cursor-pointer hover:underline"
|
||||
>
|
||||
{fallbackLabel ?? issueId.slice(0, 8)}
|
||||
@@ -26,7 +28,7 @@ export function IssueMentionCard({ issueId, fallbackLabel }: IssueMentionCardPro
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/issues/${issueId}`}
|
||||
href={wp(`/issues/${issueId}`)}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-sm hover:bg-accent transition-colors cursor-pointer no-underline"
|
||||
>
|
||||
<StatusIcon status={issue.status} className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import type { Issue } from "@/shared/types";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { useWorkspacePath } from "@/features/workspace";
|
||||
import { useIssueSelectionStore } from "@/features/issues/stores/selection-store";
|
||||
import { PriorityIcon } from "./priority-icon";
|
||||
|
||||
@@ -15,6 +16,7 @@ function formatDate(date: string): string {
|
||||
}
|
||||
|
||||
export const ListRow = memo(function ListRow({ issue }: { issue: Issue }) {
|
||||
const wp = useWorkspacePath();
|
||||
const selected = useIssueSelectionStore((s) => s.selectedIds.has(issue.id));
|
||||
const toggle = useIssueSelectionStore((s) => s.toggle);
|
||||
|
||||
@@ -39,7 +41,7 @@ export const ListRow = memo(function ListRow({ issue }: { issue: Issue }) {
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
href={`/issues/${issue.id}`}
|
||||
href={wp(`/issues/${issue.id}`)}
|
||||
className="flex flex-1 items-center gap-2 min-w-0"
|
||||
>
|
||||
<span className="w-16 shrink-0 text-xs text-muted-foreground">
|
||||
|
||||
@@ -29,7 +29,7 @@ import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich
|
||||
import { TitleEditor } from "@/components/common/title-editor";
|
||||
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useWorkspaceStore, useActorName, useWorkspacePath } from "@/features/workspace";
|
||||
import { useIssueStore } from "@/features/issues";
|
||||
import { useIssueDraftStore } from "@/features/issues/stores/draft-store";
|
||||
import { api } from "@/shared/api";
|
||||
@@ -67,6 +67,7 @@ function PillButton({
|
||||
|
||||
export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?: Record<string, unknown> | null }) {
|
||||
const router = useRouter();
|
||||
const wp = useWorkspacePath();
|
||||
const workspaceName = useWorkspaceStore((s) => s.workspace?.name);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
@@ -150,7 +151,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
type="button"
|
||||
className="ml-7 mt-2 text-sm text-primary hover:underline cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push(`/issues/${issue.id}`);
|
||||
router.push(wp(`/issues/${issue.id}`));
|
||||
toast.dismiss(t);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -18,6 +19,7 @@ import { useWorkspaceStore } from "@/features/workspace";
|
||||
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [slug, setSlug] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
@@ -43,14 +45,13 @@ export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
|
||||
if (!canSubmit) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const { createWorkspace, switchWorkspace } =
|
||||
useWorkspaceStore.getState();
|
||||
const { createWorkspace } = useWorkspaceStore.getState();
|
||||
const ws = await createWorkspace({
|
||||
name: name.trim(),
|
||||
slug: slug.trim(),
|
||||
});
|
||||
onClose();
|
||||
await switchWorkspace(ws.id);
|
||||
router.push(`/${ws.slug}/issues`);
|
||||
} catch {
|
||||
toast.error("Failed to create workspace");
|
||||
} finally {
|
||||
|
||||
@@ -17,7 +17,9 @@ export const useNavigationStore = create<NavigationState>()(
|
||||
|
||||
onPathChange: (path: string) => {
|
||||
if (!EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix))) {
|
||||
set({ lastPath: path });
|
||||
// Strip workspace slug — store only the sub-path
|
||||
const subPath = path.replace(/^\/[^/]+/, "") || "/issues";
|
||||
set({ lastPath: subPath });
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -40,3 +40,21 @@ export function useActorName() {
|
||||
|
||||
return { getMemberName, getAgentName, getActorName, getActorInitials, getActorAvatarUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that prefixes a path with the current workspace slug.
|
||||
* Usage: `const wp = useWorkspacePath(); wp("/issues")` → `"/my-workspace/issues"`
|
||||
*/
|
||||
export function useWorkspacePath() {
|
||||
const slug = useWorkspaceStore((s) => s.workspace?.slug);
|
||||
return (path: string) => (slug ? `/${slug}${path}` : path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the first path segment (workspace slug) from a pathname.
|
||||
* `"/my-workspace/issues"` → `"/issues"`
|
||||
* `"/my-workspace"` → `"/"`
|
||||
*/
|
||||
export function stripWorkspaceSlug(pathname: string): string {
|
||||
return pathname.replace(/^\/[^/]+/, "") || "/";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { useWorkspaceStore } from "./store";
|
||||
export { useActorName } from "./hooks";
|
||||
export { useActorName, useWorkspacePath, stripWorkspaceSlug } from "./hooks";
|
||||
export { WorkspaceAvatar } from "./components/workspace-avatar";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { loginAsDefault, openWorkspaceMenu } from "./helpers";
|
||||
import { loginAsDefault, openWorkspaceMenu, DEFAULT_E2E_WORKSPACE } from "./helpers";
|
||||
|
||||
test.describe("Authentication", () => {
|
||||
test("login page renders correctly", async ({ page }) => {
|
||||
@@ -27,7 +27,7 @@ test.describe("Authentication", () => {
|
||||
localStorage.removeItem("multica_workspace_id");
|
||||
});
|
||||
|
||||
await page.goto("/issues");
|
||||
await page.goto(`/${DEFAULT_E2E_WORKSPACE}/issues`);
|
||||
await page.waitForURL("**/login", { timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe("Comments", () => {
|
||||
|
||||
test("can add a comment on an issue", async ({ page }) => {
|
||||
// Wait for issues to load and click first one
|
||||
const issueLink = page.locator('a[href^="/issues/"]').first();
|
||||
const issueLink = page.locator('a[href*="/issues/"]').first();
|
||||
await expect(issueLink).toBeVisible({ timeout: 5000 });
|
||||
await issueLink.click();
|
||||
await page.waitForURL(/\/issues\/[\w-]+/);
|
||||
@@ -42,7 +42,7 @@ test.describe("Comments", () => {
|
||||
});
|
||||
|
||||
test("comment submit button is disabled when empty", async ({ page }) => {
|
||||
const issueLink = page.locator('a[href^="/issues/"]').first();
|
||||
const issueLink = page.locator('a[href*="/issues/"]').first();
|
||||
await expect(issueLink).toBeVisible({ timeout: 5000 });
|
||||
await issueLink.click();
|
||||
await page.waitForURL(/\/issues\/[\w-]+/);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TestApiClient } from "./fixtures";
|
||||
|
||||
const DEFAULT_E2E_NAME = "E2E User";
|
||||
const DEFAULT_E2E_EMAIL = "e2e@multica.ai";
|
||||
const DEFAULT_E2E_WORKSPACE = "e2e-workspace";
|
||||
export const DEFAULT_E2E_WORKSPACE = "e2e-workspace";
|
||||
|
||||
/**
|
||||
* Log in as the default E2E user and ensure the workspace exists first.
|
||||
@@ -20,8 +20,8 @@ export async function loginAsDefault(page: Page) {
|
||||
await page.evaluate((t) => {
|
||||
localStorage.setItem("multica_token", t);
|
||||
}, token);
|
||||
await page.goto("/issues");
|
||||
await page.waitForURL("**/issues", { timeout: 10000 });
|
||||
await page.goto(`/${DEFAULT_E2E_WORKSPACE}/issues`);
|
||||
await page.waitForURL(`**/${DEFAULT_E2E_WORKSPACE}/issues`, { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe("Issues", () => {
|
||||
await expect(page.locator("text=All Issues")).toBeVisible();
|
||||
|
||||
// Navigate to the issue detail
|
||||
const issueLink = page.locator(`a[href="/issues/${issue.id}"]`);
|
||||
const issueLink = page.locator(`a[href$="/issues/${issue.id}"]`);
|
||||
await expect(issueLink).toBeVisible({ timeout: 5000 });
|
||||
await issueLink.click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user