feat(ai-analysis): 添加详情页智能分析入口
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
This commit is contained in:
@@ -10,6 +10,10 @@ import { useOvertimeStore } from '@/stores/useOvertimeStore';
|
|||||||
import { RequirementTable } from '@/components/product/RequirementTable';
|
import { RequirementTable } from '@/components/product/RequirementTable';
|
||||||
import { RequirementForm } from '@/components/product/RequirementForm';
|
import { RequirementForm } from '@/components/product/RequirementForm';
|
||||||
import { ProductForm } from '@/components/product/ProductForm';
|
import { ProductForm } from '@/components/product/ProductForm';
|
||||||
|
import { AnalysisContextDrawer } from '@/components/analysis/AnalysisContextDrawer';
|
||||||
|
import { AnalysisEntryButton } from '@/components/analysis/AnalysisEntryButton';
|
||||||
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
|
import { useMemberStore } from '@/stores/useMemberStore';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: 'requirements' as const, label: '需求池' },
|
{ key: 'requirements' as const, label: '需求池' },
|
||||||
@@ -29,10 +33,13 @@ export default function ProductDetailPage() {
|
|||||||
deleteRequirement,
|
deleteRequirement,
|
||||||
} = useRequirementStore();
|
} = useRequirementStore();
|
||||||
const { records, fetchRecords } = useOvertimeStore();
|
const { records, fetchRecords } = useOvertimeStore();
|
||||||
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const roles = useMemberStore((state) => state.roles);
|
||||||
|
|
||||||
const [showReqForm, setShowReqForm] = useState(false);
|
const [showReqForm, setShowReqForm] = useState(false);
|
||||||
const [editingReq, setEditingReq] = useState<any>(null);
|
const [editingReq, setEditingReq] = useState<any>(null);
|
||||||
const [editingProduct, setEditingProduct] = useState(false);
|
const [editingProduct, setEditingProduct] = useState(false);
|
||||||
|
const [showAnalysisDrawer, setShowAnalysisDrawer] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<'requirements' | 'projects' | 'versions'>('requirements');
|
const [activeTab, setActiveTab] = useState<'requirements' | 'projects' | 'versions'>('requirements');
|
||||||
const [reqStatusFilter, setReqStatusFilter] = useState<RequirementStatus | null>(null);
|
const [reqStatusFilter, setReqStatusFilter] = useState<RequirementStatus | null>(null);
|
||||||
|
|
||||||
@@ -47,6 +54,10 @@ export default function ProductDetailPage() {
|
|||||||
if (reqStatusFilter) list = list.filter((r) => r.status === reqStatusFilter);
|
if (reqStatusFilter) list = list.filter((r) => r.status === reqStatusFilter);
|
||||||
return list;
|
return list;
|
||||||
}, [requirements, productId, reqStatusFilter]);
|
}, [requirements, productId, reqStatusFilter]);
|
||||||
|
const currentPermissions = useMemo(
|
||||||
|
() => roles.find((role) => role.id === user?.roleId)?.permissions ?? [],
|
||||||
|
[roles, user?.roleId],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterChange = (status: RequirementStatus | null) => {
|
const handleFilterChange = (status: RequirementStatus | null) => {
|
||||||
setReqStatusFilter(status);
|
setReqStatusFilter(status);
|
||||||
@@ -93,7 +104,8 @@ export default function ProductDetailPage() {
|
|||||||
{currentProduct.name}
|
{currentProduct.name}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
<AnalysisEntryButton onClick={() => setShowAnalysisDrawer(true)} />
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingProduct(true)}
|
onClick={() => setEditingProduct(true)}
|
||||||
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12px] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12px] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
||||||
@@ -215,6 +227,14 @@ export default function ProductDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AnalysisContextDrawer
|
||||||
|
open={showAnalysisDrawer}
|
||||||
|
title={`产品智能分析 · ${currentProduct.name}`}
|
||||||
|
context={{ surface: 'product_detail', productId: currentProduct.id }}
|
||||||
|
permissions={currentPermissions}
|
||||||
|
onClose={() => setShowAnalysisDrawer(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { calcGroupProgress as calcDevTaskProgress, aggregateDevTaskHours } from
|
|||||||
import { CapsuleStages } from '@/components/version/CapsuleStages';
|
import { CapsuleStages } from '@/components/version/CapsuleStages';
|
||||||
import { MemberChips } from '@/components/version/MemberChips';
|
import { MemberChips } from '@/components/version/MemberChips';
|
||||||
import { ProjectMemberPanel } from '@/components/project/ProjectMemberPanel';
|
import { ProjectMemberPanel } from '@/components/project/ProjectMemberPanel';
|
||||||
|
import { AnalysisContextDrawer } from '@/components/analysis/AnalysisContextDrawer';
|
||||||
|
import { AnalysisEntryButton } from '@/components/analysis/AnalysisEntryButton';
|
||||||
import { getRequirementCoverageSummary, type VersionPlan } from '@/lib/version-plan';
|
import { getRequirementCoverageSummary, type VersionPlan } from '@/lib/version-plan';
|
||||||
import { buildVersionTimelineSummary, calcStageEffortMetrics, formatVersionOverviewDateTime, getVersionCardDefaultExpanded, mergeStageProgressWithEffort } from '@/lib/version-overview';
|
import { buildVersionTimelineSummary, calcStageEffortMetrics, formatVersionOverviewDateTime, getVersionCardDefaultExpanded, mergeStageProgressWithEffort } from '@/lib/version-overview';
|
||||||
import { calcScopedVersionProgress } from '@/lib/version-progress';
|
import { calcScopedVersionProgress } from '@/lib/version-progress';
|
||||||
@@ -327,6 +329,7 @@ export default function ProjectDetailPage() {
|
|||||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||||
const { bugs, fetchBugs } = useBugStore();
|
const { bugs, fetchBugs } = useBugStore();
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [showAnalysisDrawer, setShowAnalysisDrawer] = useState(false);
|
||||||
|
|
||||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||||
@@ -340,6 +343,10 @@ export default function ProjectDetailPage() {
|
|||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const currentUserName = user?.name || '';
|
const currentUserName = user?.name || '';
|
||||||
const { roles } = useMemberStore();
|
const { roles } = useMemberStore();
|
||||||
|
const currentPermissions = useMemo(
|
||||||
|
() => roles.find((role) => role.id === user?.roleId)?.permissions ?? [],
|
||||||
|
[roles, user?.roleId],
|
||||||
|
);
|
||||||
const isSuperAdmin = useMemo(() => {
|
const isSuperAdmin = useMemo(() => {
|
||||||
const r = roles.find((x) => x.id === user?.roleId);
|
const r = roles.find((x) => x.id === user?.roleId);
|
||||||
return !!r && r.permissions.includes('*');
|
return !!r && r.permissions.includes('*');
|
||||||
@@ -435,8 +442,11 @@ export default function ProjectDetailPage() {
|
|||||||
<span className="ml-2 text-[var(--ink-muted)]">/</span>
|
<span className="ml-2 text-[var(--ink-muted)]">/</span>
|
||||||
<span className="ml-2 text-[15px] font-semibold text-[var(--ink)]">{project.name}</span>
|
<span className="ml-2 text-[15px] font-semibold text-[var(--ink)]">{project.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-2.5 py-1 text-xs text-[var(--ink-soft)]">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="h-3 w-3" /><span>{project.productName}</span>
|
<AnalysisEntryButton onClick={() => setShowAnalysisDrawer(true)} />
|
||||||
|
<div className="flex items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-2.5 py-1 text-xs text-[var(--ink-soft)]">
|
||||||
|
<Package className="h-3 w-3" /><span>{project.productName}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -482,6 +492,13 @@ export default function ProjectDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AnalysisContextDrawer
|
||||||
|
open={showAnalysisDrawer}
|
||||||
|
title={`项目智能分析 · ${project.name}`}
|
||||||
|
context={{ surface: 'project_detail', projectId: project.id }}
|
||||||
|
permissions={currentPermissions}
|
||||||
|
onClose={() => setShowAnalysisDrawer(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { PlanTab } from '@/components/version/PlanTab';
|
|||||||
import { DevTaskTab } from '@/components/dev-task/DevTaskTab';
|
import { DevTaskTab } from '@/components/dev-task/DevTaskTab';
|
||||||
import { TestCaseTab } from '@/components/test-case/TestCaseTab';
|
import { TestCaseTab } from '@/components/test-case/TestCaseTab';
|
||||||
import { BugTab } from '@/components/bug/BugTab';
|
import { BugTab } from '@/components/bug/BugTab';
|
||||||
|
import { AnalysisContextDrawer } from '@/components/analysis/AnalysisContextDrawer';
|
||||||
|
import { AnalysisEntryButton } from '@/components/analysis/AnalysisEntryButton';
|
||||||
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
||||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||||
@@ -135,6 +137,7 @@ export default function VersionDetailPage() {
|
|||||||
const { departments, members: allMembers, roles, fetchMembers } = useMemberStore();
|
const { departments, members: allMembers, roles, fetchMembers } = useMemberStore();
|
||||||
const { categories: taskCategories, fetchCategories } = useTaskCategoryStore();
|
const { categories: taskCategories, fetchCategories } = useTaskCategoryStore();
|
||||||
const currentRole = useMemo(() => roles.find((r) => r.id === user?.roleId), [roles, user?.roleId]);
|
const currentRole = useMemo(() => roles.find((r) => r.id === user?.roleId), [roles, user?.roleId]);
|
||||||
|
const currentPermissions = currentRole?.permissions ?? [];
|
||||||
const memberCandidates = useMemo(
|
const memberCandidates = useMemo(
|
||||||
() => allMembers.map((member) => ({
|
() => allMembers.map((member) => ({
|
||||||
id: member.id,
|
id: member.id,
|
||||||
@@ -157,6 +160,7 @@ export default function VersionDetailPage() {
|
|||||||
const [showRecommendModal, setShowRecommendModal] = useState(false);
|
const [showRecommendModal, setShowRecommendModal] = useState(false);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showReleaseModal, setShowReleaseModal] = useState(false);
|
const [showReleaseModal, setShowReleaseModal] = useState(false);
|
||||||
|
const [showAnalysisDrawer, setShowAnalysisDrawer] = useState(false);
|
||||||
const [recommendationDataReady, setRecommendationDataReady] = useState(false);
|
const [recommendationDataReady, setRecommendationDataReady] = useState(false);
|
||||||
const [v22Scope, setV22Scope] = useState<VersionDataScope | null>(null);
|
const [v22Scope, setV22Scope] = useState<VersionDataScope | null>(null);
|
||||||
|
|
||||||
@@ -432,7 +436,10 @@ export default function VersionDetailPage() {
|
|||||||
<span className="ml-1.5 text-[var(--ink-muted)]">/</span>
|
<span className="ml-1.5 text-[var(--ink-muted)]">/</span>
|
||||||
<span className="ml-1.5 text-[15px] font-semibold text-[var(--ink)]">{version.name}</span>
|
<span className="ml-1.5 text-[15px] font-semibold text-[var(--ink)]">{version.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">{renderActions()}</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<AnalysisEntryButton onClick={() => setShowAnalysisDrawer(true)} />
|
||||||
|
{renderActions()}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
@@ -1184,6 +1191,13 @@ export default function VersionDetailPage() {
|
|||||||
onClose={() => setShowMemberModal(false)}
|
onClose={() => setShowMemberModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<AnalysisContextDrawer
|
||||||
|
open={showAnalysisDrawer}
|
||||||
|
title={`版本智能分析 · ${version.name}`}
|
||||||
|
context={{ surface: 'version_detail', versionId: version.id }}
|
||||||
|
permissions={currentPermissions}
|
||||||
|
onClose={() => setShowAnalysisDrawer(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
107
apps/web/components/analysis/AnalysisContextDrawer.tsx
Normal file
107
apps/web/components/analysis/AnalysisContextDrawer.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type FormEvent, useState } from 'react';
|
||||||
|
import type { AnalysisRequest, AnalysisResponse } from '@ftb/shared';
|
||||||
|
import { Loader2, Send, X } from 'lucide-react';
|
||||||
|
import { requestAnalysis } from '@/lib/analysis-api';
|
||||||
|
import { AnalysisResultBlock } from './AnalysisResultBlock';
|
||||||
|
|
||||||
|
export function AnalysisContextDrawer({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
context,
|
||||||
|
permissions,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
context: NonNullable<AnalysisRequest['context']>;
|
||||||
|
permissions: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [question, setQuestion] = useState('');
|
||||||
|
const [response, setResponse] = useState<AnalysisResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
async function ask(prompt: string) {
|
||||||
|
const value = prompt.trim();
|
||||||
|
if (!value || loading) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await requestAnalysis({ question: value, context }, permissions);
|
||||||
|
setResponse(result);
|
||||||
|
setQuestion('');
|
||||||
|
} catch {
|
||||||
|
setResponse({
|
||||||
|
ok: false,
|
||||||
|
code: 'AI_UNAVAILABLE',
|
||||||
|
message: '业务分析暂不可用,请稍后再试。',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
void ask(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex justify-end bg-black/40">
|
||||||
|
<aside className="flex h-full w-full max-w-2xl flex-col border-l border-white/60 bg-[#f8fafc]/90 shadow-2xl backdrop-blur-xl">
|
||||||
|
<header className="flex h-14 shrink-0 items-center justify-between border-b border-white/70 px-5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-[13px] font-semibold text-[#0f172a]">{title}</p>
|
||||||
|
<p className="mt-0.5 text-[11px] text-[#64748b]">只读取当前上下文和已有权限数据,安全范围以后端为准。</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="ml-3 rounded-full p-2 text-[#64748b] transition-colors hover:bg-white hover:text-[#0f172a]"
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-5">
|
||||||
|
<form
|
||||||
|
onSubmit={submit}
|
||||||
|
className="rounded-[28px] border border-white/70 bg-white/80 p-3 shadow-[0_18px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
value={question}
|
||||||
|
onChange={(event) => setQuestion(event.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full resize-none bg-transparent px-2 py-2 text-[14px] leading-6 text-[#0f172a] outline-none placeholder:text-[#94a3b8]"
|
||||||
|
placeholder="例如:这个版本风险怎么样?需求完成趋势怎么样?"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between gap-3 px-1">
|
||||||
|
<span className="text-[11px] text-[#64748b]">不会创建、修改业务数据,也不会触发状态流转。</span>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !question.trim()}
|
||||||
|
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full bg-[#0f172a] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#1e293b] disabled:cursor-not-allowed disabled:bg-[#cbd5e1]"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||||
|
{loading ? '分析中' : '开始分析'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{response ? (
|
||||||
|
<AnalysisResultBlock response={response} onAsk={(prompt) => void ask(prompt)} />
|
||||||
|
) : (
|
||||||
|
<div className="rounded-[28px] border border-dashed border-[#dbe3ef] bg-white/55 p-6 text-[13px] leading-7 text-[#64748b]">
|
||||||
|
可以围绕当前页面提问,例如进度、风险、需求分布、成员负载或质量情况。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
apps/web/components/analysis/AnalysisEntryButton.tsx
Normal file
25
apps/web/components/analysis/AnalysisEntryButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BarChart3, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
export function AnalysisEntryButton({
|
||||||
|
onClick,
|
||||||
|
compact = false,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
compact?: boolean;
|
||||||
|
}) {
|
||||||
|
const Icon = compact ? BarChart3 : Sparkles;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className="inline-flex h-8 items-center gap-1.5 rounded-full border border-[#dbe3ef] bg-white/75 px-3 text-[12px] font-medium text-[#334155] shadow-sm backdrop-blur transition-colors hover:bg-white"
|
||||||
|
title="智能分析"
|
||||||
|
>
|
||||||
|
<Icon className="h-3.5 w-3.5" />
|
||||||
|
智能分析
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
apps/web/lib/analysis-context-entrypoints.test.ts
Normal file
44
apps/web/lib/analysis-context-entrypoints.test.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
function readSource(path: string): string {
|
||||||
|
assert.equal(existsSync(path), true, `${path} should exist`);
|
||||||
|
return readFileSync(path, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('analysis context drawer is read-only and reuses the shared analysis result block', () => {
|
||||||
|
const entryButton = readSource('components/analysis/AnalysisEntryButton.tsx');
|
||||||
|
const drawer = readSource('components/analysis/AnalysisContextDrawer.tsx');
|
||||||
|
|
||||||
|
assert.match(entryButton, /export function AnalysisEntryButton/);
|
||||||
|
assert.match(entryButton, /智能分析/);
|
||||||
|
assert.match(drawer, /requestAnalysis/);
|
||||||
|
assert.match(drawer, /AnalysisResultBlock/);
|
||||||
|
assert.match(drawer, /只读取当前上下文和已有权限数据/);
|
||||||
|
assert.match(drawer, /permissions: string\[\]/);
|
||||||
|
assert.doesNotMatch(drawer, /create(Product|Project|Version|Requirement|Plan|Task|TestCase|Bug)/);
|
||||||
|
assert.doesNotMatch(drawer, /update(Product|Project|Version|Requirement|Plan|Task|TestCase|Bug)/);
|
||||||
|
assert.doesNotMatch(drawer, /delete(Product|Project|Version|Requirement|Plan|Task|TestCase|Bug)/);
|
||||||
|
assert.doesNotMatch(drawer, /use(Product|Requirement|VersionPlan|DevTask|TestCase|Bug)Store/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('product, project, and version detail pages wire analysis drawer with scoped contexts', () => {
|
||||||
|
const productPage = readSource('app/products/[id]/page.tsx');
|
||||||
|
const projectPage = readSource('app/projects/[id]/page.tsx');
|
||||||
|
const versionPage = readSource('app/versions/[id]/page.tsx');
|
||||||
|
|
||||||
|
for (const source of [productPage, projectPage, versionPage]) {
|
||||||
|
assert.match(source, /AnalysisEntryButton/);
|
||||||
|
assert.match(source, /AnalysisContextDrawer/);
|
||||||
|
assert.match(source, /showAnalysisDrawer/);
|
||||||
|
assert.match(source, /currentPermissions/);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(productPage, /surface: 'product_detail'/);
|
||||||
|
assert.match(productPage, /productId: currentProduct\.id/);
|
||||||
|
assert.match(projectPage, /surface: 'project_detail'/);
|
||||||
|
assert.match(projectPage, /projectId: project\.id/);
|
||||||
|
assert.match(versionPage, /surface: 'version_detail'/);
|
||||||
|
assert.match(versionPage, /versionId: version\.id/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user