diff --git a/backend/app/api/v1/faq_categories.py b/backend/app/api/v1/faq_categories.py
index 907541c3..4523beaa 100644
--- a/backend/app/api/v1/faq_categories.py
+++ b/backend/app/api/v1/faq_categories.py
@@ -42,6 +42,10 @@ async def create_category(
existing = await category_crud.get_category_by_name(db, payload.study_id, payload.name)
if existing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在")
+ if payload.icon:
+ dup = await category_crud.get_category_by_icon(db, payload.study_id, payload.icon)
+ if dup:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该图标已被其他分类使用")
category = await category_crud.create_category(db, payload)
await audit_crud.log_action(
db,
@@ -65,13 +69,12 @@ async def create_category(
)
async def list_categories(
study_id: uuid.UUID | None = None,
- is_active: bool | None = True,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> list[CategoryRead]:
if not study_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID")
- categories = await category_crud.list_categories(db, study_id, include_global=False, is_active=is_active)
+ categories = await category_crud.list_categories(db, study_id, include_global=False)
return paginate([CategoryRead.model_validate(c) for c in categories], total=len(categories))
@@ -102,6 +105,10 @@ async def update_category(
existing = await category_crud.get_category_by_name(db, target_study_id, target_name)
if existing and existing.id != category.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在")
+ if payload.icon:
+ dup = await category_crud.get_category_by_icon(db, target_study_id, payload.icon)
+ if dup and dup.id != category.id:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该图标已被其他分类使用")
if not target_study_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID")
updated = await category_crud.update_category(db, category, payload)
diff --git a/backend/app/api/v1/faqs.py b/backend/app/api/v1/faqs.py
index 72fed2e8..b36f1c49 100644
--- a/backend/app/api/v1/faqs.py
+++ b/backend/app/api/v1/faqs.py
@@ -90,7 +90,6 @@ async def list_faqs(
study_id: uuid.UUID | None = None,
category_id: uuid.UUID | None = None,
keyword: str | None = None,
- is_active: bool | None = None,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> list[FaqRead]:
@@ -111,17 +110,12 @@ async def list_faqs(
member = await member_crud.get_member(db, study_id, current_user.id)
if not member or not member.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
- if is_active is False:
- allowed = await role_has_api_permission(db, study_id, member.role_in_study, "faq:update", check_prerequisites=False)
- if not allowed:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
items = await faq_crud.list_items(
db,
study_id=study_id,
category_id=category_id,
keyword=keyword,
- is_active=is_active,
study_scope="project",
)
@@ -131,10 +125,6 @@ async def list_faqs(
role = await _get_member_role(it.study_id)
if not role:
continue
- if not it.is_active and not _is_system_admin(current_user):
- allowed = await role_has_api_permission(db, it.study_id, role, "faq:update", check_prerequisites=False)
- if not allowed:
- continue
visible.append(FaqRead.model_validate(it))
return paginate(visible, total=len(visible))
@@ -156,13 +146,6 @@ async def get_faq(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
if not item.study_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
- if not item.is_active and not is_system_admin(current_user):
- member = await member_crud.get_member(db, item.study_id, current_user.id)
- if not member or not member.is_active:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
- allowed = await role_has_api_permission(db, item.study_id, member.role_in_study, "faq:update", check_prerequisites=False)
- if not allowed:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
return FaqRead.model_validate(item)
@@ -185,13 +168,9 @@ async def update_faq(
item = await faq_crud.get_item(db, item_id)
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
- old_active = item.is_active
updated = await faq_crud.update_item(db, item, payload)
action = "UPDATE_FAQ_ITEM"
detail = "FAQ updated"
- if payload.is_active is not None and payload.is_active != old_active:
- action = "FAQ_STATUS_CHANGE"
- detail = "FAQ disabled" if not payload.is_active else "FAQ enabled"
await audit_crud.log_action(
db,
study_id=item.study_id,
diff --git a/backend/app/crud/faq_category.py b/backend/app/crud/faq_category.py
index 5b90274a..05f4b67b 100644
--- a/backend/app/crud/faq_category.py
+++ b/backend/app/crud/faq_category.py
@@ -14,7 +14,7 @@ async def create_category(db: AsyncSession, category_in: CategoryCreate) -> FaqC
name=category_in.name,
description=category_in.description,
sort_order=category_in.sort_order,
- is_active=category_in.is_active,
+ icon=category_in.icon,
)
db.add(category)
await db.commit()
@@ -26,15 +26,12 @@ async def list_categories(
db: AsyncSession,
study_id: uuid.UUID | None,
include_global: bool = True,
- is_active: bool | None = True,
) -> Sequence[FaqCategory]:
stmt = select(FaqCategory)
if include_global:
stmt = stmt.where((FaqCategory.study_id == study_id) | (FaqCategory.study_id.is_(None)))
else:
stmt = stmt.where(FaqCategory.study_id == study_id)
- if is_active is not None:
- stmt = stmt.where(FaqCategory.is_active == is_active)
stmt = stmt.order_by(FaqCategory.sort_order, FaqCategory.name)
result = await db.execute(stmt)
return result.scalars().all()
@@ -59,6 +56,22 @@ async def get_category_by_name(
return result.scalar_one_or_none()
+async def get_category_by_icon(
+ db: AsyncSession,
+ study_id: uuid.UUID | None,
+ icon: str,
+) -> FaqCategory | None:
+ if not icon:
+ return None
+ stmt = select(FaqCategory).where(FaqCategory.icon == icon)
+ if study_id is None:
+ stmt = stmt.where(FaqCategory.study_id.is_(None))
+ else:
+ stmt = stmt.where(FaqCategory.study_id == study_id)
+ result = await db.execute(stmt)
+ return result.scalar_one_or_none()
+
+
async def update_category(db: AsyncSession, category: FaqCategory, category_in: CategoryUpdate) -> FaqCategory:
update_data = category_in.model_dump(exclude_unset=True)
if update_data:
diff --git a/backend/app/crud/faq_item.py b/backend/app/crud/faq_item.py
index 44d726d9..96a0531e 100644
--- a/backend/app/crud/faq_item.py
+++ b/backend/app/crud/faq_item.py
@@ -26,7 +26,6 @@ async def create_item(db: AsyncSession, item_in: FaqCreate, *, created_by: uuid.
answer=item_in.answer or "",
status="PENDING",
version=1,
- is_active=True,
created_by=created_by,
)
db.add(item)
@@ -46,7 +45,6 @@ async def list_items(
study_id: uuid.UUID | None,
category_id: uuid.UUID | None = None,
keyword: str | None = None,
- is_active: bool | None = True,
study_scope: str | None = "project",
) -> Sequence[FaqItem]:
stmt = select(FaqItem).join(FaqCategory, FaqCategory.id == FaqItem.category_id)
@@ -69,8 +67,6 @@ async def list_items(
FaqItem.keywords.ilike(like_pattern),
)
)
- if is_active is not None:
- stmt = stmt.where(FaqItem.is_active == is_active)
result = await db.execute(stmt)
return result.scalars().all()
diff --git a/backend/app/models/faq_category.py b/backend/app/models/faq_category.py
index ae20a19e..9bdd9eea 100644
--- a/backend/app/models/faq_category.py
+++ b/backend/app/models/faq_category.py
@@ -4,7 +4,7 @@ from typing import Optional
import uuid
from datetime import datetime
-from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func
+from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -20,7 +20,7 @@ class FaqCategory(Base):
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
sort_order: Mapped[int] = mapped_column(nullable=False, default=0)
- is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
+ icon: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
diff --git a/backend/app/models/faq_item.py b/backend/app/models/faq_item.py
index eb948c3f..f7523752 100644
--- a/backend/app/models/faq_item.py
+++ b/backend/app/models/faq_item.py
@@ -26,7 +26,6 @@ class FaqItem(Base):
answer: Mapped[str] = mapped_column(Text, nullable=False)
keywords: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
- is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
diff --git a/backend/app/schemas/faq.py b/backend/app/schemas/faq.py
index 4d5624aa..06b5ffe4 100644
--- a/backend/app/schemas/faq.py
+++ b/backend/app/schemas/faq.py
@@ -10,7 +10,7 @@ class CategoryCreate(BaseModel):
name: str
description: Optional[str] = None
sort_order: int = 0
- is_active: bool = True
+ icon: Optional[str] = None
class CategoryUpdate(BaseModel):
@@ -18,7 +18,7 @@ class CategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
sort_order: Optional[int] = None
- is_active: Optional[bool] = None
+ icon: Optional[str] = None
class CategoryRead(BaseModel):
@@ -27,7 +27,7 @@ class CategoryRead(BaseModel):
name: str
description: Optional[str]
sort_order: int
- is_active: bool
+ icon: Optional[str]
created_at: datetime
updated_at: datetime
@@ -44,7 +44,6 @@ class FaqCreate(BaseModel):
class FaqUpdate(BaseModel):
question: Optional[str] = None
answer: Optional[str] = None
- is_active: Optional[bool] = None
class FaqStatusUpdate(BaseModel):
@@ -94,7 +93,6 @@ class FaqRead(BaseModel):
answer: str
keywords: Optional[str]
version: int
- is_active: bool
created_by: uuid.UUID
created_at: datetime
updated_at: datetime
diff --git a/frontend/index.html b/frontend/index.html
index 00fa07c8..b6573b8e 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -5,6 +5,7 @@
+
CTMS
diff --git a/frontend/src/api/faqs.test.ts b/frontend/src/api/faqs.test.ts
index cb130076..fe345620 100644
--- a/frontend/src/api/faqs.test.ts
+++ b/frontend/src/api/faqs.test.ts
@@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const apiPost = vi.fn();
const apiPatch = vi.fn();
const apiDelete = vi.fn();
+const apiGet = vi.fn();
vi.mock("./axios", () => ({
+ apiGet,
apiPost,
apiPatch,
apiDelete,
@@ -54,4 +56,34 @@ describe("faqs item api", () => {
params: { study_id: "study-1" },
});
});
+
+ it("passes study_id as a query parameter for item detail permission checks", async () => {
+ const { fetchFaqItem, fetchFaqReplies, createFaqReply, deleteFaqReply, setFaqBestReply, setFaqStatus } = await import("./faqs");
+
+ fetchFaqItem("item-1", "study-1");
+ fetchFaqReplies("item-1", "study-1");
+ createFaqReply("item-1", "study-1", { content: "建议复查。" });
+ deleteFaqReply("item-1", "reply-1", "study-1");
+ setFaqBestReply("item-1", "study-1", { best_reply_id: "reply-1" });
+ setFaqStatus("item-1", "study-1", { status: "RESOLVED" });
+
+ expect(apiGet).toHaveBeenCalledWith("/api/v1/faqs/items/item-1", {
+ params: { study_id: "study-1" },
+ });
+ expect(apiGet).toHaveBeenCalledWith("/api/v1/faqs/items/item-1/replies", {
+ params: { study_id: "study-1" },
+ });
+ expect(apiPost).toHaveBeenCalledWith("/api/v1/faqs/items/item-1/replies", { content: "建议复查。" }, {
+ params: { study_id: "study-1" },
+ });
+ expect(apiDelete).toHaveBeenCalledWith("/api/v1/faqs/items/item-1/replies/reply-1", {
+ params: { study_id: "study-1" },
+ });
+ expect(apiPatch).toHaveBeenCalledWith("/api/v1/faqs/items/item-1/best-reply", { best_reply_id: "reply-1" }, {
+ params: { study_id: "study-1" },
+ });
+ expect(apiPatch).toHaveBeenCalledWith("/api/v1/faqs/items/item-1/status", { status: "RESOLVED" }, {
+ params: { study_id: "study-1" },
+ });
+ });
});
diff --git a/frontend/src/api/faqs.ts b/frontend/src/api/faqs.ts
index 1200ec9f..2946b716 100644
--- a/frontend/src/api/faqs.ts
+++ b/frontend/src/api/faqs.ts
@@ -8,7 +8,7 @@ export interface FaqCategory {
name: string;
description?: string | null;
sort_order: number;
- is_active: boolean;
+ icon?: string | null;
created_at: string;
updated_at: string;
}
@@ -23,7 +23,6 @@ export interface FaqItem {
answer: string;
keywords?: string | null;
version: number;
- is_active: boolean;
created_by?: string;
created_at: string;
updated_at: string;
@@ -65,7 +64,8 @@ export const deleteFaqCategory = (categoryId: string, studyId?: string | null) =
export const fetchFaqItems = (params?: Record): Promise>> =>
apiGet("/api/v1/faqs/items/", { params });
-export const fetchFaqItem = (itemId: string) => apiGet(`/api/v1/faqs/items/${itemId}`);
+export const fetchFaqItem = (itemId: string, studyId?: string | null) =>
+ apiGet(`/api/v1/faqs/items/${itemId}`, studyQuery(studyId));
export const createFaqItem = (payload: Record) =>
apiPost("/api/v1/faqs/items/", payload, studyQuery(payload.study_id));
@@ -76,17 +76,17 @@ export const updateFaqItem = (itemId: string, payload: Record) =>
export const deleteFaqItem = (itemId: string, studyId?: string | null) =>
apiDelete(`/api/v1/faqs/items/${itemId}`, studyQuery(studyId));
-export const fetchFaqReplies = (itemId: string) =>
- apiGet(`/api/v1/faqs/items/${itemId}/replies`);
+export const fetchFaqReplies = (itemId: string, studyId?: string | null) =>
+ apiGet(`/api/v1/faqs/items/${itemId}/replies`, studyQuery(studyId));
-export const createFaqReply = (itemId: string, payload: { content: string; quote_reply_id?: string | null }) =>
- apiPost(`/api/v1/faqs/items/${itemId}/replies`, payload);
+export const createFaqReply = (itemId: string, studyId: string | null | undefined, payload: { content: string; quote_reply_id?: string | null }) =>
+ apiPost(`/api/v1/faqs/items/${itemId}/replies`, payload, studyQuery(studyId));
-export const deleteFaqReply = (itemId: string, replyId: string) =>
- apiDelete(`/api/v1/faqs/items/${itemId}/replies/${replyId}`);
+export const deleteFaqReply = (itemId: string, replyId: string, studyId?: string | null) =>
+ apiDelete(`/api/v1/faqs/items/${itemId}/replies/${replyId}`, studyQuery(studyId));
-export const setFaqBestReply = (itemId: string, payload: { best_reply_id: string | null }) =>
- apiPatch(`/api/v1/faqs/items/${itemId}/best-reply`, payload);
+export const setFaqBestReply = (itemId: string, studyId: string | null | undefined, payload: { best_reply_id: string | null }) =>
+ apiPatch(`/api/v1/faqs/items/${itemId}/best-reply`, payload, studyQuery(studyId));
-export const setFaqStatus = (itemId: string, payload: { status: "PENDING" | "PROCESSING" | "RESOLVED" }) =>
- apiPatch(`/api/v1/faqs/items/${itemId}/status`, payload);
+export const setFaqStatus = (itemId: string, studyId: string | null | undefined, payload: { status: "PENDING" | "PROCESSING" | "RESOLVED" }) =>
+ apiPatch(`/api/v1/faqs/items/${itemId}/status`, payload, studyQuery(studyId));
diff --git a/frontend/src/components/FaqCategoryForm.vue b/frontend/src/components/FaqCategoryForm.vue
index 4d4f18b8..dcb01735 100644
--- a/frontend/src/components/FaqCategoryForm.vue
+++ b/frontend/src/components/FaqCategoryForm.vue
@@ -30,13 +30,28 @@
-
-
+
+
+
+
-
@@ -62,7 +59,6 @@ const form = reactive({
study_id: "",
category_id: "",
question: "",
- is_active: true,
});
const rules: FormRules = {
@@ -76,7 +72,6 @@ const reset = () => {
form.study_id = study.currentStudy?.id || "";
form.category_id = "";
form.question = "";
- form.is_active = true;
};
watch(
@@ -86,7 +81,6 @@ watch(
form.study_id = val.study_id || "";
form.category_id = val.category_id;
form.question = val.question;
- form.is_active = val.is_active;
} else {
reset();
}
@@ -105,7 +99,6 @@ const onSubmit = async () => {
study_id: study.currentStudy?.id || null,
category_id: form.category_id,
question: form.question,
- is_active: form.is_active,
};
if (isEdit.value && props.item) {
await updateFaqItem(props.item.id, payload);
diff --git a/frontend/src/components/FaqList.vue b/frontend/src/components/FaqList.vue
index 88bd4db0..2add7151 100644
--- a/frontend/src/components/FaqList.vue
+++ b/frontend/src/components/FaqList.vue
@@ -1,59 +1,61 @@
-
+
+
-
- {{ scope.row.question }}
-
+
+ {{ questionInitial(scope.row.question) }}
+
+ {{ scope.row.question }}
+
+
- {{ categoryMap[scope.row.category_id] || scope.row.category_id }}
-
-
-
-
-
- {{ scope.row.is_active ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}
-
+ {{ categoryLabel(scope.row.category_id) }}
- {{ displayDateTime(scope.row.updated_at) }}
+
+
+ {{ splitDateTime(scope.row.updated_at).date }}
+ {{ splitDateTime(scope.row.updated_at).time }}
+
+
- {{ statusLabel(scope.row.status) }}
+ {{ statusLabel(scope.row.status) }}
-
+
-
- {{ TEXT.common.actions.delete }}
-
-
-
- {{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
+
+
+ 删除
-
+
-
+
+
diff --git a/frontend/src/views/FaqDetail.vue b/frontend/src/views/FaqDetail.vue
index 13d5d71f..5fbc1997 100644
--- a/frontend/src/views/FaqDetail.vue
+++ b/frontend/src/views/FaqDetail.vue
@@ -235,7 +235,7 @@ const loadReplies = async () => {
const id = route.params.itemId as string;
repliesLoading.value = true;
try {
- const { data } = await fetchFaqReplies(id);
+ const { data } = await fetchFaqReplies(id, item.value?.study_id || study.currentStudy?.id);
replies.value = Array.isArray(data) ? data : data.items || [];
await loadReplyAttachments();
} catch (e: any) {
@@ -249,7 +249,7 @@ const loadData = async () => {
const id = route.params.itemId as string;
loading.value = true;
try {
- const { data: faqData } = await fetchFaqItem(id);
+ const { data: faqData } = await fetchFaqItem(id, study.currentStudy?.id);
item.value = faqData;
const questionTitle = String(faqData.question || "").trim();
study.setViewContext({
@@ -288,7 +288,7 @@ const submitReply = async () => {
if (!item.value) return;
submitting.value = true;
try {
- const { data } = await createFaqReply(item.value.id, {
+ const { data } = await createFaqReply(item.value.id, item.value.study_id, {
content: replyContent.value,
quote_reply_id: quoteReply.value?.id || null,
});
@@ -324,7 +324,7 @@ const removeReply = async (reply: any) => {
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
- await deleteFaqReply(item.value.id, reply.id);
+ await deleteFaqReply(item.value.id, reply.id, item.value.study_id);
ElMessage.success(TEXT.common.messages.deleteSuccess);
loadReplies();
} catch (e: any) {
@@ -344,7 +344,7 @@ const toggleBest = async (reply: any) => {
const ok = await ElMessageBox.confirm(TEXT.modules.knowledgeMedicalConsult.confirmBestReply, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
}
- const { data } = await setFaqBestReply(item.value.id, { best_reply_id: target });
+ const { data } = await setFaqBestReply(item.value.id, item.value.study_id, { best_reply_id: target });
item.value = data;
ElMessage.success(target ? TEXT.modules.knowledgeMedicalConsult.bestReplySet : TEXT.modules.knowledgeMedicalConsult.bestReplyCleared);
} catch (e: any) {
@@ -359,7 +359,7 @@ const confirmResolved = async () => {
return;
}
try {
- const { data } = await setFaqStatus(item.value.id, { status: "RESOLVED" });
+ const { data } = await setFaqStatus(item.value.id, item.value.study_id, { status: "RESOLVED" });
item.value = data;
ElMessage.success(TEXT.modules.knowledgeMedicalConsult.resolvedSuccess);
} catch (e: any) {
@@ -413,6 +413,14 @@ onMounted(() => {
.page {
padding: 16px;
}
+
+:deep(.el-card.unified-shell) {
+ border-radius: 8px !important;
+}
+
+.page :deep(.unified-shell) {
+ border-radius: 8px;
+}
.content {
white-space: pre-wrap;
line-height: 1.6;
@@ -548,7 +556,7 @@ onMounted(() => {
.question-text {
margin: 6px 0 0;
padding: 12px 14px;
- border-radius: 10px;
+ border-radius: 8px;
background: #f5f7ff;
border: 1px solid #e0e7ff;
color: #1f2a44;
diff --git a/frontend/src/views/FaqPermissionConsistency.test.ts b/frontend/src/views/FaqPermissionConsistency.test.ts
index cc1384a4..f889aca8 100644
--- a/frontend/src/views/FaqPermissionConsistency.test.ts
+++ b/frontend/src/views/FaqPermissionConsistency.test.ts
@@ -37,6 +37,16 @@ describe("FAQ project permissions", () => {
expect(source).not.toContain("row.created_by === auth.user?.id");
});
+ it("opens FAQ detail from any row cell while preserving action button click isolation", () => {
+ const source = read("../components/FaqList.vue");
+
+ expect(source).toContain('@row-click="onRowClick"');
+ expect(source).toContain("const onRowClick = (row: FaqItem) =>");
+ expect(source).not.toContain('column?.property !== "question"');
+ expect(source).toContain('@click.stop="remove(scope.row)"');
+ expect(source).toContain('@click.stop="toggle(scope.row)"');
+ });
+
it("splits FAQ category create update and delete controls by backend operation permissions", () => {
const source = read("../components/FaqCategoryPanel.vue");