docs: Refine implementation plan and document draft feature
- Updated IMPLEMENTATION-PLAN-STEP-LIBRARY-FRONTEND.md with design decisions: - Custom steps persistence: separate `custom_steps` field in sessions - Custom step navigation: full step type support (decision/action/solution) - Validation warnings: inline dismissible, no confirmation modal - Added backend migration task (B.10) for custom_steps field - Updated file count: 10 new, 8 modified, 1 migration - Clarified acceptance criteria for validation behavior - Created docs/plans/2026-02-03-draft-trees-feature.md: - Comprehensive design for draft trees and custom steps - Database schema, API changes, frontend UX patterns - Implementation phases and success metrics - Related to Issue #25 (planned for Phase 3) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,29 @@
|
||||
# Implementation Plan: Step Library Frontend + Tree Editor Validation
|
||||
|
||||
> **Date:** February 3, 2026
|
||||
> **Date:** February 3, 2026 (Updated after refinement session)
|
||||
> **Scope:** Issues #1, #8, #9, #10
|
||||
> **Estimated Components:** 8 new files, 4 modified files
|
||||
> **Estimated Components:** 10 new files, 8 modified files, 1 migration
|
||||
> **Parallel Workstreams:** 2
|
||||
> **Related Planning:** Issue #25 (Draft Trees - Phase 3)
|
||||
|
||||
---
|
||||
|
||||
## Plan Refinements (Feb 3, 2026)
|
||||
|
||||
This plan was refined through collaborative design discussion. Key changes from initial draft:
|
||||
|
||||
1. **Custom Steps Persistence** - Changed from storing in `decisions` array to separate `custom_steps` JSONB field
|
||||
- Added: Backend migration task (B.10)
|
||||
- Added: Session schema updates
|
||||
- Enables: Session resuming with custom steps
|
||||
|
||||
2. **Custom Step Navigation** - Expanded to support all step types (decision/action/solution)
|
||||
- Added: Detailed rendering logic for each type in B.11
|
||||
- Enables: Custom branching decisions during troubleshooting
|
||||
|
||||
3. **Validation Warnings** - Clarified as inline/dismissible (no confirmation modal)
|
||||
- Updated: Acceptance criteria to specify warning behavior
|
||||
- Deferred: "Save as Draft" feature to Issue #25 (Phase 3)
|
||||
|
||||
---
|
||||
|
||||
@@ -108,10 +128,11 @@ Features:
|
||||
|
||||
Modifications:
|
||||
1. Call `validate()` before save attempt
|
||||
2. Show ValidationSummary when errors exist
|
||||
3. Block save if any severity='error' exists (warnings allow save)
|
||||
4. Add "Validate" button in toolbar for manual check
|
||||
5. Auto-validate on blur from form fields (debounced)
|
||||
2. Show ValidationSummary when errors or warnings exist
|
||||
3. Block save if any severity='error' exists (disable save button)
|
||||
4. Warnings are **informational only** - do not block save, no confirmation modal
|
||||
5. Add "Validate" button in toolbar for manual check
|
||||
6. Auto-validate on blur from form fields (debounced)
|
||||
|
||||
#### A.4: Visual Node Error Indicators
|
||||
**File:** `frontend/src/components/tree-editor/NodeList.tsx`
|
||||
@@ -123,13 +144,14 @@ Modifications:
|
||||
|
||||
### Acceptance Criteria - Workstream A
|
||||
|
||||
- [ ] Cannot save tree without a name
|
||||
- [ ] Cannot save tree without at least one solution node
|
||||
- [ ] Cannot save tree without a name (error blocks save)
|
||||
- [ ] Cannot save tree without at least one solution node (error blocks save)
|
||||
- [ ] Validation errors display in a clear, clickable list
|
||||
- [ ] Clicking an error selects the problematic node
|
||||
- [ ] Circular references are detected and blocked
|
||||
- [ ] Orphan nodes show as warnings (allow save with confirmation)
|
||||
- [ ] Circular references are detected and blocked (error)
|
||||
- [ ] Orphan nodes show as warnings (informational, do not block save)
|
||||
- [ ] Save button disabled when errors exist
|
||||
- [ ] Warnings are dismissible/ignorable - user can save with warnings present
|
||||
|
||||
---
|
||||
|
||||
@@ -387,7 +409,66 @@ interface CustomStepModalProps {
|
||||
|
||||
### Issue #8: Add Custom Step Button in Tree Navigation
|
||||
|
||||
#### B.10: Modify TreeNavigationPage
|
||||
#### B.10: Add Custom Steps Backend Support
|
||||
**Files:**
|
||||
- `backend/alembic/versions/XXXX_add_custom_steps_to_sessions.py` (NEW - migration)
|
||||
- `backend/app/schemas/session.py` (MODIFIED)
|
||||
- `backend/app/api/endpoints/sessions.py` (MODIFIED)
|
||||
|
||||
**Migration:**
|
||||
```python
|
||||
def upgrade():
|
||||
# Add custom_steps JSONB column to sessions table
|
||||
op.add_column('sessions',
|
||||
sa.Column('custom_steps', JSONB, nullable=False, server_default='[]')
|
||||
)
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('sessions', 'custom_steps')
|
||||
```
|
||||
|
||||
**Schema Updates:**
|
||||
```python
|
||||
# In DecisionRecord - no changes needed (still uses node_id)
|
||||
|
||||
# In SessionResponse - add custom_steps field
|
||||
class SessionResponse(BaseModel):
|
||||
# ... existing fields
|
||||
custom_steps: list[dict[str, Any]] = Field(default_factory=list) # NEW
|
||||
|
||||
# In SessionUpdate - add custom_steps field
|
||||
class SessionUpdate(BaseModel):
|
||||
# ... existing fields
|
||||
custom_steps: Optional[list[dict[str, Any]]] = None # NEW
|
||||
```
|
||||
|
||||
**Custom Step Structure in Database:**
|
||||
```python
|
||||
# Each item in custom_steps array:
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"inserted_after_node_id": "parent-node-id",
|
||||
"step_data": {
|
||||
"title": "Check Additional Logs",
|
||||
"step_type": "action", # decision | action | solution
|
||||
"content": {
|
||||
"instructions": "Check /var/log/...",
|
||||
"help_text": "Optional help",
|
||||
"commands": [...] # optional
|
||||
},
|
||||
"category_id": "optional-uuid",
|
||||
"tags": ["optional"]
|
||||
},
|
||||
"timestamp": "2026-02-03T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**API Logic:**
|
||||
- `PUT /api/v1/sessions/{id}` - Accept `custom_steps` in request body
|
||||
- Validation: Ensure `step_data.step_type` is valid enum value
|
||||
- No cascade delete needed - custom steps are session-scoped
|
||||
|
||||
#### B.11: Modify TreeNavigationPage
|
||||
**File:** `frontend/src/pages/TreeNavigationPage.tsx`
|
||||
|
||||
Add state:
|
||||
@@ -413,31 +494,83 @@ const [customSteps, setCustomSteps] = useState<CustomStep[]>([])
|
||||
3. Handle insert:
|
||||
```typescript
|
||||
const handleInsertCustomStep = (step: Step | CustomStepDraft) => {
|
||||
// Insert after current node in session
|
||||
// Create custom step object
|
||||
const customStep: CustomStep = {
|
||||
id: crypto.randomUUID(),
|
||||
inserted_after_node_id: currentNodeId,
|
||||
step_data: step,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
setCustomSteps([...customSteps, customStep])
|
||||
|
||||
// Add to local state
|
||||
const newCustomSteps = [...customSteps, customStep]
|
||||
setCustomSteps(newCustomSteps)
|
||||
|
||||
// Navigate to custom step (becomes current)
|
||||
setCurrentNodeId(customStep.id)
|
||||
setShowCustomStepModal(false)
|
||||
|
||||
// Save to backend
|
||||
if (session) {
|
||||
sessionsApi.update(session.id, {
|
||||
custom_steps: newCustomSteps
|
||||
}).catch(err => console.error('Failed to save custom step:', err))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Render custom steps in the navigation flow with visual indicator:
|
||||
4. Render custom steps based on type:
|
||||
```tsx
|
||||
{/* Custom Step Badge */}
|
||||
<span className="rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
|
||||
Custom Step
|
||||
</span>
|
||||
{/* Render custom step in navigation flow */}
|
||||
{currentNode?.id.startsWith('custom-') && (
|
||||
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
|
||||
{/* Custom Step Badge */}
|
||||
<span className="mb-2 inline-block rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
|
||||
Custom Step
|
||||
</span>
|
||||
|
||||
{/* Render based on step type */}
|
||||
{customStepNode.step_data.step_type === 'decision' && (
|
||||
<>
|
||||
<h2>{customStepNode.step_data.title}</h2>
|
||||
<MarkdownContent content={customStepNode.step_data.content.instructions} />
|
||||
{/* Render decision options - user creates them inline or uses predefined */}
|
||||
<div className="mt-4 space-y-2">
|
||||
<button onClick={() => handleCustomDecisionOption('yes')}>Yes</button>
|
||||
<button onClick={() => handleCustomDecisionOption('no')}>No</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{customStepNode.step_data.step_type === 'action' && (
|
||||
<>
|
||||
<h2>{customStepNode.step_data.title}</h2>
|
||||
<MarkdownContent content={customStepNode.step_data.content.instructions} />
|
||||
{customStepNode.step_data.content.commands?.map(cmd => (
|
||||
<pre key={cmd.label}>{cmd.command}</pre>
|
||||
))}
|
||||
<button onClick={handleContinueFromCustomAction}>Continue</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{customStepNode.step_data.step_type === 'solution' && (
|
||||
<>
|
||||
<h2>{customStepNode.step_data.title}</h2>
|
||||
<MarkdownContent content={customStepNode.step_data.content.instructions} />
|
||||
<button onClick={handleComplete}>Complete Session</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
#### B.11: Update Session Export
|
||||
**File:** `frontend/src/components/session/ExportPreviewModal.tsx`
|
||||
**Navigation Logic:**
|
||||
- Custom decision: User selects option → continues to next tree node or another custom step
|
||||
- Custom action: User clicks Continue → returns to tree flow (next_node_id of original tree node)
|
||||
- Custom solution: User clicks Complete → ends session
|
||||
|
||||
#### B.12: Update Session Export
|
||||
**File:** `frontend/src/components/session/ExportPreviewModal.tsx` or `backend/app/api/endpoints/sessions.py`
|
||||
|
||||
Ensure custom steps are included in export with clear marking:
|
||||
|
||||
@@ -448,7 +581,7 @@ Ensure custom steps are included in export with clear marking:
|
||||
Instructions: ...
|
||||
```
|
||||
|
||||
#### B.12: Update Session Types
|
||||
#### B.13: Update Session Types
|
||||
**File:** `frontend/src/types/index.ts`
|
||||
|
||||
Add:
|
||||
@@ -487,26 +620,29 @@ export interface CustomStepDraft {
|
||||
|
||||
| File | Issue | Description |
|
||||
|------|-------|-------------|
|
||||
| `components/tree-editor/ValidationSummary.tsx` | #1 | Error/warning display |
|
||||
| `api/steps.ts` | #10 | Steps API client |
|
||||
| `api/stepCategories.ts` | #10 | Categories API client |
|
||||
| `types/step.ts` | #10 | Step TypeScript types |
|
||||
| `components/step-library/StepCard.tsx` | #10 | Step list item card |
|
||||
| `components/step-library/StepDetailModal.tsx` | #10 | Step preview modal |
|
||||
| `components/step-library/StepLibraryBrowser.tsx` | #10 | Main browser component |
|
||||
| `components/step-library/StepForm.tsx` | #9 | Step creation form |
|
||||
| `components/step-library/CustomStepModal.tsx` | #9 | Tabbed modal wrapper |
|
||||
| `frontend/components/tree-editor/ValidationSummary.tsx` | #1 | Error/warning display |
|
||||
| `frontend/api/steps.ts` | #10 | Steps API client |
|
||||
| `frontend/api/stepCategories.ts` | #10 | Categories API client |
|
||||
| `frontend/types/step.ts` | #10 | Step TypeScript types |
|
||||
| `frontend/components/step-library/StepCard.tsx` | #10 | Step list item card |
|
||||
| `frontend/components/step-library/StepDetailModal.tsx` | #10 | Step preview modal |
|
||||
| `frontend/components/step-library/StepLibraryBrowser.tsx` | #10 | Main browser component |
|
||||
| `frontend/components/step-library/StepForm.tsx` | #9 | Step creation form |
|
||||
| `frontend/components/step-library/CustomStepModal.tsx` | #9 | Tabbed modal wrapper |
|
||||
| `backend/alembic/versions/XXXX_add_custom_steps.py` | #8 | Add custom_steps to sessions |
|
||||
|
||||
### Modified Files (5)
|
||||
### Modified Files (6)
|
||||
|
||||
| File | Issue | Changes |
|
||||
|------|-------|---------|
|
||||
| `store/treeEditorStore.ts` | #1 | Add circular reference detection |
|
||||
| `pages/TreeEditorPage.tsx` | #1 | Integrate validation UI |
|
||||
| `components/tree-editor/NodeList.tsx` | #1 | Node error indicators |
|
||||
| `pages/TreeNavigationPage.tsx` | #8 | Add custom step button + modal |
|
||||
| `api/index.ts` | #10 | Export new API clients |
|
||||
| `types/index.ts` | #8 | Add CustomStep types |
|
||||
| `frontend/store/treeEditorStore.ts` | #1 | Add circular reference detection |
|
||||
| `frontend/pages/TreeEditorPage.tsx` | #1 | Integrate validation UI |
|
||||
| `frontend/components/tree-editor/NodeList.tsx` | #1 | Node error indicators |
|
||||
| `frontend/pages/TreeNavigationPage.tsx` | #8 | Add custom step button + rendering |
|
||||
| `frontend/api/index.ts` | #10 | Export new API clients |
|
||||
| `frontend/types/index.ts` | #8 | Add CustomStep types |
|
||||
| `backend/app/schemas/session.py` | #8 | Add custom_steps field |
|
||||
| `backend/app/api/endpoints/sessions.py` | #8 | Handle custom_steps in update |
|
||||
|
||||
---
|
||||
|
||||
@@ -536,8 +672,9 @@ Workstream A Workstream B
|
||||
───────────── ─────────────
|
||||
Testing & polish B.8 StepForm
|
||||
B.9 CustomStepModal
|
||||
B.10 TreeNavigationPage integration
|
||||
B.11-12 Export & types updates
|
||||
B.10 Backend custom steps support (migration + schemas)
|
||||
B.11 TreeNavigationPage integration
|
||||
B.12-13 Export & types updates
|
||||
```
|
||||
|
||||
---
|
||||
@@ -578,13 +715,54 @@ Testing & polish B.8 StepForm
|
||||
|
||||
---
|
||||
|
||||
## Questions to Resolve Before Starting
|
||||
## Design Decisions (Resolved)
|
||||
|
||||
1. **Custom steps persistence**: Should custom steps be saved to the session in the database, or only exist in frontend state until export?
|
||||
- *Recommendation*: Save to session.decisions with a `is_custom_step: true` flag
|
||||
### 1. Custom Steps Persistence ✅
|
||||
**Decision:** Separate `custom_steps` JSONB field in sessions table
|
||||
|
||||
2. **Step library page**: Should there be a standalone `/steps` page for browsing the library outside of navigation?
|
||||
- *Recommendation*: Yes, add this as a future enhancement after Issue #10
|
||||
**Rationale:**
|
||||
- Clean separation of concerns (decisions track actions taken, custom_steps store step content)
|
||||
- Enables session resuming with custom steps intact
|
||||
- Easier to query and export custom steps separately
|
||||
- Follows normalized data structure patterns
|
||||
|
||||
3. **Rate limiting**: Should step creation have rate limiting to prevent spam?
|
||||
- *Recommendation*: Backend concern, out of scope for this plan
|
||||
**Implementation:** Task B.10 adds migration + schema updates
|
||||
|
||||
### 2. Custom Step Navigation Flow ✅
|
||||
**Decision:** Full step type support (decision/action/solution)
|
||||
|
||||
**Rationale:**
|
||||
- Real-world troubleshooting requires branching after custom actions
|
||||
- Example: "Try clearing cache" (action) → "Did it work?" (decision)
|
||||
- Consistent with tree structure mental model
|
||||
- Maximum flexibility for engineers
|
||||
|
||||
**Implementation:** Task B.11 renders custom steps based on `step_type`
|
||||
|
||||
### 3. Validation Warning UX ✅
|
||||
**Decision:** Inline dismissible warnings, no confirmation modal
|
||||
|
||||
**Rationale:**
|
||||
- Warnings are informational, not critical
|
||||
- Orphan nodes might be intentional (work in progress)
|
||||
- Trust users to manage their own tree quality
|
||||
- Simpler UX, fewer clicks
|
||||
- Draft feature (Issue #25) handles "save incomplete work" use case
|
||||
|
||||
**Implementation:** Task A.2-A.3 show warnings in ValidationSummary, don't block save
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements (Out of Scope)
|
||||
|
||||
1. **Draft Trees Feature** - Documented in Issue #25
|
||||
- Save incomplete trees without validation errors blocking
|
||||
- Planned for Phase 3 after Step Library is complete
|
||||
|
||||
2. **Standalone Step Library Page** - `/steps` route
|
||||
- Browse/manage personal step library outside of active sessions
|
||||
- Add after Issue #10 is complete and usage patterns are understood
|
||||
|
||||
3. **Rate Limiting** - Backend concern
|
||||
- Prevent step library spam
|
||||
- Add if abuse is observed in production
|
||||
|
||||
520
docs/plans/2026-02-03-draft-trees-feature.md
Normal file
520
docs/plans/2026-02-03-draft-trees-feature.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# Feature Design: Draft Trees & Custom Steps
|
||||
|
||||
> **Date:** February 3, 2026
|
||||
> **Status:** Planned for Phase 3
|
||||
> **Related Issues:** TBD
|
||||
> **Dependencies:** Tree Editor Validation UI (Issue #1)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Enable users to save incomplete trees and custom steps as drafts, allowing them to return later to finish editing without validation errors blocking their work.
|
||||
|
||||
**Use Cases:**
|
||||
- Building a complex tree over multiple sessions
|
||||
- Starting a tree without all solution nodes defined
|
||||
- Experimenting with tree structures before publishing
|
||||
- Saving custom steps for later refinement
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Currently, validation errors block saving trees. This creates friction when:
|
||||
- User wants to save progress on a complex tree (10+ nodes)
|
||||
- User is interrupted mid-editing and needs to save incomplete work
|
||||
- User wants to experiment without committing to a "valid" structure
|
||||
- User creates a custom step during troubleshooting but wants to refine it later
|
||||
|
||||
**Goal:** Allow users to save work-in-progress without bypassing quality checks for published trees.
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Database Changes
|
||||
|
||||
#### Trees Table
|
||||
Add `status` column to `trees` table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE trees
|
||||
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'published';
|
||||
|
||||
ALTER TABLE trees
|
||||
ADD CONSTRAINT trees_status_check
|
||||
CHECK (status IN ('draft', 'published'));
|
||||
|
||||
CREATE INDEX idx_trees_status ON trees(status);
|
||||
```
|
||||
|
||||
**Statuses:**
|
||||
- `draft` - Incomplete, may have validation errors, only visible to author
|
||||
- `published` - Complete, passes validation, visible per sharing settings
|
||||
|
||||
#### Step Library Table
|
||||
Add `status` column to `step_library` table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE step_library
|
||||
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'published';
|
||||
|
||||
ALTER TABLE step_library
|
||||
ADD CONSTRAINT step_library_status_check
|
||||
CHECK (status IN ('draft', 'published'));
|
||||
|
||||
CREATE INDEX idx_step_library_status ON step_library(status);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Changes
|
||||
|
||||
### Trees Endpoints
|
||||
|
||||
#### GET /api/v1/trees
|
||||
Add query parameter:
|
||||
```python
|
||||
@router.get("/")
|
||||
async def list_trees(
|
||||
include_drafts: bool = False, # NEW
|
||||
category_id: Optional[UUID] = None,
|
||||
tags: Optional[str] = None,
|
||||
# ... existing params
|
||||
):
|
||||
"""
|
||||
List trees.
|
||||
|
||||
By default, only returns published trees.
|
||||
Set include_drafts=true to include user's own draft trees.
|
||||
"""
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
- Default: Only return `status='published'` trees
|
||||
- `include_drafts=true`: Return published trees + current user's drafts
|
||||
- Never show other users' drafts
|
||||
|
||||
#### POST /api/v1/trees
|
||||
```python
|
||||
class TreeCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
tree_structure: dict
|
||||
status: str = "published" # NEW: default to published
|
||||
# ... existing fields
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- `status='draft'`: Skip validation, allow saving with errors
|
||||
- `status='published'`: Run full validation, reject if errors exist
|
||||
|
||||
#### PUT /api/v1/trees/{id}
|
||||
```python
|
||||
class TreeUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
tree_structure: Optional[dict] = None
|
||||
status: Optional[str] = None # NEW: allow status change
|
||||
# ... existing fields
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- Changing `draft` → `published`: Run validation, reject if errors
|
||||
- Changing `published` → `draft`: Allow without validation
|
||||
- Updating draft: Skip validation
|
||||
- Updating published: Run validation
|
||||
|
||||
#### GET /api/v1/trees/{id}/can-publish
|
||||
```python
|
||||
@router.get("/{id}/can-publish")
|
||||
async def can_publish_tree(id: UUID) -> dict:
|
||||
"""
|
||||
Check if a draft tree can be published.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"can_publish": bool,
|
||||
"errors": ValidationError[],
|
||||
"warnings": ValidationError[]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**Use case:** Frontend calls this before showing "Publish" button to preview errors.
|
||||
|
||||
### Step Library Endpoints
|
||||
|
||||
Same pattern as trees:
|
||||
- `GET /api/v1/steps?include_drafts=true`
|
||||
- `POST /api/v1/steps` with `status` field
|
||||
- `PUT /api/v1/steps/{id}` with status change validation
|
||||
- `GET /api/v1/steps/{id}/can-publish`
|
||||
|
||||
---
|
||||
|
||||
## Frontend Changes
|
||||
|
||||
### Tree Library Page
|
||||
|
||||
**Visual Changes:**
|
||||
```tsx
|
||||
// Draft badge on tree cards
|
||||
{tree.status === 'draft' && (
|
||||
<span className="rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
|
||||
// Filter toggle
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeDrafts}
|
||||
onChange={(e) => setIncludeDrafts(e.target.checked)}
|
||||
/>
|
||||
Show my drafts
|
||||
</label>
|
||||
```
|
||||
|
||||
**Default:** Only show published trees
|
||||
**With "Show my drafts" enabled:** Show published + user's drafts
|
||||
|
||||
### Tree Editor Page
|
||||
|
||||
**Save Button Logic:**
|
||||
|
||||
```tsx
|
||||
const { canSave, validationErrors, validationWarnings } = useValidation()
|
||||
const isDraft = tree.status === 'draft'
|
||||
|
||||
// Two-button layout when draft has errors
|
||||
{isDraft && validationErrors.length > 0 ? (
|
||||
<>
|
||||
<button onClick={handleSaveDraft}>
|
||||
Save Draft
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePublish}
|
||||
disabled={validationErrors.length > 0}
|
||||
title={validationErrors.length > 0 ? "Fix errors to publish" : ""}
|
||||
>
|
||||
Publish
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={handleSave}>
|
||||
{isDraft ? 'Save Draft' : 'Save'}
|
||||
</button>
|
||||
)}
|
||||
```
|
||||
|
||||
**Validation Display:**
|
||||
|
||||
```tsx
|
||||
// Show validation summary for drafts
|
||||
{isDraft && (
|
||||
<ValidationSummary
|
||||
errors={validationErrors}
|
||||
warnings={validationWarnings}
|
||||
mode="draft" // Shows "Fix these to publish" message
|
||||
/>
|
||||
)}
|
||||
|
||||
// Show validation summary for published (blocks save)
|
||||
{!isDraft && validationErrors.length > 0 && (
|
||||
<ValidationSummary
|
||||
errors={validationErrors}
|
||||
warnings={validationWarnings}
|
||||
mode="published" // Shows "Cannot save" message
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
**Status Badge in Editor:**
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<h1>{tree.name}</h1>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="rounded bg-yellow-100 px-2 py-1 text-sm font-medium text-yellow-800">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Tree Navigation Page
|
||||
|
||||
**Draft trees behavior:**
|
||||
- Can be selected and used for navigation
|
||||
- Show warning banner: "⚠️ This is a draft tree and may be incomplete"
|
||||
- Allow session creation (useful for testing draft trees)
|
||||
|
||||
### Step Library Browser
|
||||
|
||||
**Draft custom steps:**
|
||||
```tsx
|
||||
// In CustomStepModal, add checkbox:
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={saveAsDraft}
|
||||
onChange={(e) => setSaveAsDraft(e.target.checked)}
|
||||
/>
|
||||
Save as draft (you can refine it later)
|
||||
</label>
|
||||
|
||||
// In StepLibraryBrowser, filter control:
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDrafts}
|
||||
onChange={(e) => setShowDrafts(e.target.checked)}
|
||||
/>
|
||||
Show my draft steps
|
||||
</label>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Flows
|
||||
|
||||
### Flow 1: Save Draft Tree
|
||||
|
||||
1. User creates new tree, clicks "Create Tree"
|
||||
2. Tree Editor opens, user adds nodes
|
||||
3. User clicks "Save Draft" (or just "Save" if creating as draft from start)
|
||||
4. Validation runs but doesn't block—tree saved with `status='draft'`
|
||||
5. Success message: "Draft saved. Publish when ready."
|
||||
|
||||
### Flow 2: Publish Draft Tree
|
||||
|
||||
1. User opens draft tree in editor
|
||||
2. ValidationSummary shows errors/warnings
|
||||
3. User fixes all errors
|
||||
4. "Publish" button becomes enabled
|
||||
5. User clicks "Publish"
|
||||
6. Tree status changes to `published`
|
||||
7. Success message: "Tree published and available to team"
|
||||
|
||||
### Flow 3: Unpublish Tree
|
||||
|
||||
1. User opens published tree
|
||||
2. Clicks "Convert to Draft" (in dropdown menu)
|
||||
3. Confirmation modal: "This will hide the tree from others. Continue?"
|
||||
4. Tree status changes to `draft`
|
||||
5. Tree removed from other users' tree library view
|
||||
|
||||
### Flow 4: Save Draft Custom Step
|
||||
|
||||
1. User adds custom step during navigation
|
||||
2. In CustomStepModal, checks "Save as draft"
|
||||
3. Step saved to personal library with `status='draft'`
|
||||
4. Step inserted into current session (works like published step)
|
||||
5. Later, user opens "My Steps" page, refines draft, publishes
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Draft Trees
|
||||
- ✅ Can save with missing required fields
|
||||
- ✅ Can save with orphan nodes
|
||||
- ✅ Can save with circular references
|
||||
- ✅ Can save without solution nodes
|
||||
- ❌ Still validate JSONB structure (prevent corrupted data)
|
||||
|
||||
### Published Trees
|
||||
- ❌ Cannot save with any validation errors
|
||||
- ⚠️ Can save with warnings (orphan nodes, etc.)
|
||||
- ✅ Must have at least one solution node
|
||||
- ✅ Must have valid tree_structure
|
||||
|
||||
### Publishing Transition
|
||||
- When `draft` → `published`: Run full validation, reject if errors
|
||||
- Show clear error message: "Cannot publish: 3 errors found. [View Details]"
|
||||
|
||||
---
|
||||
|
||||
## UI Mockup Descriptions
|
||||
|
||||
### Tree Library Page
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Tree Library │
|
||||
│ │
|
||||
│ [Search...] [Category ▼] [+ New Tree] │
|
||||
│ │
|
||||
│ ☑ Show my drafts │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Citrix Connection Issues [DRAFT]│ │
|
||||
│ │ Last edited: 2 hours ago │ │
|
||||
│ │ 5 nodes · 2 errors │ │
|
||||
│ │ [Continue Editing] │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Outlook Won't Start │ │
|
||||
│ │ Last used: Yesterday │ │
|
||||
│ │ 12 nodes · Published │ │
|
||||
│ │ [Start Session] │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tree Editor - Draft Mode
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Citrix Connection Issues [DRAFT] │
|
||||
│ │
|
||||
│ ⚠ Validation (2 errors, 1 warning) │
|
||||
│ ├─ ❌ Tree must have at least one solution node│
|
||||
│ ├─ ❌ Node "Check firewall" is orphaned │
|
||||
│ └─ ⚠ Node "Reboot" has no help text │
|
||||
│ │
|
||||
│ [Node editing area...] │
|
||||
│ │
|
||||
│ [Cancel] [Save Draft] [Publish] ← disabled │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tree Editor - Ready to Publish
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Citrix Connection Issues [DRAFT] │
|
||||
│ │
|
||||
│ ✅ No validation errors │
|
||||
│ ⚠ 1 warning: Node "Reboot" has no help text │
|
||||
│ │
|
||||
│ [Node editing area...] │
|
||||
│ │
|
||||
│ [Cancel] [Save Draft] [Publish] ← enabled │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Backend Foundation
|
||||
- [ ] Add `status` column to `trees` table
|
||||
- [ ] Update Trees API endpoints (list, create, update)
|
||||
- [ ] Add `can_publish` endpoint
|
||||
- [ ] Update validation logic to respect status
|
||||
- [ ] Write tests for draft/publish transitions
|
||||
|
||||
### Phase 2: Frontend - Trees
|
||||
- [ ] Update Tree Library to filter by status
|
||||
- [ ] Add "Show my drafts" toggle
|
||||
- [ ] Update Tree Editor save button logic
|
||||
- [ ] Add "Publish" button for drafts
|
||||
- [ ] Add status badge to tree cards and editor
|
||||
- [ ] Add confirmation modal for unpublishing
|
||||
|
||||
### Phase 3: Backend - Step Library
|
||||
- [ ] Add `status` column to `step_library` table
|
||||
- [ ] Update Step Library API endpoints
|
||||
- [ ] Add `can_publish` endpoint for steps
|
||||
- [ ] Write tests
|
||||
|
||||
### Phase 4: Frontend - Step Library
|
||||
- [ ] Update CustomStepModal with draft option
|
||||
- [ ] Update StepLibraryBrowser to filter drafts
|
||||
- [ ] Add "Publish" action to step detail modal
|
||||
- [ ] Add status badge to step cards
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Trees
|
||||
- [ ] Create draft tree with validation errors → saves successfully
|
||||
- [ ] Try to publish draft with errors → rejected with clear message
|
||||
- [ ] Fix errors, publish draft → becomes published
|
||||
- [ ] Edit published tree, introduce error → cannot save
|
||||
- [ ] Convert published tree to draft → hidden from others
|
||||
- [ ] Other users cannot see my draft trees
|
||||
- [ ] Draft trees show in "My Trees" when filter enabled
|
||||
|
||||
### Step Library
|
||||
- [ ] Save custom step as draft → appears in "My Steps" with badge
|
||||
- [ ] Draft steps not shown in team/public views
|
||||
- [ ] Publish draft step → validation runs
|
||||
- [ ] Draft step can be inserted into session (works like published)
|
||||
- [ ] Edit draft step, publish when ready
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Create draft → close browser → reopen → draft still there
|
||||
- [ ] Two users editing same tree: User A drafts, User B can't see draft
|
||||
- [ ] Published tree with 100 uses → convert to draft → sessions still work
|
||||
- [ ] Delete draft tree → no orphaned sessions
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Auto-save for drafts?**
|
||||
- Should drafts auto-save every N seconds like Google Docs?
|
||||
- Recommendation: Phase 5 enhancement, manual save for now
|
||||
|
||||
2. **Draft expiration?**
|
||||
- Should drafts older than 30 days be auto-deleted?
|
||||
- Recommendation: No expiration for now, add later if storage becomes issue
|
||||
|
||||
3. **Version history for drafts?**
|
||||
- Should we track versions of draft edits?
|
||||
- Recommendation: Out of scope, add with general version control feature later
|
||||
|
||||
4. **Team drafts?**
|
||||
- Should teams be able to collaborate on draft trees?
|
||||
- Recommendation: Phase 6 - "shared drafts" with permissions
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Database Migration
|
||||
```python
|
||||
# Migration: add_tree_status_column
|
||||
def upgrade():
|
||||
# Add column with default 'published' for existing trees
|
||||
op.add_column('trees', sa.Column('status', sa.String(20), nullable=False, server_default='published'))
|
||||
op.create_check_constraint('trees_status_check', 'trees', "status IN ('draft', 'published')")
|
||||
op.create_index('idx_trees_status', 'trees', ['status'])
|
||||
|
||||
# Same for step_library
|
||||
op.add_column('step_library', sa.Column('status', sa.String(20), nullable=False, server_default='published'))
|
||||
op.create_check_constraint('step_library_status_check', 'step_library', "status IN ('draft', 'published')")
|
||||
op.create_index('idx_step_library_status', 'step_library', ['status'])
|
||||
```
|
||||
|
||||
**Rollback safety:** All existing trees default to `published`, no data loss.
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- **Adoption:** % of users who create at least one draft tree per month
|
||||
- **Completion:** % of drafts that get published (vs abandoned)
|
||||
- **Time savings:** Avg time to create complex trees (before/after draft feature)
|
||||
- **Error reduction:** % reduction in "cannot save" frustration incidents
|
||||
|
||||
**Target:** 60% of users with 5+ trees use draft feature within 2 months of launch.
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **Tree Editor Validation** (Issue #1) - Prerequisite
|
||||
- **Step Library Browser** (Issue #10) - Will benefit from draft steps
|
||||
- **Tree Forking** (Issue #13) - Forked trees could start as drafts
|
||||
- **Tree Sharing** (Issue #16) - Published status required to share
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Draft feature inspired by Gmail drafts, Google Docs, Notion page publishing
|
||||
- Key principle: **Never lose work** - always allow saving, validate on publish
|
||||
- This feature enables iterative tree building, which is critical for complex MSP workflows
|
||||
Reference in New Issue
Block a user