diff --git a/.github/workflows/backend-pr-ci.yml b/.github/workflows/backend-pr-ci.yml
new file mode 100644
index 0000000..9e5bf95
--- /dev/null
+++ b/.github/workflows/backend-pr-ci.yml
@@ -0,0 +1,101 @@
+name: backend-pr-ci
+
+on:
+ pull_request:
+ paths:
+ - "backend/**"
+ - ".github/workflows/backend-pr-ci.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: backend-pr-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ GO_VERSION: "1.24"
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ defaults:
+ run:
+ working-directory: backend
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache-dependency-path: backend/go.sum
+
+ - name: Install golangci-lint
+ run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
+
+ - name: Run golangci-lint
+ run: golangci-lint run --timeout=5m
+
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ defaults:
+ run:
+ working-directory: backend
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache-dependency-path: backend/go.sum
+
+ - name: Build WASM
+ run: GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o bdd.wasm .
+
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ defaults:
+ run:
+ working-directory: backend
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache-dependency-path: backend/go.sum
+
+ - name: Run tests (race detector)
+ run: go test -race -timeout 60s -coverprofile=coverage.out $(go list ./...)
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: backend/coverage.out
+ retention-days: 7
diff --git a/.github/workflows/frontend-pr-ci.yml b/.github/workflows/frontend-pr-ci.yml
new file mode 100644
index 0000000..8798bad
--- /dev/null
+++ b/.github/workflows/frontend-pr-ci.yml
@@ -0,0 +1,53 @@
+name: frontend-pr-ci
+
+on:
+ pull_request:
+ paths:
+ - "frontend/**"
+ - ".github/workflows/frontend-pr-ci.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: frontend-pr-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ web-quality:
+ name: Typecheck and build frontend
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ defaults:
+ run:
+ working-directory: frontend
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.2.23"
+
+ - name: Cache Bun packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-bun-
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Typecheck
+ run: bun run typecheck
+
+ - name: Build production bundle
+ run: bun run build
diff --git a/.github/workflows/mcp-pr-ci.yml b/.github/workflows/mcp-pr-ci.yml
new file mode 100644
index 0000000..1429198
--- /dev/null
+++ b/.github/workflows/mcp-pr-ci.yml
@@ -0,0 +1,53 @@
+name: mcp-pr-ci
+
+on:
+ pull_request:
+ paths:
+ - "mcp/**"
+ - ".github/workflows/mcp-pr-ci.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: mcp-pr-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ mcp-quality:
+ name: Typecheck and build MCP server
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ defaults:
+ run:
+ working-directory: mcp
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.2.23"
+
+ - name: Cache Bun packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-mcp-${{ hashFiles('mcp/bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-bun-mcp-
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Typecheck
+ run: bun run typecheck
+
+ - name: Build
+ run: bun run build
diff --git a/backend/go.mod b/backend/go.mod
index 6d09bde..805bd6b 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -2,6 +2,6 @@ module github.com/Paca-AI/first-party/bdd
go 1.24
-require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3
+require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5
require github.com/google/uuid v1.6.0
diff --git a/backend/go.sum b/backend/go.sum
index 664e182..344bb56 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -1,4 +1,4 @@
-github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3 h1:2MFHmdVIKVo1/Cyo7GL3S/LsAFxpsqhJSAbTao+xNEw=
-github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw=
+github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5 h1:xrEPnJLM2sdwtY/wRaGOdfeDPv5zPUpR8QZJ/w2nQVU=
+github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/backend/scenarios.go b/backend/scenarios.go
index ef5f724..a62f7ce 100644
--- a/backend/scenarios.go
+++ b/backend/scenarios.go
@@ -110,6 +110,8 @@ func (p *bddPlugin) createScenario(req *plugin.Request, res *plugin.Response) {
CreatedAt: now,
UpdatedAt: now,
}
+ plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.created",
+ map[string]any{"title": b.Title})
created(res, scenario)
}
@@ -225,6 +227,22 @@ func (p *bddPlugin) updateScenario(req *plugin.Request, res *plugin.Response) {
res.Error(404, "bdd scenario not found")
return
}
+ // Collect which fields changed for the activity record.
+ var changedFields []string
+ if b.Title != nil && *b.Title != sc.str("title") {
+ changedFields = append(changedFields, "title")
+ }
+ if b.Given != nil && *b.Given != sc.str("given_text") {
+ changedFields = append(changedFields, "given")
+ }
+ if b.When != nil && *b.When != sc.str("when_text") {
+ changedFields = append(changedFields, "when")
+ }
+ if b.Then != nil && *b.Then != sc.str("then_text") {
+ changedFields = append(changedFields, "then")
+ }
+ plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.updated",
+ map[string]any{"title": updTitle, "changes": changedFields})
ok(res, bddScenario{
ID: scenarioID,
TaskID: taskID,
@@ -247,6 +265,23 @@ func (p *bddPlugin) deleteScenario(req *plugin.Request, res *plugin.Response) {
return
}
+ // Fetch title before deletion for the activity record.
+ titleResult, err := p.db.Query(
+ `SELECT title FROM bdd_scenarios WHERE id = $1 AND task_id = $2`,
+ scenarioID, taskID,
+ )
+ if err != nil {
+ p.log.Error("deleteScenario title fetch: " + err.Error())
+ res.Error(500, "failed to delete bdd scenario")
+ return
+ }
+ if len(titleResult.Rows) == 0 {
+ res.Error(404, "bdd scenario not found")
+ return
+ }
+ scenarioTitleSC := newRowScanner(titleResult.Columns, titleResult.Rows[0])
+ scenarioTitle := scenarioTitleSC.str("title")
+
affected, err := p.db.Exec(
`DELETE FROM bdd_scenarios WHERE id = $1 AND task_id = $2`,
scenarioID, taskID,
@@ -260,6 +295,8 @@ func (p *bddPlugin) deleteScenario(req *plugin.Request, res *plugin.Response) {
res.Error(404, "bdd scenario not found")
return
}
+ plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.deleted",
+ map[string]any{"title": scenarioTitle})
res.NoContent()
}
diff --git a/frontend/src/BDDScenarioCard.tsx b/frontend/src/BDDScenarioCard.tsx
deleted file mode 100644
index e095e1e..0000000
--- a/frontend/src/BDDScenarioCard.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { ChevronDown, ChevronUp, Pencil, Trash2, X, Check } from "lucide-react";
-import { useState } from "react";
-import type { BDDScenario } from "./types";
-
-function cn(...classes: (string | boolean | undefined | null)[]): string {
- return classes.filter(Boolean).join(" ");
-}
-
-interface BDDScenarioCardProps {
- scenario: BDDScenario;
- canEdit: boolean;
- onUpdate: (
- id: string,
- patch: { title?: string; given?: string; when?: string; then?: string },
- ) => void;
- onDelete: (id: string) => void;
-}
-
-export function BDDScenarioCard({
- scenario,
- canEdit,
- onUpdate,
- onDelete,
-}: BDDScenarioCardProps) {
- const [expanded, setExpanded] = useState(false);
- const [editing, setEditing] = useState(false);
- const [draft, setDraft] = useState({
- title: scenario.title,
- given: scenario.given,
- when: scenario.when,
- then: scenario.then,
- });
-
- const startEdit = () => {
- setDraft({
- title: scenario.title,
- given: scenario.given,
- when: scenario.when,
- then: scenario.then,
- });
- setEditing(true);
- setExpanded(true);
- };
-
- const cancelEdit = () => {
- setEditing(false);
- setDraft({
- title: scenario.title,
- given: scenario.given,
- when: scenario.when,
- then: scenario.then,
- });
- };
-
- const submitEdit = () => {
- if (!draft.title.trim()) return;
- onUpdate(scenario.id, {
- title: draft.title.trim(),
- given: draft.given,
- when: draft.when,
- then: draft.then,
- });
- setEditing(false);
- };
-
- const hasContent = scenario.given || scenario.when || scenario.then;
-
- return (
-
- {/* Header */}
-
-
-
- {canEdit && (
-
- {editing ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
-
- )}
-
-
- {/* Body */}
- {(expanded || editing) && (
-
- {(["given", "when", "then"] as const).map((field) => (
-
-
- {field}
-
- {editing ? (
-
- ))}
- {editing && (
-
-
-
-
- )}
-
- )}
-
- );
-}
diff --git a/frontend/src/BDDScenariosSection.tsx b/frontend/src/BDDScenariosSection.tsx
index 477acc7..dff6917 100644
--- a/frontend/src/BDDScenariosSection.tsx
+++ b/frontend/src/BDDScenariosSection.tsx
@@ -1,142 +1,429 @@
-import { PluginApiClient, PluginQueryClientProvider } from "@paca-ai/plugin-sdk-react";
+import { PluginQueryClientProvider } from "@paca-ai/plugin-sdk-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { FlaskConical, Plus } from "lucide-react";
-import { useMemo } from "react";
-import { BDDScenarioCard } from "./BDDScenarioCard";
+import {
+ ChevronDown,
+ ChevronRight,
+ FlaskConical,
+ Plus,
+ Trash2,
+} from "lucide-react";
+import { type CSSProperties, useEffect, useRef, useState } from "react";
+import {
+ bddScenariosQueryOptions,
+ createBDDScenario,
+ deleteBDDScenario,
+ updateBDDScenario,
+} from "./lib/interaction-api";
import type { BDDScenario } from "./types";
+import { cn } from "./lib/utils";
-// ── Constants ─────────────────────────────────────────────────────────────────
+// ── Types ──────────────────────────────────────────────────────────────────────
-const PLUGIN_ID = "com.paca.bdd";
+interface BDDSectionProps {
+ projectId: string;
+ taskId: string;
+ canEdit?: boolean;
+}
+
+// ── Main export with provider ──────────────────────────────────────────────────
+
+export default function BDDScenariosSection(props: BDDSectionProps) {
+ return (
+
+
+
+ );
+}
+
+// ── Editor row for a single clause (Given / When / Then) ──────────────────────
+
+function ClauseRow({
+ label,
+ color,
+ value,
+ placeholder,
+ onChange,
+ onBlur,
+ disabled,
+}: {
+ label: string;
+ color: string;
+ value: string;
+ placeholder: string;
+ onChange: (v: string) => void;
+ onBlur?: () => void;
+ disabled?: boolean;
+}) {
+ return (
+
+
+ {label}
+
+
+ );
+}
+
+// ── Single scenario card ───────────────────────────────────────────────────────
+
+function ScenarioCard({
+ scenario,
+ projectId,
+ taskId,
+ canEdit,
+}: {
+ scenario: BDDScenario;
+ projectId: string;
+ taskId: string;
+ canEdit: boolean;
+}) {
+ const qc = useQueryClient();
+ const [expanded, setExpanded] = useState(false);
+ const [editTitle, setEditTitle] = useState(false);
+ const [titleDraft, setTitleDraft] = useState(scenario.title);
+
+ // Debounced clause drafts (updated locally then flushed on blur)
+ const [given, setGiven] = useState(scenario.given);
+ const [when, setWhen] = useState(scenario.when);
+ const [then, setThen] = useState(scenario.then);
+
+ useEffect(() => {
+ setTitleDraft(scenario.title);
+ setGiven(scenario.given);
+ setWhen(scenario.when);
+ setThen(scenario.then);
+ }, [scenario.id, scenario.title, scenario.given, scenario.when, scenario.then]);
+
+ const qKey = bddScenariosQueryOptions(projectId, taskId).queryKey;
+
+ const updateMut = useMutation({
+ mutationFn: (payload: Parameters[3]) =>
+ updateBDDScenario(projectId, taskId, scenario.id, payload),
+ onSuccess: () => qc.invalidateQueries({ queryKey: qKey }),
+ });
+
+ const deleteMut = useMutation({
+ mutationFn: () => deleteBDDScenario(projectId, taskId, scenario.id),
+ onSuccess: () => qc.invalidateQueries({ queryKey: qKey }),
+ });
+
+ const flushClause = (field: "given" | "when" | "then", val: string) => {
+ const original = scenario[field];
+ const normalized = val.trim();
+ if (normalized !== original.trim()) {
+ updateMut.mutate({ [field]: normalized });
+ }
+ };
-// ── Props ─────────────────────────────────────────────────────────────────────
+ return (
+
+ {/* Header row */}
+
+ {/* Expand/collapse button */}
+
-interface BDDScenariosSectionProps {
- projectId: string;
- taskId: string;
- canEdit?: boolean;
+ {/* Title area */}
+ {editTitle && canEdit ? (
+ {
+ el?.focus();
+ }}
+ aria-label="Scenario title"
+ value={titleDraft}
+ onChange={(e) => setTitleDraft(e.target.value)}
+ onBlur={() => {
+ setEditTitle(false);
+ const trimmed = titleDraft.trim();
+ if (trimmed && trimmed !== scenario.title) {
+ updateMut.mutate({ title: trimmed });
+ } else {
+ setTitleDraft(scenario.title);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === "Escape") {
+ e.currentTarget.blur();
+ }
+ }}
+ className="flex-1 bg-transparent text-[13px] font-semibold text-foreground outline-none"
+ />
+ ) : (
+
+ )}
+
+ {canEdit && (
+
+ )}
+
+
+ {/* Expanded body: Given / When / Then */}
+ {expanded && (
+
+
+ flushClause("given", given)}
+ disabled={!canEdit}
+ />
+ flushClause("when", when)}
+ disabled={!canEdit}
+ />
+ flushClause("then", then)}
+ disabled={!canEdit}
+ />
+
+
+ )}
+
+ );
}
-// ── Component ─────────────────────────────────────────────────────────────────
-
-/**
- * BDDScenariosSection — the entry component exposed by the BDD plugin.
- *
- * Receives props directly from the host's spread and builds
- * its own PluginApiClient using window.location.origin so it can run as an
- * independent micro-frontend.
- */
-export default function BDDScenariosSection(props: BDDScenariosSectionProps) {
- return (
-
-
-
- );
+// ── New scenario quick-add form ────────────────────────────────────────────────
+
+function NewScenarioForm({
+ projectId,
+ taskId,
+ onDone,
+}: {
+ projectId: string;
+ taskId: string;
+ onDone: () => void;
+}) {
+ const qc = useQueryClient();
+ const titleRef = useRef(null);
+ useEffect(() => {
+ titleRef.current?.focus();
+ }, []);
+ const [title, setTitle] = useState("");
+ const [given, setGiven] = useState("");
+ const [when, setWhen] = useState("");
+ const [then, setThen] = useState("");
+
+ const qKey = bddScenariosQueryOptions(projectId, taskId).queryKey;
+
+ const createMut = useMutation({
+ mutationFn: () =>
+ createBDDScenario(projectId, taskId, {
+ title: title.trim(),
+ given: given.trim(),
+ when: when.trim(),
+ // biome-ignore lint/suspicious/noThenProperty: "then" is a BDD domain field
+ then: then.trim(),
+ }),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: qKey });
+ onDone();
+ },
+ });
+
+ const canSave = title.trim().length > 0 && !createMut.isPending;
+
+ return (
+
+
setTitle(e.target.value)}
+ placeholder="Scenario title…"
+ className="w-full bg-transparent text-[13px] font-semibold text-foreground outline-none placeholder:text-muted-foreground/50 focus:placeholder:text-muted-foreground/70 transition-colors"
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && canSave) createMut.mutate();
+ if (e.key === "Escape") onDone();
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
+// ── Section header ─────────────────────────────────────────────────────────────
+
function BDDScenariosSectionInner({
- projectId,
- taskId,
- canEdit = false,
-}: BDDScenariosSectionProps) {
- const api = useMemo(
- () =>
- new PluginApiClient({
- baseUrl: `${window.location.origin}/api/v1`,
- projectId,
- fetch: (url, init) =>
- window.fetch(url, { ...init, credentials: "include" }),
- }),
- [projectId],
- );
-
- const qc = useQueryClient();
- const queryKey = ["plugin", PLUGIN_ID, "bdd-scenarios", projectId, taskId];
-
- // ── Query ──────────────────────────────────────────────────────────────────
-
- const { data: scenarios = [], isLoading } = useQuery({
- queryKey,
- queryFn: () =>
- api.pluginGet(
- PLUGIN_ID,
- `/tasks/${taskId}/bdd-scenarios`,
- ),
- });
-
- // ── Mutations ──────────────────────────────────────────────────────────────
-
- const invalidate = () => qc.invalidateQueries({ queryKey });
-
- const createScenario = useMutation({
- mutationFn: () =>
- api.pluginPost(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios`, {
- title: `Scenario ${scenarios.length + 1}`,
- }),
- onSuccess: invalidate,
- });
-
- const updateScenario = useMutation({
- mutationFn: ({
- id,
- patch,
- }: {
- id: string;
- patch: { title?: string; given?: string; when?: string; then?: string };
- }) =>
- api.pluginPatch(
- PLUGIN_ID,
- `/tasks/${taskId}/bdd-scenarios/${id}`,
- patch,
- ),
- onSuccess: invalidate,
- });
-
- const deleteScenario = useMutation({
- mutationFn: (id: string) =>
- api.pluginDelete(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios/${id}`),
- onSuccess: invalidate,
- });
-
- // ── Render ─────────────────────────────────────────────────────────────────
-
- return (
-
-
-
- BDD Scenarios
-
-
- {canEdit && (
-
- )}
-
-
- {isLoading ? null : scenarios.length > 0 ? (
-
- {scenarios.map((scenario) => (
- updateScenario.mutate({ id, patch })}
- onDelete={(id) => deleteScenario.mutate(id)}
- />
- ))}
-
- ) : (
-
-
-
No BDD scenarios yet
-
- )}
-
- );
+ projectId,
+ taskId,
+ canEdit = false,
+}: BDDSectionProps) {
+ const [adding, setAdding] = useState(false);
+
+ const { data: scenarios = [], isLoading } = useQuery(
+ bddScenariosQueryOptions(projectId, taskId),
+ );
+
+ return (
+
+ {/* Section heading */}
+
+
+ BDD Scenarios
+ {scenarios.length > 0 && (
+
+ {scenarios.length}
+
+ )}
+
+
+
+ {canEdit && !adding && (
+
+ )}
+
+
+ {/* List */}
+ {isLoading ? (
+
+ ) : scenarios.length > 0 ? (
+
+ {scenarios.map((s) => (
+
+ ))}
+
+ ) : (
+ !adding && (
+
+
+
No BDD scenarios yet
+
+ )
+ )}
+
+ {/* Inline creation form */}
+ {adding && (
+
setAdding(false)}
+ />
+ )}
+
+ );
}
diff --git a/frontend/src/lib/interaction-api.ts b/frontend/src/lib/interaction-api.ts
new file mode 100644
index 0000000..a0dd080
--- /dev/null
+++ b/frontend/src/lib/interaction-api.ts
@@ -0,0 +1,86 @@
+import { PluginApiClient } from "@paca-ai/plugin-sdk-react";
+import { useMemo } from "react";
+import type { BDDScenario } from "../types";
+
+const PLUGIN_ID = "com.paca.bdd";
+
+export function useBddApi(projectId: string) {
+ return useMemo(
+ () =>
+ new PluginApiClient({
+ baseUrl: `${window.location.origin}/api/v1`,
+ projectId,
+ fetch: (url, init) =>
+ window.fetch(url, { ...init, credentials: "include" }),
+ }),
+ [projectId],
+ );
+}
+
+export function bddScenariosQueryOptions(projectId: string, taskId: string) {
+ return {
+ queryKey: ["plugin", PLUGIN_ID, "bdd-scenarios", projectId, taskId] as const,
+ queryFn: async (): Promise => {
+ const api = new PluginApiClient({
+ baseUrl: `${window.location.origin}/api/v1`,
+ projectId,
+ fetch: (url, init) =>
+ window.fetch(url, { ...init, credentials: "include" }),
+ });
+ return api.pluginGet(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios`);
+ },
+ };
+}
+
+export async function createBDDScenario(
+ projectId: string,
+ taskId: string,
+ data: {
+ title: string;
+ given: string;
+ when: string;
+ then: string;
+ },
+) {
+ const api = new PluginApiClient({
+ baseUrl: `${window.location.origin}/api/v1`,
+ projectId,
+ fetch: (url, init) =>
+ window.fetch(url, { ...init, credentials: "include" }),
+ });
+ return api.pluginPost(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios`, data);
+}
+
+export async function updateBDDScenario(
+ projectId: string,
+ taskId: string,
+ scenarioId: string,
+ data: {
+ title?: string;
+ given?: string;
+ when?: string;
+ then?: string;
+ },
+) {
+ const api = new PluginApiClient({
+ baseUrl: `${window.location.origin}/api/v1`,
+ projectId,
+ fetch: (url, init) =>
+ window.fetch(url, { ...init, credentials: "include" }),
+ });
+ return api.pluginPatch(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios/${scenarioId}`, data);
+}
+
+export async function deleteBDDScenario(
+ projectId: string,
+ taskId: string,
+ scenarioId: string,
+) {
+ const api = new PluginApiClient({
+ baseUrl: `${window.location.origin}/api/v1`,
+ projectId,
+ fetch: (url, init) =>
+ window.fetch(url, { ...init, credentials: "include" }),
+ });
+ return api.pluginDelete(PLUGIN_ID, `/tasks/${taskId}/bdd-scenarios/${scenarioId}`);
+}
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..a391478
--- /dev/null
+++ b/frontend/src/lib/utils.ts
@@ -0,0 +1,3 @@
+export function cn(...classes: (string | false | undefined | null)[]): string {
+ return classes.filter(Boolean).join(" ");
+}
diff --git a/plugin.json b/plugin.json
index e015cff..cc4c382 100644
--- a/plugin.json
+++ b/plugin.json
@@ -2,7 +2,7 @@
"id": "com.paca.bdd",
"displayName": "BDD Scenarios",
"description": "Adds Given/When/Then BDD acceptance criteria to tasks.",
- "version": "0.1.0",
+ "version": "0.2.0",
"permissions": ["db.read", "db.write", "events.subscribe"],
"backend": {
"eventSubscriptions": ["task.deleted"],