7861607fb8
Lets a project mute individual rules or whole topics from rulebooks it subscribes to, without unsubscribing the rulebook. Two new association tables (migration 0060), 4 MCP tools (suppress/unsuppress × rule/topic), 4 REST endpoints, and an inline "× skip" affordance plus collapsed "Suppressed (N)" section in the project's Rules tab. get_applicable_rules now emits suppressed_rules and suppressed_topics (detail objects with rulebook/topic context, not just IDs) so the UI can render the suppressed list without a follow-up lookup. The main rules projection grew topic_id and rulebook_id columns for the per-row suppress affordance. Project deletion cascades the suppression rows via hard DELETE — they are pure associations with no soft-delete column, and restoring a deleted project should start fresh, not inherit stale mutes. Project-scoped rules (Rule.project_id) are deliberately not suppressible — delete them with delete_rule instead. Implements plan-task #187. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
184 lines
6.5 KiB
TypeScript
184 lines
6.5 KiB
TypeScript
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
|
|
|
|
export interface Rulebook {
|
|
id: number;
|
|
owner_user_id: number;
|
|
title: string;
|
|
description: string;
|
|
always_on: boolean;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface RulebookTopic {
|
|
id: number;
|
|
rulebook_id: number;
|
|
title: string;
|
|
description: string;
|
|
order_index: number;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface Rule {
|
|
id: number;
|
|
topic_id: number | null;
|
|
project_id: number | null;
|
|
title: string;
|
|
statement: string;
|
|
why: string;
|
|
how_to_apply: string;
|
|
order_index: number;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface RuleHeader {
|
|
id: number;
|
|
title: string;
|
|
statement: string;
|
|
topic_id: number | null;
|
|
}
|
|
|
|
export interface ApplicableRules {
|
|
rules: {
|
|
id: number;
|
|
title: string;
|
|
statement: string;
|
|
topic_id: number;
|
|
topic_title: string;
|
|
rulebook_id: number;
|
|
rulebook_title: string;
|
|
}[];
|
|
project_rules: {
|
|
id: number;
|
|
title: string;
|
|
statement: string;
|
|
}[];
|
|
suppressed_rules: {
|
|
id: number;
|
|
title: string;
|
|
topic_id: number;
|
|
topic_title: string;
|
|
rulebook_id: number;
|
|
rulebook_title: string;
|
|
}[];
|
|
suppressed_topics: {
|
|
id: number;
|
|
title: string;
|
|
rulebook_id: number;
|
|
rulebook_title: string;
|
|
}[];
|
|
truncated: boolean;
|
|
subscribed_rulebooks: { id: number; title: string }[];
|
|
}
|
|
|
|
// ── Rulebooks ───────────────────────────────────────────────────────
|
|
|
|
export async function listRulebooks(): Promise<Rulebook[]> {
|
|
const data = await apiGet<{ rulebooks: Rulebook[] }>("/api/rulebooks");
|
|
return data.rulebooks;
|
|
}
|
|
|
|
export async function getRulebook(id: number): Promise<Rulebook & { topics: RulebookTopic[] }> {
|
|
return apiGet(`/api/rulebooks/${id}`);
|
|
}
|
|
|
|
export async function createRulebook(data: { title: string; description?: string }): Promise<Rulebook> {
|
|
return apiPost("/api/rulebooks", data);
|
|
}
|
|
|
|
export async function updateRulebook(id: number, data: Partial<{ title: string; description: string; always_on: boolean }>): Promise<Rulebook> {
|
|
return apiPatch(`/api/rulebooks/${id}`, data);
|
|
}
|
|
|
|
export async function deleteRulebook(id: number): Promise<void> {
|
|
return apiDelete(`/api/rulebooks/${id}`);
|
|
}
|
|
|
|
// ── Topics ─────────────────────────────────────────────────────────
|
|
|
|
export async function listTopics(rulebookId: number): Promise<RulebookTopic[]> {
|
|
const data = await apiGet<{ topics: RulebookTopic[] }>(`/api/rulebooks/${rulebookId}/topics`);
|
|
return data.topics;
|
|
}
|
|
|
|
export async function createTopic(rulebookId: number, data: { title: string; description?: string; order_index?: number }): Promise<RulebookTopic> {
|
|
return apiPost(`/api/rulebooks/${rulebookId}/topics`, data);
|
|
}
|
|
|
|
export async function updateTopic(id: number, data: Partial<{ title: string; description: string; order_index: number }>): Promise<RulebookTopic> {
|
|
return apiPatch(`/api/rulebook-topics/${id}`, data);
|
|
}
|
|
|
|
export async function deleteTopic(id: number): Promise<void> {
|
|
return apiDelete(`/api/rulebook-topics/${id}`);
|
|
}
|
|
|
|
// ── Rules ──────────────────────────────────────────────────────────
|
|
|
|
export async function listRules(filters: { rulebook_id?: number; topic_id?: number; project_id?: number } = {}): Promise<Rule[]> {
|
|
const params = new URLSearchParams();
|
|
if (filters.rulebook_id) params.set("rulebook_id", String(filters.rulebook_id));
|
|
if (filters.topic_id) params.set("topic_id", String(filters.topic_id));
|
|
if (filters.project_id) params.set("project_id", String(filters.project_id));
|
|
const qs = params.toString();
|
|
const data = await apiGet<{ rules: Rule[] }>(`/api/rules${qs ? `?${qs}` : ""}`);
|
|
return data.rules;
|
|
}
|
|
|
|
export async function getRule(id: number): Promise<Rule> {
|
|
return apiGet(`/api/rules/${id}`);
|
|
}
|
|
|
|
export async function createRule(topicId: number, data: { title: string; statement: string; why?: string; how_to_apply?: string; order_index?: number }): Promise<Rule> {
|
|
return apiPost(`/api/rulebook-topics/${topicId}/rules`, data);
|
|
}
|
|
|
|
export async function updateRule(id: number, data: Partial<{ title: string; statement: string; why: string; how_to_apply: string; order_index: number }>): Promise<Rule> {
|
|
return apiPatch(`/api/rules/${id}`, data);
|
|
}
|
|
|
|
export async function deleteRule(id: number): Promise<void> {
|
|
return apiDelete(`/api/rules/${id}`);
|
|
}
|
|
|
|
// ── Subscriptions ──────────────────────────────────────────────────
|
|
|
|
export async function subscribeProject(projectId: number, rulebookId: number): Promise<void> {
|
|
await apiPost(`/api/projects/${projectId}/rulebook-subscriptions`, { rulebook_id: rulebookId });
|
|
}
|
|
|
|
export async function unsubscribeProject(projectId: number, rulebookId: number): Promise<void> {
|
|
return apiDelete(`/api/projects/${projectId}/rulebook-subscriptions/${rulebookId}`);
|
|
}
|
|
|
|
export async function getProjectApplicableRules(projectId: number): Promise<ApplicableRules> {
|
|
return apiGet(`/api/projects/${projectId}/rules`);
|
|
}
|
|
|
|
export async function createProjectRule(
|
|
projectId: number,
|
|
data: { statement: string; title?: string; why?: string; how_to_apply?: string },
|
|
): Promise<Rule> {
|
|
return apiPost(`/api/projects/${projectId}/rules`, data);
|
|
}
|
|
|
|
// ── Suppressions ───────────────────────────────────────────────────
|
|
|
|
export async function suppressRuleForProject(projectId: number, ruleId: number): Promise<void> {
|
|
await apiPost(`/api/projects/${projectId}/suppressions/rules/${ruleId}`, {});
|
|
}
|
|
|
|
export async function unsuppressRuleForProject(projectId: number, ruleId: number): Promise<void> {
|
|
return apiDelete(`/api/projects/${projectId}/suppressions/rules/${ruleId}`);
|
|
}
|
|
|
|
export async function suppressTopicForProject(projectId: number, topicId: number): Promise<void> {
|
|
await apiPost(`/api/projects/${projectId}/suppressions/topics/${topicId}`, {});
|
|
}
|
|
|
|
export async function unsuppressTopicForProject(projectId: number, topicId: number): Promise<void> {
|
|
return apiDelete(`/api/projects/${projectId}/suppressions/topics/${topicId}`);
|
|
}
|