mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
feat: add three viewing modes for app specification (#566)
* feat: add three viewing modes for app specification Introduces View, Edit, and Source modes for the spec page: - View: Clean read-only display with cards, badges, and accordions - Edit: Structured form-based editor for all spec fields - Source: Raw XML editor for advanced users Also adds @automaker/spec-parser shared package for XML parsing between server and client. * fix: address PR review feedback - Replace array index keys with stable UUIDs in array-field-editor, features-section, and roadmap-section components - Replace regex-based XML parsing with fast-xml-parser for robustness - Simplify renderContent logic in spec-view by removing dead code paths * fix: convert git+ssh URLs to https in package-lock.json * fix: address PR review feedback for spec visualiser - Remove unused RefreshCw import from spec-view.tsx - Add explicit parsedSpec check in renderContent for robustness - Hide save button in view mode since it's read-only - Remove GripVertical drag handles since drag-and-drop is not implemented - Rename Map imports to MapIcon to avoid shadowing global Map - Escape tagName in xml-utils.ts RegExp functions for safety - Add aria-label attributes for accessibility on mode tabs * fix: address additional PR review feedback - Fix Textarea controlled/uncontrolled warning with default value - Preserve IDs in useEffect sync to avoid unnecessary remounts - Consolidate lucide-react imports - Add JSDoc note about tag attributes limitation in xml-utils.ts - Remove redundant disabled prop from SpecModeTabs
This commit is contained in:
committed by
GitHub
parent
af95dae73a
commit
c4652190eb
@@ -1,19 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
// Extracted hooks
|
||||
import { useSpecLoading, useSpecSave, useSpecGeneration } from './spec-view/hooks';
|
||||
import { useSpecLoading, useSpecSave, useSpecGeneration, useSpecParser } from './spec-view/hooks';
|
||||
|
||||
// Extracted components
|
||||
import { SpecHeader, SpecEditor, SpecEmptyState } from './spec-view/components';
|
||||
import {
|
||||
SpecHeader,
|
||||
SpecEditor,
|
||||
SpecEmptyState,
|
||||
SpecViewMode,
|
||||
SpecEditMode,
|
||||
SpecModeTabs,
|
||||
} from './spec-view/components';
|
||||
|
||||
// Extracted dialogs
|
||||
import { CreateSpecDialog, RegenerateSpecDialog } from './spec-view/dialogs';
|
||||
|
||||
// Types
|
||||
import type { SpecViewMode as SpecViewModeType } from './spec-view/types';
|
||||
|
||||
export function SpecView() {
|
||||
const { currentProject, appSpec } = useAppStore();
|
||||
|
||||
// View mode state - default to 'view'
|
||||
const [mode, setMode] = useState<SpecViewModeType>('view');
|
||||
|
||||
// Actions panel state (for tablet/mobile)
|
||||
const [showActionsPanel, setShowActionsPanel] = useState(false);
|
||||
|
||||
@@ -21,7 +34,10 @@ export function SpecView() {
|
||||
const { isLoading, specExists, isGenerationRunning, loadSpec } = useSpecLoading();
|
||||
|
||||
// Save state
|
||||
const { isSaving, hasChanges, saveSpec, handleChange, setHasChanges } = useSpecSave();
|
||||
const { isSaving, hasChanges, saveSpec, handleChange } = useSpecSave();
|
||||
|
||||
// Parse the spec XML
|
||||
const { isValid: isParseValid, parsedSpec, errors: parseErrors } = useSpecParser(appSpec);
|
||||
|
||||
// Generation state and handlers
|
||||
const {
|
||||
@@ -70,8 +86,17 @@ export function SpecView() {
|
||||
handleSync,
|
||||
} = useSpecGeneration({ loadSpec });
|
||||
|
||||
// Reset hasChanges when spec is reloaded
|
||||
// (This is needed because loadSpec updates appSpec in the store)
|
||||
// Handle mode change - if parse is invalid, force source mode
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: SpecViewModeType) => {
|
||||
if ((newMode === 'view' || newMode === 'edit') && !isParseValid) {
|
||||
// Can't switch to view/edit if parse is invalid
|
||||
return;
|
||||
}
|
||||
setMode(newMode);
|
||||
},
|
||||
[isParseValid]
|
||||
);
|
||||
|
||||
// No project selected
|
||||
if (!currentProject) {
|
||||
@@ -126,6 +151,28 @@ export function SpecView() {
|
||||
);
|
||||
}
|
||||
|
||||
// Render content based on mode
|
||||
const renderContent = () => {
|
||||
// If the XML is invalid or spec is not parsed, we can only show the source editor.
|
||||
// The tabs for other modes are disabled, but this is an extra safeguard.
|
||||
if (!isParseValid || !parsedSpec) {
|
||||
return <SpecEditor value={appSpec} onChange={handleChange} />;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case 'view':
|
||||
return <SpecViewMode spec={parsedSpec} />;
|
||||
case 'edit':
|
||||
return <SpecEditMode spec={parsedSpec} onChange={handleChange} />;
|
||||
case 'source':
|
||||
default:
|
||||
return <SpecEditor value={appSpec} onChange={handleChange} />;
|
||||
}
|
||||
};
|
||||
|
||||
const isProcessing =
|
||||
isRegenerating || isGenerationRunning || isCreating || isGeneratingFeatures || isSyncing;
|
||||
|
||||
// Main view - spec exists
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view">
|
||||
@@ -145,9 +192,33 @@ export function SpecView() {
|
||||
onSaveClick={saveSpec}
|
||||
showActionsPanel={showActionsPanel}
|
||||
onToggleActionsPanel={() => setShowActionsPanel(!showActionsPanel)}
|
||||
showSaveButton={mode !== 'view'}
|
||||
/>
|
||||
|
||||
<SpecEditor value={appSpec} onChange={handleChange} />
|
||||
{/* Mode tabs and content container */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Mode tabs bar - inside the content area, centered */}
|
||||
{!isProcessing && (
|
||||
<div className="flex items-center justify-center px-4 py-2 border-b border-border bg-muted/30 relative">
|
||||
<SpecModeTabs mode={mode} onModeChange={handleModeChange} isParseValid={isParseValid} />
|
||||
{/* Show parse error indicator - positioned to the right */}
|
||||
{!isParseValid && parseErrors.length > 0 && (
|
||||
<span className="absolute right-4 text-xs text-destructive">
|
||||
XML has errors - fix in Source mode
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show parse error banner if in source mode with errors */}
|
||||
{!isParseValid && parseErrors.length > 0 && mode === 'source' && (
|
||||
<div className="px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-sm text-destructive">
|
||||
<span className="font-medium">XML Parse Errors:</span> {parseErrors.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
<RegenerateSpecDialog
|
||||
open={showRegenerateDialog}
|
||||
|
||||
Reference in New Issue
Block a user