Refactor site building workflow and context handling; update API response structure for improved clarity and consistency. Adjust frontend components to align with new data structure, including error handling and loading states.
This commit is contained in:
@@ -138,11 +138,9 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips"));
|
||||
const Videos = lazy(() => import("./pages/Settings/UiElements/Videos"));
|
||||
|
||||
export default function App() {
|
||||
const { isAuthenticated, refreshUser, logout } = useAuthStore((state) => ({
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
refreshUser: state.refreshUser,
|
||||
logout: state.logout,
|
||||
}));
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
|
||||
@@ -119,6 +119,7 @@ export {
|
||||
// Aliases for commonly used icon names
|
||||
export { AngleLeftIcon as ArrowLeftIcon };
|
||||
export { FileIcon as FileTextIcon };
|
||||
export { FileIcon as ImageIcon }; // Use FileIcon as ImageIcon alias
|
||||
export { TimeIcon as ClockIcon };
|
||||
export { ErrorIcon as XCircleIcon };
|
||||
export { BoxIcon as TagIcon };
|
||||
|
||||
@@ -111,15 +111,14 @@ export default function IndustriesSectorsKeywords() {
|
||||
} catch (error: any) {
|
||||
if (error instanceof AccountSettingsError) {
|
||||
if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') {
|
||||
console.debug('No saved user preferences yet.');
|
||||
// Preferences don't exist yet - this is expected for new users
|
||||
return;
|
||||
}
|
||||
console.warn('Failed to load user preferences:', error);
|
||||
toast.error(getAccountSettingsPreferenceMessage(error));
|
||||
// For other errors (500, etc.), silently handle - user can still use the page
|
||||
// Don't show error toast for server errors - graceful degradation
|
||||
return;
|
||||
}
|
||||
console.warn('Failed to load user preferences:', error);
|
||||
toast.error('Unable to load your saved preferences right now.');
|
||||
// For non-AccountSettingsError errors, silently handle - graceful degradation
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,17 @@ import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Card } from "../../../components/ui/card";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
import { useToast } from "../../../components/ui/toast/ToastContainer";
|
||||
import { FileText, Loader2, Plus, Trash2, CheckSquare, Square, Rocket } from "lucide-react";
|
||||
import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
|
||||
import {
|
||||
TableIcon,
|
||||
PlusIcon,
|
||||
FileIcon,
|
||||
TrashBinIcon,
|
||||
CheckLineIcon,
|
||||
BoxIcon,
|
||||
PaperPlaneIcon,
|
||||
BoltIcon
|
||||
} from "../../../icons";
|
||||
import { useSiteStore } from "../../../store/siteStore";
|
||||
import { useBuilderStore } from "../../../store/builderStore";
|
||||
import { siteBuilderApi } from "../../../services/siteBuilder.api";
|
||||
@@ -135,9 +145,20 @@ export default function SiteBuilderBlueprints() {
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation tabs for Sites module
|
||||
const sitesTabs = [
|
||||
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
|
||||
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
|
||||
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<PageMeta title="Blueprints - IGNY8" />
|
||||
<PageMeta title="Blueprints - IGNY8" description="View and manage site blueprints" />
|
||||
|
||||
{/* In-page navigation tabs */}
|
||||
<ModuleNavigationTabs tabs={sitesTabs} />
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||
@@ -156,7 +177,7 @@ export default function SiteBuilderBlueprints() {
|
||||
onClick={handleBulkDeleteClick}
|
||||
variant="solid"
|
||||
tone="danger"
|
||||
startIcon={<Trash2 className="h-4 w-4" />}
|
||||
startIcon={<TrashBinIcon className="h-4 w-4" />}
|
||||
>
|
||||
Delete {selectedIds.size} selected
|
||||
</Button>
|
||||
@@ -165,7 +186,7 @@ export default function SiteBuilderBlueprints() {
|
||||
onClick={() => navigate("/sites/builder")}
|
||||
variant="solid"
|
||||
tone="brand"
|
||||
startIcon={<Plus className="h-4 w-4" />}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
>
|
||||
Create blueprint
|
||||
</Button>
|
||||
@@ -180,12 +201,12 @@ export default function SiteBuilderBlueprints() {
|
||||
</Card>
|
||||
) : loading ? (
|
||||
<div className="flex h-64 items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
<div className="mr-2 h-5 w-5 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Loading blueprints…
|
||||
</div>
|
||||
) : blueprints.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||
<FileIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||||
No blueprints created yet for {activeSite.name}.
|
||||
</p>
|
||||
@@ -202,9 +223,9 @@ export default function SiteBuilderBlueprints() {
|
||||
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
{selectedIds.size === blueprints.length ? (
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
<CheckLineIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Square className="h-4 w-4" />
|
||||
<BoxIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{selectedIds.size === blueprints.length
|
||||
@@ -244,9 +265,9 @@ export default function SiteBuilderBlueprints() {
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
{selectedIds.has(blueprint.id) ? (
|
||||
<CheckSquare className="h-5 w-5 text-brand-600 dark:text-brand-400" />
|
||||
<CheckLineIcon className="h-5 w-5 text-brand-600 dark:text-brand-400" />
|
||||
) : (
|
||||
<Square className="h-5 w-5 text-gray-400" />
|
||||
<BoxIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -260,13 +281,21 @@ export default function SiteBuilderBlueprints() {
|
||||
<span className="capitalize">{blueprint.status}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="solid"
|
||||
tone="brand"
|
||||
onClick={() => navigate(`/sites/builder/workflow/${blueprint.id}`)}
|
||||
startIcon={<BoltIcon className="h-4 w-4" />}
|
||||
>
|
||||
Continue Workflow
|
||||
</Button>
|
||||
{(blueprint.status === 'ready' || blueprint.status === 'deployed') && (
|
||||
<Button
|
||||
variant="solid"
|
||||
tone="brand"
|
||||
disabled={deployingId === blueprint.id}
|
||||
onClick={() => handleDeploy(blueprint)}
|
||||
startIcon={deployingId === blueprint.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />}
|
||||
startIcon={deployingId === blueprint.id ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
|
||||
>
|
||||
{deployingId === blueprint.id ? "Deploying..." : blueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
|
||||
</Button>
|
||||
@@ -279,7 +308,7 @@ export default function SiteBuilderBlueprints() {
|
||||
>
|
||||
{isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Loading…
|
||||
</>
|
||||
) : (
|
||||
@@ -299,7 +328,7 @@ export default function SiteBuilderBlueprints() {
|
||||
variant="ghost"
|
||||
tone="danger"
|
||||
onClick={() => handleDeleteClick(blueprint)}
|
||||
startIcon={<Trash2 className="h-4 w-4" />}
|
||||
startIcon={<TrashBinIcon className="h-4 w-4" />}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Alert from "../../../components/ui/alert/Alert";
|
||||
import { useBuilderStore } from "../../../store/builderStore";
|
||||
import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore";
|
||||
import ProgressModal from "../../../components/common/ProgressModal";
|
||||
import { Eye, Play, Loader2, Rocket } from "lucide-react";
|
||||
import { EyeIcon, PaperPlaneIcon } from "../../../icons";
|
||||
import { useToast } from "../../../components/ui/toast/ToastContainer";
|
||||
import { siteBuilderApi } from "../../../services/siteBuilder.api";
|
||||
|
||||
@@ -76,9 +76,9 @@ export default function SiteBuilderPreview() {
|
||||
if (!activeBlueprint) {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<PageMeta title="Site Preview - IGNY8" />
|
||||
<PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
|
||||
<Card className="p-12 text-center">
|
||||
<Eye className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||
<EyeIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Run the Site Builder wizard or open a blueprint to preview it.
|
||||
</p>
|
||||
@@ -97,7 +97,7 @@ export default function SiteBuilderPreview() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<PageMeta title="Site Preview - IGNY8" />
|
||||
<PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||
@@ -117,7 +117,7 @@ export default function SiteBuilderPreview() {
|
||||
tone="brand"
|
||||
onClick={handleGenerateAll}
|
||||
disabled={isGenerating}
|
||||
startIcon={isGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
||||
startIcon={isGenerating ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
|
||||
>
|
||||
{isGenerating ? "Generating..." : "Generate All Pages"}
|
||||
</Button>
|
||||
@@ -128,7 +128,7 @@ export default function SiteBuilderPreview() {
|
||||
tone="brand"
|
||||
onClick={handleDeploy}
|
||||
disabled={isDeploying}
|
||||
startIcon={isDeploying ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />}
|
||||
startIcon={isDeploying ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
|
||||
>
|
||||
{isDeploying ? "Deploying..." : activeBlueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
|
||||
</Button>
|
||||
|
||||
@@ -10,11 +10,15 @@ import PageMeta from "../../../components/common/PageMeta";
|
||||
import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector";
|
||||
import PageHeader from "../../../components/common/PageHeader";
|
||||
import Alert from "../../../components/ui/alert/Alert";
|
||||
import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
|
||||
import {
|
||||
GridIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
BoltIcon,
|
||||
TableIcon,
|
||||
PlusIcon,
|
||||
FileIcon,
|
||||
} from "../../../icons";
|
||||
import { useSiteStore } from "../../../store/siteStore";
|
||||
import { useSectorStore } from "../../../store/sectorStore";
|
||||
@@ -54,6 +58,7 @@ export default function SiteBuilderWizard() {
|
||||
metadataError,
|
||||
isMetadataLoading,
|
||||
loadMetadata,
|
||||
loadBlueprint,
|
||||
} = useBuilderStore();
|
||||
|
||||
// Progress modal for AI functions
|
||||
@@ -304,9 +309,20 @@ export default function SiteBuilderWizard() {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Navigation tabs for Sites module
|
||||
const sitesTabs = [
|
||||
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
|
||||
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
|
||||
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<PageMeta title="Create Site - IGNY8" />
|
||||
|
||||
{/* In-page navigation tabs */}
|
||||
<ModuleNavigationTabs tabs={sitesTabs} />
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<PageHeader
|
||||
title="Site Builder"
|
||||
|
||||
@@ -16,9 +16,13 @@ import IdeasHandoffStep from './steps/IdeasHandoffStep';
|
||||
import Alert from '../../../components/ui/alert/Alert';
|
||||
import PageMeta from '../../../components/common/PageMeta';
|
||||
import Button from '../../../components/ui/button/Button';
|
||||
import { Loader2, HelpCircle } from 'lucide-react';
|
||||
import { InfoIcon } from '../../../icons';
|
||||
|
||||
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType> = {
|
||||
interface StepComponentProps {
|
||||
blueprintId: number;
|
||||
}
|
||||
|
||||
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType<StepComponentProps>> = {
|
||||
business_details: BusinessDetailsStep,
|
||||
clusters: ClusterAssignmentStep,
|
||||
taxonomies: TaxonomyBuilderStep,
|
||||
@@ -124,7 +128,7 @@ export default function WorkflowWizard() {
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Alert variant="error">Invalid blueprint ID</Alert>
|
||||
<Alert variant="error" title="Error" message="Invalid blueprint ID" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -132,7 +136,7 @@ export default function WorkflowWizard() {
|
||||
if (loading && !context) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,7 +144,7 @@ export default function WorkflowWizard() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Alert variant="error">{error}</Alert>
|
||||
<Alert variant="error" title="Error" message={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -149,7 +153,10 @@ export default function WorkflowWizard() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<PageMeta title={`Site Builder - ${STEP_LABELS[currentStep]}`} />
|
||||
<PageMeta
|
||||
title={`Site Builder - ${STEP_LABELS[currentStep]}`}
|
||||
description={`Site Builder Workflow: ${STEP_LABELS[currentStep]}`}
|
||||
/>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header with Help Button */}
|
||||
@@ -166,9 +173,8 @@ export default function WorkflowWizard() {
|
||||
onClick={() => setHelperDrawerOpen(!helperDrawerOpen)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Press F1 or ? for help"
|
||||
startIcon={<InfoIcon className="h-4 w-4" />}
|
||||
>
|
||||
<HelpCircle className="h-4 w-4 mr-2" />
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Contextual help for each wizard step
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { X, HelpCircle, ChevronRight } from 'lucide-react';
|
||||
import { WizardStep } from '../../../../store/builderWorkflowStore';
|
||||
import Button from '../../../../components/ui/button/Button';
|
||||
import { CloseIcon, InfoIcon, ArrowRightIcon } from '../../../../icons';
|
||||
|
||||
interface HelperDrawerProps {
|
||||
currentStep: WizardStep;
|
||||
@@ -95,7 +95,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<InfoIcon className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<h2 id="helper-drawer-title" className="text-lg font-semibold">
|
||||
Help & Tips
|
||||
</h2>
|
||||
@@ -107,7 +107,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
|
||||
className="p-1"
|
||||
aria-label="Close help drawer"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<CloseIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
|
||||
<ul className="space-y-3">
|
||||
{help.content.map((item, index) => (
|
||||
<li key={index} className="flex items-start gap-3">
|
||||
<ChevronRight className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" />
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Shows breadcrumb with step completion status
|
||||
*/
|
||||
import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore';
|
||||
import { CheckCircle2, Circle } from 'lucide-react';
|
||||
import { CheckCircleIcon } from '../../../../icons';
|
||||
|
||||
const STEPS: Array<{ key: WizardStep; label: string }> = [
|
||||
{ key: 'business_details', label: 'Business Details' },
|
||||
@@ -19,9 +19,23 @@ interface WizardProgressProps {
|
||||
}
|
||||
|
||||
export default function WizardProgress({ currentStep }: WizardProgressProps) {
|
||||
const { completedSteps, blockingIssues } = useBuilderWorkflowStore();
|
||||
const { completedSteps, blockingIssues, goToStep } = useBuilderWorkflowStore();
|
||||
const currentIndex = STEPS.findIndex(s => s.key === currentStep);
|
||||
|
||||
const handleStepClick = (step: WizardStep, index: number) => {
|
||||
// Allow navigation to:
|
||||
// 1. Current step
|
||||
// 2. Completed steps
|
||||
// 3. Next step if current step is completed
|
||||
const isCompleted = completedSteps.has(step.key);
|
||||
const isCurrent = step.key === currentStep;
|
||||
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
|
||||
|
||||
if (canNavigate && !blockingIssues.some(issue => issue.step === step.key)) {
|
||||
goToStep(step);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<nav aria-label="Progress">
|
||||
@@ -31,34 +45,39 @@ export default function WizardProgress({ currentStep }: WizardProgressProps) {
|
||||
const isCurrent = step.key === currentStep;
|
||||
const isBlocked = blockingIssues.some(issue => issue.step === step.key);
|
||||
const isAccessible = index <= currentIndex || isCompleted;
|
||||
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
|
||||
|
||||
return (
|
||||
<li key={step.key} className="flex-1 flex items-center">
|
||||
<div className="flex items-center w-full">
|
||||
{/* Step Circle */}
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStepClick(step.key, index)}
|
||||
disabled={!canNavigate || isBlocked}
|
||||
className={`
|
||||
flex items-center justify-center w-10 h-10 rounded-full border-2
|
||||
flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all
|
||||
${
|
||||
isCompleted
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
? 'bg-green-500 border-green-500 text-white cursor-pointer hover:bg-green-600'
|
||||
: isCurrent
|
||||
? 'bg-primary border-primary text-white'
|
||||
? 'bg-primary border-primary text-white cursor-default'
|
||||
: isBlocked
|
||||
? 'bg-red-100 border-red-500 text-red-500'
|
||||
: isAccessible
|
||||
? 'bg-gray-100 border-gray-300 text-gray-600'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-400'
|
||||
? 'bg-red-100 border-red-500 text-red-500 cursor-not-allowed'
|
||||
: canNavigate
|
||||
? 'bg-gray-100 border-gray-300 text-gray-600 cursor-pointer hover:bg-gray-200 hover:border-gray-400'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
title={isBlocked ? 'This step is blocked' : canNavigate ? `Go to ${step.label}` : 'Complete previous steps first'}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckCircle2 className="w-6 h-6" />
|
||||
<CheckCircleIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<span className="text-sm font-semibold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
className={`
|
||||
mt-2 text-xs font-medium text-center
|
||||
|
||||
@@ -56,7 +56,7 @@ export function BusinessDetailsStep(props: BusinessDetailsStepProps) {
|
||||
|
||||
// Stage 2 Workflow Component
|
||||
function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
|
||||
const { context, completeStep, loading } = useBuilderWorkflowStore();
|
||||
const { context, completeStep, loading, goToStep } = useBuilderWorkflowStore();
|
||||
const [blueprint, setBlueprint] = useState<SiteBlueprint | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
@@ -100,13 +100,52 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
|
||||
});
|
||||
setBlueprint(updated);
|
||||
|
||||
// Mark step as complete
|
||||
await completeStep('business_details', {
|
||||
blueprint_name: formData.name,
|
||||
hosting_type: formData.hosting_type,
|
||||
});
|
||||
// Mark step as complete - catch and handle workflow errors separately
|
||||
try {
|
||||
await completeStep('business_details', {
|
||||
blueprint_name: formData.name,
|
||||
hosting_type: formData.hosting_type,
|
||||
});
|
||||
// If successful, automatically advance to next step
|
||||
const { goToStep } = useBuilderWorkflowStore.getState();
|
||||
goToStep('clusters');
|
||||
} catch (workflowErr: any) {
|
||||
// If workflow update fails but blueprint was saved, mark step as complete locally and advance
|
||||
const workflowErrorMsg = workflowErr?.response?.error ||
|
||||
workflowErr?.response?.message ||
|
||||
workflowErr?.message ||
|
||||
'Workflow step update failed';
|
||||
|
||||
// Check if it's a server error (500) - might be workflow service not enabled
|
||||
const isServerError = workflowErr?.status === 500;
|
||||
const errorDetail = workflowErr?.response?.error || workflowErr?.response?.message || '';
|
||||
|
||||
if (isServerError && errorDetail.includes('Workflow service not enabled')) {
|
||||
// Workflow service is disabled - just advance without marking as complete
|
||||
console.warn('Workflow service not enabled, advancing to next step');
|
||||
const { goToStep } = useBuilderWorkflowStore.getState();
|
||||
goToStep('clusters');
|
||||
// Don't show error - workflow is optional
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('Workflow step update failed:', workflowErrorMsg, workflowErr);
|
||||
|
||||
// Mark step as completed locally so user can proceed
|
||||
const { completedSteps, goToStep } = useBuilderWorkflowStore.getState();
|
||||
const updatedCompletedSteps = new Set(completedSteps);
|
||||
updatedCompletedSteps.add('business_details');
|
||||
useBuilderWorkflowStore.setState({
|
||||
completedSteps: updatedCompletedSteps,
|
||||
currentStep: 'clusters' // Advance to step 2
|
||||
});
|
||||
|
||||
// Show a non-blocking warning to the user
|
||||
setError(`Blueprint saved successfully, but workflow step update failed: ${workflowErrorMsg}. You can continue to the next step.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to save business details');
|
||||
const errorMessage = err?.response?.error || err?.message || 'Failed to save business details';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -124,9 +163,7 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="error" className="mt-4">
|
||||
{error}
|
||||
</Alert>
|
||||
<Alert variant="error" title="Error" message={error} />
|
||||
)}
|
||||
|
||||
<div className="mt-6 space-y-4">
|
||||
@@ -307,6 +344,7 @@ function BusinessDetailsStepStage1({
|
||||
onChange={(e) => onChange('industry', e.target.value)}
|
||||
placeholder={userPreferences?.selectedIndustry || "Supply chain automation"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-4 shadow-theme-sm dark:border-white/5 dark:bg-white/[0.02]">
|
||||
<label className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Hosting preference
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../../../../components/ui/table';
|
||||
import { useToast } from '../../../../components/ui/toast/ToastContainer';
|
||||
import { useSectorStore } from '../../../../store/sectorStore';
|
||||
import { Loader2, CheckCircle2Icon, XCircleIcon } from 'lucide-react';
|
||||
import { CheckCircleIcon, XCircleIcon } from '../../../../icons';
|
||||
|
||||
interface ClusterAssignmentStepProps {
|
||||
blueprintId: number;
|
||||
@@ -218,16 +218,16 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Clusters</div>
|
||||
<div className="text-2xl font-bold">{context.cluster_summary.total}</div>
|
||||
<div className="text-2xl font-bold">{context.cluster_summary.clusters?.length || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Attached</div>
|
||||
<div className="text-2xl font-bold">{context.cluster_summary.attached}</div>
|
||||
<div className="text-2xl font-bold">{context.cluster_summary.attached_count || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Coverage</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{context.cluster_summary.coverage_stats.complete} / {context.cluster_summary.total}
|
||||
{context.cluster_summary.coverage_counts?.complete || 0} / {context.cluster_summary.clusters?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
>
|
||||
{attaching ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Attaching...
|
||||
</>
|
||||
) : (
|
||||
@@ -305,7 +305,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
>
|
||||
{detaching ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Detaching...
|
||||
</>
|
||||
) : (
|
||||
@@ -319,7 +319,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
{/* Cluster Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
</div>
|
||||
) : filteredClusters.length === 0 ? (
|
||||
<Alert variant="info" className="mt-4">
|
||||
@@ -371,7 +371,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{cluster.keywords_count || 0}</TableCell>
|
||||
<TableCell>{cluster.keyword_count || 0}</TableCell>
|
||||
<TableCell>{cluster.volume?.toLocaleString() || 0}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
@@ -385,7 +385,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
<TableCell>
|
||||
{isAttached ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2Icon className="h-4 w-4" />
|
||||
<CheckCircleIcon className="h-4 w-4" />
|
||||
<span className="text-sm">Attached</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -433,7 +433,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
|
||||
>
|
||||
{workflowLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -15,20 +15,22 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
|
||||
const { context, completeStep, blockingIssues } = useBuilderWorkflowStore();
|
||||
const coverageBlocking = blockingIssues.find(issue => issue.step === 'coverage');
|
||||
|
||||
const clusterStats = context?.cluster_summary?.coverage_stats || {
|
||||
complete: 0,
|
||||
in_progress: 0,
|
||||
pending: 0,
|
||||
const clusterStats = {
|
||||
complete: context?.cluster_summary?.coverage_counts?.complete || 0,
|
||||
in_progress: context?.cluster_summary?.coverage_counts?.in_progress || 0,
|
||||
pending: context?.cluster_summary?.coverage_counts?.pending || 0,
|
||||
};
|
||||
const totalClusters = context?.cluster_summary?.total || 0;
|
||||
const attachedClusters = context?.cluster_summary?.attached || 0;
|
||||
const totalClusters = context?.cluster_summary?.clusters?.length || 0;
|
||||
const attachedClusters = context?.cluster_summary?.attached_count || 0;
|
||||
const clusterCoverage = totalClusters > 0 ? (attachedClusters / totalClusters) * 100 : 0;
|
||||
|
||||
const totalTaxonomies = context?.taxonomy_summary?.total || 0;
|
||||
const taxonomyByType = context?.taxonomy_summary?.by_type || {};
|
||||
const totalTaxonomies = context?.taxonomy_summary?.total_taxonomies || 0;
|
||||
const taxonomyByType = context?.taxonomy_summary?.counts_by_type || {};
|
||||
|
||||
const sitemapCoverage = context?.sitemap_summary?.coverage_percentage || 0;
|
||||
const totalPages = context?.sitemap_summary?.total_pages || 0;
|
||||
// Calculate sitemap coverage from pages_by_status if available
|
||||
const sitemapPages = context?.sitemap_summary?.pages_total || 0;
|
||||
const sitemapCoverage = sitemapPages > 0 ? 100 : 0; // Simplified - backend doesn't provide coverage_percentage
|
||||
const totalPages = sitemapPages;
|
||||
|
||||
const getCoverageStatus = (percentage: number) => {
|
||||
if (percentage >= 90) return { status: 'excellent', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' };
|
||||
@@ -152,9 +154,9 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
|
||||
<span className="font-medium">{totalPages}</span>
|
||||
</div>
|
||||
{context?.sitemap_summary?.by_type && (
|
||||
{context?.sitemap_summary?.pages_by_type && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
|
||||
{Object.entries(context.sitemap_summary.pages_by_type).map(([type, count]) => (
|
||||
<div key={type} className="flex justify-between text-xs">
|
||||
<span className="text-gray-600 dark:text-gray-400 capitalize">{type}:</span>
|
||||
<span className="font-medium">{count as number}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { BuilderFormData } from "../../../../types/siteBuilder";
|
||||
import { Card } from "../../../../components/ui/card/Card";
|
||||
import { Card } from "../../../../components/ui/card";
|
||||
import Button from "../../../../components/ui/button/Button";
|
||||
|
||||
const inputClass =
|
||||
|
||||
@@ -135,16 +135,22 @@ export default function SitemapReviewStep({ blueprintId }: SitemapReviewStepProp
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
|
||||
<span className="ml-2 font-semibold">{context.sitemap_summary.total_pages}</span>
|
||||
<span className="ml-2 font-semibold">{context.sitemap_summary.pages_total || 0}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">Coverage:</span>
|
||||
<span className="ml-2 font-semibold">{context.sitemap_summary.coverage_percentage}%</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">By Status:</span>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{Object.entries(context.sitemap_summary.pages_by_status || {}).map(([status, count]) => (
|
||||
<span key={status} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
|
||||
{status}: {count}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">By Type:</span>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
|
||||
{Object.entries(context.sitemap_summary.pages_by_type || {}).map(([type, count]) => (
|
||||
<span key={type} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
|
||||
{type}: {count}
|
||||
</span>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from "../../../../types/siteBuilder";
|
||||
import { Card } from "../../../../components/ui/card";
|
||||
import { Dropdown } from "../../../../components/ui/dropdown/Dropdown";
|
||||
import { Check } from "lucide-react";
|
||||
import { CheckLineIcon } from "../../../../icons";
|
||||
|
||||
const labelClass =
|
||||
"text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
|
||||
@@ -197,7 +197,7 @@ export function StyleStep({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{isSelected && <Check className="h-4 w-4" />}
|
||||
{isSelected && <CheckLineIcon className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
@@ -312,7 +312,7 @@ export function StyleStep({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{isSelected && <Check className="h-4 w-4" />}
|
||||
{isSelected && <CheckLineIcon className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '../../../../components/ui/table';
|
||||
import { useToast } from '../../../../components/ui/toast/ToastContainer';
|
||||
import { useSectorStore } from '../../../../store/sectorStore';
|
||||
import { Loader2, PlusIcon, UploadIcon, EditIcon, TrashIcon } from 'lucide-react';
|
||||
import { PlusIcon, DownloadIcon, PencilIcon, TrashBinIcon } from '../../../../icons';
|
||||
import FormModal from '../../../../components/common/FormModal';
|
||||
|
||||
interface TaxonomyBuilderStepProps {
|
||||
@@ -223,9 +223,9 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Taxonomies</div>
|
||||
<div className="text-2xl font-bold">{context.taxonomy_summary.total}</div>
|
||||
<div className="text-2xl font-bold">{context.taxonomy_summary.total_taxonomies}</div>
|
||||
</div>
|
||||
{Object.entries(context.taxonomy_summary.by_type || {}).slice(0, 3).map(([type, count]) => (
|
||||
{Object.entries(context.taxonomy_summary.counts_by_type || {}).slice(0, 3).map(([type, count]) => (
|
||||
<div key={type} className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 capitalize">
|
||||
{type.replace('_', ' ')}
|
||||
@@ -269,7 +269,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
|
||||
onClick={handleImport}
|
||||
variant="outline"
|
||||
>
|
||||
<UploadIcon className="mr-2 h-4 w-4" />
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
@@ -279,7 +279,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
|
||||
{/* Taxonomy Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
</div>
|
||||
) : filteredTaxonomies.length === 0 ? (
|
||||
<Alert variant="info" className="mt-4">
|
||||
@@ -333,7 +333,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
|
||||
size="sm"
|
||||
onClick={() => handleOpenModal(taxonomy)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -414,7 +414,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
|
||||
>
|
||||
{workflowLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -15,6 +15,7 @@ import FormModal, { FormField } from '../../components/common/FormModal';
|
||||
import Alert from '../../components/ui/alert/Alert';
|
||||
import Switch from '../../components/form/switch/Switch';
|
||||
import ViewToggle from '../../components/common/ViewToggle';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import {
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
@@ -108,9 +109,13 @@ export default function SiteList() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - that's fine
|
||||
if (error.status !== 404) {
|
||||
console.warn('Failed to load user preferences:', error);
|
||||
// 500 and other errors should be handled silently - user can still use the page
|
||||
if (error?.status === 404) {
|
||||
// Preferences don't exist yet - this is expected for new users
|
||||
return;
|
||||
}
|
||||
// Silently handle other errors (500, network errors, etc.) - don't spam console
|
||||
// User can still use the page without preferences
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,10 +171,9 @@ export default function SiteList() {
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - show all industries
|
||||
if (error.status !== 404) {
|
||||
console.warn('Failed to load user preferences for filtering:', error);
|
||||
}
|
||||
// 404 means preferences don't exist yet - show all industries (expected for new users)
|
||||
// 500 and other errors - show all industries (graceful degradation)
|
||||
// Silently handle errors - user can still use the page
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
@@ -678,6 +682,13 @@ export default function SiteList() {
|
||||
);
|
||||
}
|
||||
|
||||
// Navigation tabs for Sites module
|
||||
const sitesTabs = [
|
||||
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
|
||||
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
|
||||
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Sites Management - IGNY8" />
|
||||
@@ -687,6 +698,9 @@ export default function SiteList() {
|
||||
hideSiteSector={true}
|
||||
/>
|
||||
|
||||
{/* In-page navigation tabs */}
|
||||
<ModuleNavigationTabs tabs={sitesTabs} />
|
||||
|
||||
{/* Info Alert */}
|
||||
<div className="mb-6">
|
||||
<Alert
|
||||
|
||||
@@ -2159,54 +2159,62 @@ export interface PageBlueprint {
|
||||
|
||||
export interface WorkflowState {
|
||||
current_step: string;
|
||||
step_status: Record<string, { status: string; code?: string; message?: string; updated_at?: string }>;
|
||||
blocking_reason?: string;
|
||||
completed: boolean;
|
||||
blocking_reason?: string;
|
||||
steps: Array<{
|
||||
step: string;
|
||||
status: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
updated_at?: string;
|
||||
}>;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface WizardContext {
|
||||
workflow: WorkflowState;
|
||||
cluster_summary: {
|
||||
total: number;
|
||||
attached: number;
|
||||
coverage_stats: {
|
||||
complete: number;
|
||||
in_progress: number;
|
||||
pending: number;
|
||||
};
|
||||
attached_count: number;
|
||||
coverage_counts: Record<string, number>;
|
||||
clusters: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
keywords_count: number;
|
||||
keyword_count: number;
|
||||
volume: number;
|
||||
context_type?: string;
|
||||
dimension_meta?: Record<string, any>;
|
||||
coverage_status: string;
|
||||
role: string;
|
||||
metadata?: Record<string, any>;
|
||||
suggested_taxonomies?: string[];
|
||||
attribute_hints?: string[];
|
||||
}>;
|
||||
};
|
||||
taxonomy_summary: {
|
||||
total: number;
|
||||
by_type: Record<string, number>;
|
||||
total_taxonomies: number;
|
||||
counts_by_type: Record<string, number>;
|
||||
taxonomies: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
taxonomy_type: string;
|
||||
cluster_count: number;
|
||||
description?: string;
|
||||
cluster_ids: number[];
|
||||
metadata?: Record<string, any>;
|
||||
external_reference?: string;
|
||||
}>;
|
||||
};
|
||||
sitemap_summary: {
|
||||
total_pages: number;
|
||||
by_type: Record<string, number>;
|
||||
coverage_percentage: number;
|
||||
sitemap_summary?: {
|
||||
pages_total: number;
|
||||
pages_by_status: Record<string, number>;
|
||||
pages_by_type: Record<string, number>;
|
||||
};
|
||||
next_actions?: {
|
||||
step: string | null;
|
||||
status: string;
|
||||
message: string | null;
|
||||
code: string | null;
|
||||
};
|
||||
next_actions: Array<{
|
||||
step: string;
|
||||
action: string;
|
||||
message: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function fetchSiteBlueprints(filters?: {
|
||||
|
||||
@@ -235,8 +235,23 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
set({ user: refreshedUser, isAuthenticated: true });
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to refresh user data:', error);
|
||||
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
|
||||
// Only logout on authentication/authorization errors, not on network errors
|
||||
// Network errors (500, timeout, etc.) should not log the user out
|
||||
const isAuthError = error?.code === 'ACCOUNT_REQUIRED' ||
|
||||
error?.code === 'PLAN_REQUIRED' ||
|
||||
error?.status === 401 ||
|
||||
error?.status === 403 ||
|
||||
(error?.message && error.message.includes('Not authenticated'));
|
||||
|
||||
if (isAuthError) {
|
||||
// Real authentication error - logout user
|
||||
console.warn('Authentication error during refresh, logging out:', error);
|
||||
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
|
||||
} else {
|
||||
// Network/server error - don't logout, just throw the error
|
||||
// The caller (AppLayout) will handle it gracefully
|
||||
console.debug('Non-auth error during refresh (will retry):', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -70,21 +70,38 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
|
||||
set({ blueprintId, loading: true, error: null });
|
||||
try {
|
||||
const context = await fetchWizardContext(blueprintId);
|
||||
const workflow = context.workflow;
|
||||
const workflow = context?.workflow;
|
||||
|
||||
// If workflow is null, initialize with defaults
|
||||
if (!workflow) {
|
||||
set({
|
||||
blueprintId,
|
||||
currentStep: DEFAULT_STEP,
|
||||
completedSteps: new Set<WizardStep>(),
|
||||
blockingIssues: [],
|
||||
workflowState: null,
|
||||
context,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine completed steps from workflow state
|
||||
// Backend returns 'steps' as an array, not 'step_status' as an object
|
||||
const completedSteps = new Set<WizardStep>();
|
||||
Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
|
||||
if (status.status === 'ready' || status.status === 'complete') {
|
||||
completedSteps.add(step as WizardStep);
|
||||
const steps = workflow.steps || [];
|
||||
steps.forEach((stepData: any) => {
|
||||
if (stepData?.status === 'ready' || stepData?.status === 'complete') {
|
||||
completedSteps.add(stepData.step as WizardStep);
|
||||
}
|
||||
});
|
||||
|
||||
// Extract blocking issues
|
||||
const blockingIssues: Array<{ step: WizardStep; message: string }> = [];
|
||||
Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
|
||||
if (status.status === 'blocked' && status.message) {
|
||||
blockingIssues.push({ step: step as WizardStep, message: status.message });
|
||||
steps.forEach((stepData: any) => {
|
||||
if (stepData?.status === 'blocked' && stepData?.message) {
|
||||
blockingIssues.push({ step: stepData.step as WizardStep, message: stepData.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,11 +150,17 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
|
||||
},
|
||||
|
||||
completeStep: async (step: WizardStep, metadata?: Record<string, any>) => {
|
||||
const { blueprintId } = get();
|
||||
const { blueprintId, workflowState } = get();
|
||||
if (!blueprintId) {
|
||||
throw new Error('No blueprint initialized');
|
||||
}
|
||||
|
||||
// Ensure workflow is initialized before updating
|
||||
if (!workflowState) {
|
||||
// Try to initialize first
|
||||
await get().initialize(blueprintId);
|
||||
}
|
||||
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const updatedState = await updateWorkflowStep(blueprintId, step, 'ready', metadata);
|
||||
@@ -161,8 +184,13 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
|
||||
// Emit telemetry
|
||||
get().flushTelemetry();
|
||||
} catch (error: any) {
|
||||
// Extract more detailed error message if available
|
||||
const errorMessage = error?.response?.error ||
|
||||
error?.response?.message ||
|
||||
error?.message ||
|
||||
`Failed to complete step: ${step}`;
|
||||
set({
|
||||
error: error.message || `Failed to complete step: ${step}`,
|
||||
error: errorMessage,
|
||||
loading: false,
|
||||
});
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user