@shepai/cli 1.52.2 → 1.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/json-schema/CiFixRecord.yaml +24 -0
- package/apis/json-schema/Feature.yaml +3 -0
- package/apis/json-schema/PullRequest.yaml +10 -0
- package/apis/json-schema/SdlcLifecycle.yaml +1 -0
- package/apis/json-schema/WorkflowConfig.yaml +15 -0
- package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts +8 -0
- package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts.map +1 -1
- package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts +12 -0
- package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts +30 -0
- package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts.map +1 -0
- package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.js +74 -0
- package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.js +60 -16
- package/dist/packages/core/src/application/use-cases/features/create/types.d.ts +2 -0
- package/dist/packages/core/src/application/use-cases/features/create/types.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.js +11 -4
- package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts +28 -0
- package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts.map +1 -0
- package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.js +53 -0
- package/dist/packages/core/src/domain/generated/output.d.ts +47 -1
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/packages/core/src/domain/generated/output.js +1 -0
- package/dist/packages/core/src/domain/lifecycle-gates.d.ts +16 -0
- package/dist/packages/core/src/domain/lifecycle-gates.d.ts.map +1 -0
- package/dist/packages/core/src/domain/lifecycle-gates.js +19 -0
- package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.js +12 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts +3 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.js +8 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +28 -1
- package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts +1 -0
- package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.js +12 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +52 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +7 -2
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts +27 -5
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.js +19 -16
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts +24 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.js +47 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts +43 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.js +118 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge-output-parser.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.d.ts → merge/merge.node.d.ts} +10 -4
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge.node.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.js → merge/merge.node.js} +44 -17
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts +12 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.js +38 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts +4 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.js +13 -0
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts +2 -0
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.js +29 -1
- package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/ls.command.js +40 -3
- package/dist/src/presentation/cli/commands/feat/new.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/new.command.js +18 -0
- package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/show.command.js +20 -0
- package/dist/src/presentation/web/app/actions/create-feature.d.ts +1 -0
- package/dist/src/presentation/web/app/actions/create-feature.d.ts.map +1 -1
- package/dist/src/presentation/web/app/actions/create-feature.js +2 -1
- package/dist/src/presentation/web/app/page.js +34 -33
- package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.d.ts +6 -0
- package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.js +23 -0
- package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts +1 -0
- package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/add-repository-node/index.js +1 -0
- package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts +11 -1
- package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.js +52 -5
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +4 -0
- package/dist/src/presentation/web/components/common/feature-node/feature-node.js +1 -1
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +13 -4
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +2 -0
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +26 -8
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts +7 -0
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts.map +1 -0
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.js +20 -0
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts +6 -0
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.js +58 -0
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +7 -3
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
- package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/app/actions/create-feature.ts +4 -1
- package/web/.next/standalone/src/presentation/web/app/page.tsx +36 -36
- package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-button.tsx +43 -0
- package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/index.ts +1 -0
- package/web/.next/standalone/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx +199 -2
- package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +5 -0
- package/web/.next/standalone/src/presentation/web/components/common/feature-node/feature-node.tsx +1 -1
- package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +20 -1
- package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +43 -12
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.stories.tsx +81 -0
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.tsx +29 -0
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +16 -2
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/static/chunks/212e3c682d3b7f50.js +10 -0
- package/web/.next/static/chunks/476fe030d242c153.css +2 -0
- package/web/.next/static/chunks/{31e5030eed01a941.js → 68fb91b6eca14536.js} +1 -1
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge-output-parser.d.ts.map +0 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.d.ts.map +0 -1
- package/web/.next/static/chunks/23cae32e6f42833d.js +0 -10
- package/web/.next/static/chunks/59b8bd47023e0249.css +0 -2
- /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.d.ts → merge/merge-output-parser.d.ts} +0 -0
- /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.js → merge/merge-output-parser.js} +0 -0
- /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_buildManifest.js +0 -0
- /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Plus, Loader2 } from 'lucide-react';
|
|
5
|
+
import { pickFolder } from './pick-folder';
|
|
6
|
+
|
|
7
|
+
interface AddRepositoryButtonProps {
|
|
8
|
+
onSelect?: (path: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AddRepositoryButton({ onSelect }: AddRepositoryButtonProps) {
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
|
|
14
|
+
async function handleClick() {
|
|
15
|
+
if (loading) return;
|
|
16
|
+
setLoading(true);
|
|
17
|
+
try {
|
|
18
|
+
const path = await pickFolder();
|
|
19
|
+
if (path) {
|
|
20
|
+
onSelect?.(path);
|
|
21
|
+
}
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
data-testid="add-repository-button"
|
|
31
|
+
onClick={handleClick}
|
|
32
|
+
disabled={loading}
|
|
33
|
+
className="border-muted-foreground/30 flex cursor-pointer items-center gap-2 rounded-full border-2 border-dashed px-3 py-1.5 text-xs transition-colors hover:border-blue-500 hover:text-blue-500 disabled:cursor-wait disabled:opacity-60"
|
|
34
|
+
>
|
|
35
|
+
{loading ? (
|
|
36
|
+
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
|
|
37
|
+
) : (
|
|
38
|
+
<Plus className="h-3.5 w-3.5 shrink-0" />
|
|
39
|
+
)}
|
|
40
|
+
<span className="font-medium">{loading ? 'Opening...' : 'Add Repository'}</span>
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import type { LucideIcon } from 'lucide-react';
|
|
5
5
|
import {
|
|
6
6
|
XIcon,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ImageIcon,
|
|
11
11
|
CodeIcon,
|
|
12
12
|
Trash2Icon,
|
|
13
|
+
ChevronsUpDown,
|
|
14
|
+
CheckIcon,
|
|
13
15
|
} from 'lucide-react';
|
|
14
16
|
import { cn } from '@/lib/utils';
|
|
15
17
|
import {
|
|
@@ -28,12 +30,19 @@ import { CheckboxGroup } from '@/components/ui/checkbox-group';
|
|
|
28
30
|
import { CheckboxGroupItem } from '@/components/ui/checkbox-group-item';
|
|
29
31
|
import { Separator } from '@/components/ui/separator';
|
|
30
32
|
import { Badge } from '@/components/ui/badge';
|
|
33
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
31
34
|
import type { FileAttachment } from '@shepai/core/infrastructure/services/file-dialog.service';
|
|
32
35
|
import type { WorkflowDefaults } from '@/app/actions/get-workflow-defaults';
|
|
33
36
|
import { pickFiles } from './pick-files';
|
|
34
37
|
|
|
35
38
|
export type { FileAttachment } from '@shepai/core/infrastructure/services/file-dialog.service';
|
|
36
39
|
|
|
40
|
+
/** Minimal feature descriptor for the parent selector. */
|
|
41
|
+
export interface ParentFeatureOption {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
export interface FeatureCreatePayload {
|
|
38
47
|
name: string;
|
|
39
48
|
description?: string;
|
|
@@ -46,6 +55,7 @@ export interface FeatureCreatePayload {
|
|
|
46
55
|
};
|
|
47
56
|
push: boolean;
|
|
48
57
|
openPr: boolean;
|
|
58
|
+
parentId?: string;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
const AUTO_APPROVE_OPTIONS = [
|
|
@@ -67,6 +77,10 @@ export interface FeatureCreateDrawerProps {
|
|
|
67
77
|
repositoryPath: string;
|
|
68
78
|
isSubmitting?: boolean;
|
|
69
79
|
workflowDefaults?: WorkflowDefaults;
|
|
80
|
+
/** List of existing features available for selection as a parent. */
|
|
81
|
+
features?: ParentFeatureOption[];
|
|
82
|
+
/** Pre-select a parent feature when the drawer opens (e.g. from (+) button on a feature node). */
|
|
83
|
+
initialParentId?: string;
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
export function FeatureCreateDrawer({
|
|
@@ -76,6 +90,8 @@ export function FeatureCreateDrawer({
|
|
|
76
90
|
repositoryPath,
|
|
77
91
|
isSubmitting = false,
|
|
78
92
|
workflowDefaults,
|
|
93
|
+
features,
|
|
94
|
+
initialParentId,
|
|
79
95
|
}: FeatureCreateDrawerProps) {
|
|
80
96
|
const defaultGates = workflowDefaults?.approvalGates ?? EMPTY_GATES;
|
|
81
97
|
const defaultPush = workflowDefaults?.push ?? false;
|
|
@@ -87,6 +103,7 @@ export function FeatureCreateDrawer({
|
|
|
87
103
|
const [approvalGates, setApprovalGates] = useState<Record<string, boolean>>({ ...defaultGates });
|
|
88
104
|
const [push, setPush] = useState(defaultPush);
|
|
89
105
|
const [openPr, setOpenPr] = useState(defaultOpenPr);
|
|
106
|
+
const [parentId, setParentId] = useState<string | undefined>(undefined);
|
|
90
107
|
|
|
91
108
|
// Sync state when workflowDefaults load asynchronously
|
|
92
109
|
useEffect(() => {
|
|
@@ -97,6 +114,13 @@ export function FeatureCreateDrawer({
|
|
|
97
114
|
}
|
|
98
115
|
}, [workflowDefaults]);
|
|
99
116
|
|
|
117
|
+
// Pre-select parent when initialParentId changes (e.g. (+) button on feature node)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (open && initialParentId) {
|
|
120
|
+
setParentId(initialParentId);
|
|
121
|
+
}
|
|
122
|
+
}, [open, initialParentId]);
|
|
123
|
+
|
|
100
124
|
const resetForm = useCallback(() => {
|
|
101
125
|
setName('');
|
|
102
126
|
setDescription('');
|
|
@@ -104,6 +128,7 @@ export function FeatureCreateDrawer({
|
|
|
104
128
|
setApprovalGates({ ...defaultGates });
|
|
105
129
|
setPush(defaultPush);
|
|
106
130
|
setOpenPr(defaultOpenPr);
|
|
131
|
+
setParentId(undefined);
|
|
107
132
|
}, [defaultGates, defaultPush, defaultOpenPr]);
|
|
108
133
|
|
|
109
134
|
const handleOpenChange = useCallback(
|
|
@@ -133,9 +158,20 @@ export function FeatureCreateDrawer({
|
|
|
133
158
|
},
|
|
134
159
|
push: push || openPr,
|
|
135
160
|
openPr,
|
|
161
|
+
...(parentId ? { parentId } : {}),
|
|
136
162
|
});
|
|
137
163
|
},
|
|
138
|
-
[
|
|
164
|
+
[
|
|
165
|
+
name,
|
|
166
|
+
description,
|
|
167
|
+
attachments,
|
|
168
|
+
approvalGates,
|
|
169
|
+
repositoryPath,
|
|
170
|
+
onSubmit,
|
|
171
|
+
push,
|
|
172
|
+
openPr,
|
|
173
|
+
parentId,
|
|
174
|
+
]
|
|
139
175
|
);
|
|
140
176
|
|
|
141
177
|
const handleAddFiles = useCallback(async () => {
|
|
@@ -153,6 +189,8 @@ export function FeatureCreateDrawer({
|
|
|
153
189
|
setAttachments((prev) => prev.filter((f) => f.path !== path));
|
|
154
190
|
}, []);
|
|
155
191
|
|
|
192
|
+
const hasFeatures = features && features.length > 0;
|
|
193
|
+
|
|
156
194
|
return (
|
|
157
195
|
<Drawer direction="right" modal={false} handleOnly open={open} onOpenChange={handleOpenChange}>
|
|
158
196
|
<DrawerContent direction="right" className="w-96" showCloseButton={false}>
|
|
@@ -220,6 +258,24 @@ export function FeatureCreateDrawer({
|
|
|
220
258
|
/>
|
|
221
259
|
</div>
|
|
222
260
|
|
|
261
|
+
{/* Parent feature selector (only when features are available) */}
|
|
262
|
+
{hasFeatures ? (
|
|
263
|
+
<div className="flex flex-col gap-1.5">
|
|
264
|
+
<Label
|
|
265
|
+
htmlFor="parent-feature"
|
|
266
|
+
className="text-muted-foreground text-xs font-semibold tracking-wider"
|
|
267
|
+
>
|
|
268
|
+
PARENT FEATURE
|
|
269
|
+
</Label>
|
|
270
|
+
<ParentFeatureCombobox
|
|
271
|
+
features={features}
|
|
272
|
+
value={parentId}
|
|
273
|
+
onChange={setParentId}
|
|
274
|
+
disabled={isSubmitting}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
) : null}
|
|
278
|
+
|
|
223
279
|
{/* Auto-approve checkboxes */}
|
|
224
280
|
<div className="flex flex-col gap-1.5">
|
|
225
281
|
<Label className="text-muted-foreground text-xs font-semibold tracking-wider">
|
|
@@ -313,6 +369,147 @@ export function FeatureCreateDrawer({
|
|
|
313
369
|
);
|
|
314
370
|
}
|
|
315
371
|
|
|
372
|
+
/* ---------------------------------------------------------------------------
|
|
373
|
+
* ParentFeatureCombobox — searchable dropdown for parent feature selection
|
|
374
|
+
* ------------------------------------------------------------------------- */
|
|
375
|
+
|
|
376
|
+
interface ParentFeatureComboboxProps {
|
|
377
|
+
features: ParentFeatureOption[];
|
|
378
|
+
value: string | undefined;
|
|
379
|
+
onChange: (id: string | undefined) => void;
|
|
380
|
+
disabled?: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function ParentFeatureCombobox({
|
|
384
|
+
features,
|
|
385
|
+
value,
|
|
386
|
+
onChange,
|
|
387
|
+
disabled,
|
|
388
|
+
}: ParentFeatureComboboxProps) {
|
|
389
|
+
const [open, setOpen] = useState(false);
|
|
390
|
+
const [query, setQuery] = useState('');
|
|
391
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
392
|
+
|
|
393
|
+
const selectedFeature = features.find((f) => f.id === value);
|
|
394
|
+
|
|
395
|
+
const filtered = query.trim()
|
|
396
|
+
? features.filter(
|
|
397
|
+
(f) =>
|
|
398
|
+
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
399
|
+
f.id.toLowerCase().includes(query.toLowerCase())
|
|
400
|
+
)
|
|
401
|
+
: features;
|
|
402
|
+
|
|
403
|
+
const handleSelect = useCallback(
|
|
404
|
+
(id: string | undefined) => {
|
|
405
|
+
onChange(id);
|
|
406
|
+
setOpen(false);
|
|
407
|
+
setQuery('');
|
|
408
|
+
},
|
|
409
|
+
[onChange]
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (open) {
|
|
414
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
415
|
+
} else {
|
|
416
|
+
setQuery('');
|
|
417
|
+
}
|
|
418
|
+
}, [open]);
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
422
|
+
<PopoverTrigger asChild>
|
|
423
|
+
<button
|
|
424
|
+
id="parent-feature"
|
|
425
|
+
type="button"
|
|
426
|
+
role="combobox"
|
|
427
|
+
aria-expanded={open}
|
|
428
|
+
aria-label="Parent Feature"
|
|
429
|
+
disabled={disabled}
|
|
430
|
+
data-testid="parent-feature-combobox"
|
|
431
|
+
className={cn(
|
|
432
|
+
'border-input bg-background ring-offset-background focus:ring-ring flex h-9 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
433
|
+
!selectedFeature && 'text-muted-foreground'
|
|
434
|
+
)}
|
|
435
|
+
>
|
|
436
|
+
<span className="truncate">
|
|
437
|
+
{selectedFeature
|
|
438
|
+
? `${selectedFeature.name} (${selectedFeature.id.slice(0, 8)})`
|
|
439
|
+
: 'Select parent feature...'}
|
|
440
|
+
</span>
|
|
441
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
442
|
+
</button>
|
|
443
|
+
</PopoverTrigger>
|
|
444
|
+
<PopoverContent
|
|
445
|
+
className="w-80 p-0"
|
|
446
|
+
align="start"
|
|
447
|
+
data-testid="parent-feature-combobox-content"
|
|
448
|
+
>
|
|
449
|
+
<div className="flex flex-col">
|
|
450
|
+
{/* Search input */}
|
|
451
|
+
<div className="border-b p-2">
|
|
452
|
+
<Input
|
|
453
|
+
ref={inputRef}
|
|
454
|
+
placeholder="Search features..."
|
|
455
|
+
value={query}
|
|
456
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
457
|
+
className="h-8 border-0 p-0 text-sm shadow-none focus-visible:ring-0"
|
|
458
|
+
data-testid="parent-feature-search"
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
{/* Options list */}
|
|
463
|
+
<div className="max-h-48 overflow-y-auto py-1" role="listbox" aria-label="Features">
|
|
464
|
+
{/* No parent option */}
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
role="option"
|
|
468
|
+
aria-selected={value === undefined}
|
|
469
|
+
onClick={() => handleSelect(undefined)}
|
|
470
|
+
className={cn(
|
|
471
|
+
'hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 px-3 py-2 text-sm',
|
|
472
|
+
value === undefined && 'bg-accent/50'
|
|
473
|
+
)}
|
|
474
|
+
data-testid="parent-feature-option-none"
|
|
475
|
+
>
|
|
476
|
+
<CheckIcon className={cn('h-4 w-4 shrink-0', value !== undefined && 'invisible')} />
|
|
477
|
+
<span className="text-muted-foreground italic">No parent</span>
|
|
478
|
+
</button>
|
|
479
|
+
|
|
480
|
+
{filtered.length === 0 && query ? (
|
|
481
|
+
<p className="text-muted-foreground px-3 py-2 text-sm">No features found.</p>
|
|
482
|
+
) : (
|
|
483
|
+
filtered.map((f) => (
|
|
484
|
+
<button
|
|
485
|
+
key={f.id}
|
|
486
|
+
type="button"
|
|
487
|
+
role="option"
|
|
488
|
+
aria-selected={value === f.id}
|
|
489
|
+
onClick={() => handleSelect(f.id)}
|
|
490
|
+
className={cn(
|
|
491
|
+
'hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 px-3 py-2 text-sm',
|
|
492
|
+
value === f.id && 'bg-accent/50'
|
|
493
|
+
)}
|
|
494
|
+
data-testid={`parent-feature-option-${f.id}`}
|
|
495
|
+
>
|
|
496
|
+
<CheckIcon className={cn('h-4 w-4 shrink-0', value !== f.id && 'invisible')} />
|
|
497
|
+
<span className="truncate">
|
|
498
|
+
{f.name}{' '}
|
|
499
|
+
<span className="text-muted-foreground font-mono text-xs">
|
|
500
|
+
({f.id.slice(0, 8)})
|
|
501
|
+
</span>
|
|
502
|
+
</span>
|
|
503
|
+
</button>
|
|
504
|
+
))
|
|
505
|
+
)}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</PopoverContent>
|
|
509
|
+
</Popover>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
316
513
|
/* ---------------------------------------------------------------------------
|
|
317
514
|
* Private sub-components & utilities
|
|
318
515
|
* ------------------------------------------------------------------------- */
|
|
@@ -21,6 +21,11 @@ import type { FeatureNodeState } from './feature-node-state-config';
|
|
|
21
21
|
* 6. No agent run → fall back to plan tasks / lifecycle
|
|
22
22
|
*/
|
|
23
23
|
export function deriveNodeState(feature: Feature, agentRun?: AgentRun | null): FeatureNodeState {
|
|
24
|
+
// Blocked lifecycle takes priority — child waiting on parent regardless of agent run
|
|
25
|
+
if (feature.lifecycle === SdlcLifecycle.Blocked) {
|
|
26
|
+
return 'blocked';
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
if (agentRun) {
|
|
25
30
|
switch (agentRun.status) {
|
|
26
31
|
case AgentRunStatus.waitingApproval:
|
package/web/.next/standalone/src/presentation/web/components/common/feature-node/feature-node.tsx
CHANGED
|
@@ -100,7 +100,7 @@ export function FeatureNode({
|
|
|
100
100
|
data-testid="feature-node-lifecycle-label"
|
|
101
101
|
className={cn('text-[10px] font-semibold tracking-wider')}
|
|
102
102
|
>
|
|
103
|
-
{lifecycleDisplayLabels[data.lifecycle]}
|
|
103
|
+
{data.state === 'blocked' ? 'BLOCKED' : lifecycleDisplayLabels[data.lifecycle]}
|
|
104
104
|
</span>
|
|
105
105
|
{data.onSettings ? (
|
|
106
106
|
<button
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import type { Edge } from '@xyflow/react';
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import type {
|
|
@@ -18,6 +18,7 @@ import type { TechDecisionsReviewData } from '@/components/common/tech-decisions
|
|
|
18
18
|
import type { MergeReviewData } from '@/components/common/merge-review';
|
|
19
19
|
import { FeaturesCanvas } from '@/components/features/features-canvas';
|
|
20
20
|
import type { CanvasNodeType } from '@/components/features/features-canvas';
|
|
21
|
+
import type { FeatureNodeData } from '@/components/common/feature-node';
|
|
21
22
|
import { FeatureDrawer, FeatureCreateDrawer } from '@/components/common';
|
|
22
23
|
import { PrdQuestionnaireDrawer } from '@/components/common/prd-questionnaire';
|
|
23
24
|
import type { PrdQuestionnaireData } from '@/components/common/prd-questionnaire';
|
|
@@ -52,8 +53,22 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
|
|
|
52
53
|
handleDeleteFeature,
|
|
53
54
|
handleDeleteRepository,
|
|
54
55
|
closeCreateDrawer,
|
|
56
|
+
pendingParentFeatureId,
|
|
55
57
|
} = useControlCenterState(initialNodes, initialEdges);
|
|
56
58
|
|
|
59
|
+
// Extract feature list for the parent selector in the create drawer
|
|
60
|
+
const featureOptions = useMemo(
|
|
61
|
+
() =>
|
|
62
|
+
nodes
|
|
63
|
+
.filter((n) => n.type === 'featureNode')
|
|
64
|
+
.map((n) => {
|
|
65
|
+
const data = n.data as FeatureNodeData;
|
|
66
|
+
return { id: data.featureId, name: data.name };
|
|
67
|
+
})
|
|
68
|
+
.filter((f) => f.id && !f.id.startsWith('#')), // exclude optimistic temp nodes
|
|
69
|
+
[nodes]
|
|
70
|
+
);
|
|
71
|
+
|
|
57
72
|
// Workflow defaults for the create-feature drawer
|
|
58
73
|
const [workflowDefaults, setWorkflowDefaults] = useState<WorkflowDefaults | undefined>();
|
|
59
74
|
useEffect(() => {
|
|
@@ -321,6 +336,8 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
|
|
|
321
336
|
onSubmit={handleCreateFeatureSubmit}
|
|
322
337
|
repositoryPath={pendingRepositoryPath}
|
|
323
338
|
workflowDefaults={workflowDefaults}
|
|
339
|
+
features={featureOptions}
|
|
340
|
+
initialParentId={pendingParentFeatureId}
|
|
324
341
|
/>
|
|
325
342
|
</>
|
|
326
343
|
);
|
|
@@ -414,6 +431,8 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
|
|
|
414
431
|
onSubmit={handleCreateFeatureSubmit}
|
|
415
432
|
repositoryPath={pendingRepositoryPath}
|
|
416
433
|
workflowDefaults={workflowDefaults}
|
|
434
|
+
features={featureOptions}
|
|
435
|
+
initialParentId={pendingParentFeatureId}
|
|
417
436
|
/>
|
|
418
437
|
</>
|
|
419
438
|
);
|
|
@@ -21,6 +21,8 @@ export interface ControlCenterState {
|
|
|
21
21
|
selectedNode: FeatureNodeData | null;
|
|
22
22
|
isCreateDrawerOpen: boolean;
|
|
23
23
|
pendingRepositoryPath: string;
|
|
24
|
+
/** Parent feature ID pre-selected when (+) is clicked on a feature node. */
|
|
25
|
+
pendingParentFeatureId: string | undefined;
|
|
24
26
|
onNodesChange: (changes: NodeChange<CanvasNodeType>[]) => void;
|
|
25
27
|
handleConnect: (connection: Connection) => void;
|
|
26
28
|
clearSelection: () => void;
|
|
@@ -191,7 +193,11 @@ export function useControlCenterState(
|
|
|
191
193
|
);
|
|
192
194
|
|
|
193
195
|
const createFeatureNode = useCallback(
|
|
194
|
-
(
|
|
196
|
+
(
|
|
197
|
+
sourceNodeId: string | null,
|
|
198
|
+
dataOverride?: Partial<FeatureNodeData>,
|
|
199
|
+
edgeType?: string
|
|
200
|
+
): string => {
|
|
195
201
|
const id = `feature-${Date.now()}-${nextFeatureId++}`;
|
|
196
202
|
const newFeatureData: FeatureNodeData = {
|
|
197
203
|
name: dataOverride?.name ?? 'New Feature',
|
|
@@ -292,10 +298,13 @@ export function useControlCenterState(
|
|
|
292
298
|
setEdges((currentEdges) => [
|
|
293
299
|
...currentEdges,
|
|
294
300
|
{
|
|
295
|
-
id:
|
|
301
|
+
id:
|
|
302
|
+
edgeType === 'dependencyEdge'
|
|
303
|
+
? `dep-${sourceNodeId}-${id}`
|
|
304
|
+
: `edge-${sourceNodeId}-${id}`,
|
|
296
305
|
source: sourceNodeId,
|
|
297
306
|
target: id,
|
|
298
|
-
style: { strokeDasharray: '5 5' },
|
|
307
|
+
...(edgeType ? { type: edgeType } : { style: { strokeDasharray: '5 5' } }),
|
|
299
308
|
},
|
|
300
309
|
]);
|
|
301
310
|
}
|
|
@@ -314,19 +323,30 @@ export function useControlCenterState(
|
|
|
314
323
|
setIsCreateDrawerOpen(true);
|
|
315
324
|
}, []);
|
|
316
325
|
|
|
326
|
+
const [pendingParentFeatureId, setPendingParentFeatureId] = useState<string | undefined>();
|
|
327
|
+
|
|
317
328
|
const handleCreateFeatureSubmit = useCallback(
|
|
318
329
|
(data: FeatureCreatePayload) => {
|
|
319
330
|
// 1. Insert optimistic node instantly
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
// For child features, connect to the parent feature node instead of the repo node
|
|
332
|
+
const sourceNodeId = pendingParentFeatureId
|
|
333
|
+
? `feat-${pendingParentFeatureId}`
|
|
334
|
+
: pendingRepoNodeId;
|
|
335
|
+
const tempId = createFeatureNode(
|
|
336
|
+
sourceNodeId,
|
|
337
|
+
{
|
|
338
|
+
state: 'creating',
|
|
339
|
+
name: data.name,
|
|
340
|
+
description: data.description,
|
|
341
|
+
repositoryPath: data.repositoryPath,
|
|
342
|
+
},
|
|
343
|
+
pendingParentFeatureId ? 'dependencyEdge' : undefined
|
|
344
|
+
);
|
|
326
345
|
|
|
327
346
|
// 2. Close drawer and clear pending state immediately
|
|
328
347
|
setIsCreateDrawerOpen(false);
|
|
329
348
|
setPendingRepoNodeId(null);
|
|
349
|
+
setPendingParentFeatureId(undefined);
|
|
330
350
|
|
|
331
351
|
// 3. Fire server action in the background
|
|
332
352
|
createFeature(data)
|
|
@@ -348,11 +368,12 @@ export function useControlCenterState(
|
|
|
348
368
|
toast.error('Failed to create feature');
|
|
349
369
|
});
|
|
350
370
|
},
|
|
351
|
-
[router, createFeatureNode, pendingRepoNodeId, setEdges]
|
|
371
|
+
[router, createFeatureNode, pendingRepoNodeId, pendingParentFeatureId, setEdges]
|
|
352
372
|
);
|
|
353
373
|
|
|
354
374
|
const closeCreateDrawer = useCallback(() => {
|
|
355
375
|
setIsCreateDrawerOpen(false);
|
|
376
|
+
setPendingParentFeatureId(undefined);
|
|
356
377
|
}, []);
|
|
357
378
|
|
|
358
379
|
const handleDeleteFeature = useCallback(
|
|
@@ -426,9 +447,18 @@ export function useControlCenterState(
|
|
|
426
447
|
|
|
427
448
|
const handleAddFeatureToFeature = useCallback(
|
|
428
449
|
(featureNodeId: string) => {
|
|
429
|
-
|
|
450
|
+
// Extract feature ID from node ID (format: "feat-<uuid>")
|
|
451
|
+
const featureId = featureNodeId.startsWith('feat-') ? featureNodeId.slice(5) : featureNodeId;
|
|
452
|
+
// Find the repo node that owns this feature
|
|
453
|
+
const repoEdge = edges.find((e) => e.target === featureNodeId);
|
|
454
|
+
const repoNodeId = repoEdge?.source ?? null;
|
|
455
|
+
|
|
456
|
+
setSelectedNode(null);
|
|
457
|
+
setPendingRepoNodeId(repoNodeId);
|
|
458
|
+
setPendingParentFeatureId(featureId);
|
|
459
|
+
setIsCreateDrawerOpen(true);
|
|
430
460
|
},
|
|
431
|
-
[
|
|
461
|
+
[edges]
|
|
432
462
|
);
|
|
433
463
|
|
|
434
464
|
const handleLayout = useCallback(
|
|
@@ -564,6 +594,7 @@ export function useControlCenterState(
|
|
|
564
594
|
selectedNode,
|
|
565
595
|
isCreateDrawerOpen,
|
|
566
596
|
pendingRepositoryPath,
|
|
597
|
+
pendingParentFeatureId,
|
|
567
598
|
onNodesChange,
|
|
568
599
|
handleConnect,
|
|
569
600
|
clearSelection,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ReactFlow, ReactFlowProvider } from '@xyflow/react';
|
|
3
|
+
import { DependencyEdge } from './dependency-edge';
|
|
4
|
+
import { FeatureNode } from '@/components/common/feature-node';
|
|
5
|
+
import type { FeatureNodeData } from '@/components/common/feature-node';
|
|
6
|
+
|
|
7
|
+
import '@xyflow/react/dist/style.css';
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: 'Features/DependencyEdge',
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div style={{ width: '800px', height: '400px' }}>
|
|
14
|
+
<ReactFlowProvider>
|
|
15
|
+
<Story />
|
|
16
|
+
</ReactFlowProvider>
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
const parentData: FeatureNodeData = {
|
|
25
|
+
name: 'Auth System',
|
|
26
|
+
description: 'User authentication',
|
|
27
|
+
featureId: 'feat-1234',
|
|
28
|
+
lifecycle: 'implementation',
|
|
29
|
+
state: 'running',
|
|
30
|
+
progress: 60,
|
|
31
|
+
repositoryPath: '/repo',
|
|
32
|
+
branch: 'feat/auth',
|
|
33
|
+
showHandles: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const childData: FeatureNodeData = {
|
|
37
|
+
name: 'Login Page',
|
|
38
|
+
description: 'Login UI component',
|
|
39
|
+
featureId: 'feat-5678',
|
|
40
|
+
lifecycle: 'requirements',
|
|
41
|
+
state: 'blocked',
|
|
42
|
+
progress: 0,
|
|
43
|
+
repositoryPath: '/repo',
|
|
44
|
+
branch: 'feat/login',
|
|
45
|
+
blockedBy: 'Auth System',
|
|
46
|
+
showHandles: true,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Default: StoryObj = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<ReactFlow
|
|
52
|
+
nodes={[
|
|
53
|
+
{
|
|
54
|
+
id: 'parent',
|
|
55
|
+
type: 'featureNode',
|
|
56
|
+
position: { x: 50, y: 100 },
|
|
57
|
+
data: parentData,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'child',
|
|
61
|
+
type: 'featureNode',
|
|
62
|
+
position: { x: 450, y: 100 },
|
|
63
|
+
data: childData,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
edges={[
|
|
67
|
+
{
|
|
68
|
+
id: 'dep-1',
|
|
69
|
+
source: 'parent',
|
|
70
|
+
target: 'child',
|
|
71
|
+
type: 'dependencyEdge',
|
|
72
|
+
},
|
|
73
|
+
]}
|
|
74
|
+
nodeTypes={{ featureNode: FeatureNode }}
|
|
75
|
+
edgeTypes={{ dependencyEdge: DependencyEdge }}
|
|
76
|
+
fitView
|
|
77
|
+
nodesDraggable={false}
|
|
78
|
+
nodesConnectable={false}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { BaseEdge, getBezierPath } from '@xyflow/react';
|
|
4
|
+
import type { EdgeProps } from '@xyflow/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Custom React Flow edge for parent→child feature dependencies.
|
|
8
|
+
* Uses bezier curves and dashed style matching repo→feature edges.
|
|
9
|
+
*/
|
|
10
|
+
export function DependencyEdge(props: EdgeProps) {
|
|
11
|
+
const [edgePath] = getBezierPath({
|
|
12
|
+
sourceX: props.sourceX,
|
|
13
|
+
sourceY: props.sourceY,
|
|
14
|
+
targetX: props.targetX,
|
|
15
|
+
targetY: props.targetY,
|
|
16
|
+
sourcePosition: props.sourcePosition,
|
|
17
|
+
targetPosition: props.targetPosition,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<BaseEdge
|
|
22
|
+
id={props.id}
|
|
23
|
+
path={edgePath}
|
|
24
|
+
style={{
|
|
25
|
+
strokeDasharray: '5 5',
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|