frontend changes upto based features ruels generation
This commit is contained in:
parent
343ef63563
commit
d1c150e055
@ -31,23 +31,80 @@ export function AICustomFeatureCreator({
|
||||
projectType,
|
||||
onAdd,
|
||||
onClose,
|
||||
editingFeature,
|
||||
}: {
|
||||
projectType?: string
|
||||
onAdd: (feature: { name: string; description: string; complexity: Complexity; logic_rules?: string[]; requirements?: Array<{ text: string; rules: string[] }>; business_rules?: Array<{ requirement: string; rules: string[] }> }) => void
|
||||
onClose: () => void
|
||||
editingFeature?: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
complexity: Complexity;
|
||||
business_rules?: any;
|
||||
technical_requirements?: any;
|
||||
additional_business_rules?: any;
|
||||
}
|
||||
}) {
|
||||
const [featureName, setFeatureName] = useState('')
|
||||
const [featureDescription, setFeatureDescription] = useState('')
|
||||
const [selectedComplexity, setSelectedComplexity] = useState<Complexity | undefined>(undefined)
|
||||
const [featureName, setFeatureName] = useState(editingFeature?.name || '')
|
||||
const [featureDescription, setFeatureDescription] = useState(editingFeature?.description || '')
|
||||
const [selectedComplexity, setSelectedComplexity] = useState<Complexity | undefined>(editingFeature?.complexity || undefined)
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
const [aiAnalysis, setAiAnalysis] = useState<AIAnalysisResult | null>(null)
|
||||
const [aiAnalysis, setAiAnalysis] = useState<AIAnalysisResult | null>(() => {
|
||||
if (editingFeature) {
|
||||
return {
|
||||
suggested_name: editingFeature.name,
|
||||
complexity: editingFeature.complexity,
|
||||
confidence_score: 1,
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
const [analysisError, setAnalysisError] = useState<string | null>(null)
|
||||
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>([
|
||||
{ text: '', rules: [] },
|
||||
])
|
||||
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>(() => {
|
||||
// Initialize requirements from existing feature data
|
||||
if (editingFeature) {
|
||||
console.log('🔍 Editing feature data:', editingFeature)
|
||||
try {
|
||||
// Try to get business rules from multiple sources
|
||||
let businessRules = null;
|
||||
|
||||
// First try direct business_rules field
|
||||
if (editingFeature.business_rules) {
|
||||
console.log('📋 Found business_rules field:', editingFeature.business_rules)
|
||||
businessRules = Array.isArray(editingFeature.business_rules)
|
||||
? editingFeature.business_rules
|
||||
: JSON.parse(editingFeature.business_rules)
|
||||
}
|
||||
// Then try additional_business_rules from feature_business_rules table
|
||||
else if ((editingFeature as any).additional_business_rules) {
|
||||
console.log('📋 Found additional_business_rules field:', (editingFeature as any).additional_business_rules)
|
||||
businessRules = Array.isArray((editingFeature as any).additional_business_rules)
|
||||
? (editingFeature as any).additional_business_rules
|
||||
: JSON.parse((editingFeature as any).additional_business_rules)
|
||||
}
|
||||
|
||||
console.log('📋 Parsed business rules:', businessRules)
|
||||
|
||||
if (businessRules && Array.isArray(businessRules) && businessRules.length > 0) {
|
||||
const requirements = businessRules.map((rule: any) => ({
|
||||
text: rule.requirement || rule.text || '',
|
||||
rules: rule.rules || []
|
||||
}))
|
||||
console.log('📋 Mapped requirements:', requirements)
|
||||
return requirements
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing business rules:', error)
|
||||
}
|
||||
}
|
||||
return [{ text: '', rules: [] }]
|
||||
})
|
||||
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
|
||||
const hasAnyAnalysis = !!aiAnalysis || requirements.some(r => (r.rules || []).length > 0)
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (hasAnyAnalysis) return
|
||||
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
||||
setIsAnalyzing(true)
|
||||
setAnalysisError(null)
|
||||
@ -100,7 +157,9 @@ export function AICustomFeatureCreator({
|
||||
|
||||
const handleAnalyzeRequirement = async (idx: number) => {
|
||||
const req = requirements[idx]
|
||||
if (hasAnyAnalysis) return
|
||||
if (!req?.text?.trim()) return
|
||||
if ((req.rules || []).length > 0) return
|
||||
setAnalyzingIdx(idx)
|
||||
setAnalysisError(null)
|
||||
try {
|
||||
@ -138,14 +197,10 @@ export function AICustomFeatureCreator({
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!aiAnalysis) {
|
||||
await handleAnalyze()
|
||||
return
|
||||
}
|
||||
onAdd({
|
||||
name: aiAnalysis.suggested_name || featureName.trim() || 'Custom Feature',
|
||||
name: aiAnalysis?.suggested_name || featureName.trim() || 'Custom Feature',
|
||||
description: featureDescription.trim(),
|
||||
complexity: aiAnalysis.complexity || selectedComplexity || 'medium',
|
||||
complexity: aiAnalysis?.complexity || selectedComplexity || 'medium',
|
||||
logic_rules: requirements.flatMap(r => r.rules || []),
|
||||
requirements: requirements,
|
||||
business_rules: requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })),
|
||||
@ -158,7 +213,9 @@ export function AICustomFeatureCreator({
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl max-w-4xl w-full max-h-[90vh] backdrop-blur flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-white text-lg font-semibold">AI-Powered Feature Creator</h3>
|
||||
<h3 className="text-white text-lg font-semibold">
|
||||
{editingFeature ? 'Edit Custom Feature' : 'AI-Powered Feature Creator'}
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-white/60 hover:text-white">×</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -197,10 +254,10 @@ export function AICustomFeatureCreator({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleAnalyzeRequirement(idx)}
|
||||
disabled={isAnalyzing || analyzingIdx === idx || !r.text.trim()}
|
||||
disabled={isAnalyzing || analyzingIdx === idx || !r.text.trim() || hasAnyAnalysis || (r.rules || []).length > 0}
|
||||
className="border-white/20 text-white hover:bg-white/10"
|
||||
>
|
||||
{analyzingIdx === idx ? 'Analyzing…' : (r.rules?.length ? 'Re-analyze' : 'Analyze With AI')}
|
||||
{analyzingIdx === idx ? 'Analyzing…' : (((r.rules || []).length > 0) || hasAnyAnalysis ? 'Analyzed' : 'Analyze With AI')}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
@ -276,7 +333,10 @@ export function AICustomFeatureCreator({
|
||||
)}
|
||||
<Button type="button" variant="outline" onClick={onClose} className="border-white/20 text-white hover:bg-white/10">Cancel</Button>
|
||||
<Button type="submit" disabled={(!featureDescription.trim() && requirements.every(r => !r.text.trim())) || isAnalyzing} className="bg-orange-500 hover:bg-orange-400 text-black">
|
||||
{aiAnalysis ? 'Add Feature with Tagged Rules' : 'Analyze & Add Feature'}
|
||||
{editingFeature
|
||||
? (aiAnalysis ? 'Update Feature with Tagged Rules' : 'Update Feature')
|
||||
: (aiAnalysis ? 'Add Feature with Tagged Rules' : 'Add Feature')
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@ -286,6 +346,4 @@ export function AICustomFeatureCreator({
|
||||
)
|
||||
}
|
||||
|
||||
export default AICustomFeatureCreator
|
||||
|
||||
|
||||
export default AICustomFeatureCreator
|
||||
@ -100,7 +100,8 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh');
|
||||
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
if (refreshToken) {
|
||||
@ -112,6 +113,11 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
||||
return client(originalRequest);
|
||||
}
|
||||
// No refresh token available
|
||||
clearTokens();
|
||||
safeLocalStorage.removeItem('codenuk_user');
|
||||
window.location.href = '/signin';
|
||||
return Promise.reject(error);
|
||||
} catch (refreshError) {
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
clearTokens();
|
||||
|
||||
@ -21,6 +21,7 @@ interface DualCanvasEditorProps {
|
||||
onGenerationStart?: () => void
|
||||
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
||||
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
||||
initialPrompt?: string
|
||||
}
|
||||
|
||||
export function DualCanvasEditor({
|
||||
@ -28,7 +29,8 @@ export function DualCanvasEditor({
|
||||
onWireframeGenerated,
|
||||
onGenerationStart,
|
||||
selectedDevice = 'desktop',
|
||||
onDeviceChange
|
||||
onDeviceChange,
|
||||
initialPrompt
|
||||
}: DualCanvasEditorProps) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const [canvasMode, setCanvasMode] = useState<'wireframe' | 'components'>('wireframe')
|
||||
@ -216,6 +218,7 @@ export function DualCanvasEditor({
|
||||
className="shrink-0"
|
||||
selectedDevice={selectedDevice}
|
||||
onDeviceChange={handleDeviceChange}
|
||||
initialPrompt={initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -7,7 +7,6 @@ import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { EditFeatureForm } from "@/components/edit-feature-form"
|
||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette } from "lucide-react"
|
||||
import { useTemplates } from "@/hooks/useTemplates"
|
||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||
@ -20,6 +19,7 @@ import { Tooltip } from "@/components/ui/tooltip"
|
||||
import WireframeCanvas from "@/components/wireframe-canvas"
|
||||
import PromptSidePanel from "@/components/prompt-side-panel"
|
||||
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
||||
import { getAccessToken } from "@/components/apis/authApiClients"
|
||||
|
||||
interface Template {
|
||||
id: string
|
||||
@ -857,14 +857,14 @@ function FeatureSelectionStep({
|
||||
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
|
||||
|
||||
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
|
||||
const idForApi = f.feature_type === 'custom' ? (f.feature_id?.replace(/^custom_/, '') || f.id) : f.id
|
||||
await updateFeature(idForApi, { ...updates, isCustom: f.feature_type === 'custom' })
|
||||
// Use the actual id field directly (no need to extract from feature_id)
|
||||
await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' })
|
||||
await load()
|
||||
}
|
||||
|
||||
const handleDelete = async (f: TemplateFeature) => {
|
||||
const idForApi = f.feature_type === 'custom' ? (f.feature_id?.replace(/^custom_/, '') || f.id) : f.id
|
||||
await deleteFeature(idForApi, { isCustom: f.feature_type === 'custom' })
|
||||
// Use the actual id field directly (no need to extract from feature_id)
|
||||
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(f.id)
|
||||
@ -977,23 +977,25 @@ function FeatureSelectionStep({
|
||||
|
||||
{section('Your Custom Features', custom)}
|
||||
|
||||
{showAIModal && (
|
||||
{(showAIModal || editingFeature) && (
|
||||
<AICustomFeatureCreator
|
||||
projectType={template.type || template.title}
|
||||
onAdd={async (f) => { await handleAddAIAnalyzed(f); setShowAIModal(false) }}
|
||||
onClose={() => setShowAIModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingFeature && (
|
||||
<EditFeatureForm
|
||||
feature={editingFeature}
|
||||
onSubmit={async (updates) => {
|
||||
await handleUpdate(editingFeature, updates)
|
||||
onAdd={async (f) => {
|
||||
if (editingFeature) {
|
||||
// Update existing feature
|
||||
await handleUpdate(editingFeature, f)
|
||||
setEditingFeature(null)
|
||||
} else {
|
||||
// Add new feature
|
||||
await handleAddAIAnalyzed(f)
|
||||
setShowAIModal(false)
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowAIModal(false)
|
||||
setEditingFeature(null)
|
||||
}}
|
||||
onCancel={() => setEditingFeature(null)}
|
||||
isOpen={!!editingFeature}
|
||||
editingFeature={editingFeature || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1037,9 +1039,10 @@ function BusinessQuestionsStep({
|
||||
setError('No features selected')
|
||||
return
|
||||
}
|
||||
const resp = await fetch(`${BACKEND_URL}/api/v1/generate-comprehensive-business-questions`, {
|
||||
const token = getAccessToken()
|
||||
const resp = await fetch(`${BACKEND_URL}/api/questions/generate-comprehensive-business-questions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||
body: JSON.stringify({
|
||||
allFeatures: selected,
|
||||
projectName: template.title,
|
||||
@ -1327,6 +1330,7 @@ function AIMockupStep({
|
||||
const [wireframeData, setWireframeData] = useState<any>(null)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [selectedDevice, setSelectedDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
|
||||
const [initialPrompt, setInitialPrompt] = useState<string>("")
|
||||
|
||||
// Load state from localStorage after component mounts
|
||||
useEffect(() => {
|
||||
@ -1352,6 +1356,17 @@ function AIMockupStep({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Build prompt from selected features and template
|
||||
useEffect(() => {
|
||||
if (!template) return
|
||||
const featureNames = (selectedFeatures || []).map(f => f.name).filter(Boolean)
|
||||
const base = `${template.title} dashboard`
|
||||
const parts = featureNames.length > 0 ? ` with ${featureNames.join(', ')}` : ''
|
||||
const footer = '. Optimize layout and spacing. Include navigation.'
|
||||
const prompt = `${base}${parts}${footer}`
|
||||
setInitialPrompt(prompt)
|
||||
}, [template, JSON.stringify(selectedFeatures)])
|
||||
|
||||
const handleWireframeGenerated = (data: any) => {
|
||||
setWireframeData(data)
|
||||
setIsGenerating(false)
|
||||
@ -1414,6 +1429,7 @@ function AIMockupStep({
|
||||
onGenerationStart={handleWireframeGenerationStart}
|
||||
selectedDevice={selectedDevice}
|
||||
onDeviceChange={handleDeviceChange}
|
||||
initialPrompt={initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState, useEffect } from "react"
|
||||
import { useMemo, useState, useEffect, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
@ -11,11 +11,13 @@ import { getAIMockupHealthUrl, AI_MOCKUP_CONFIG } from "@/lib/api-config"
|
||||
export function PromptSidePanel({
|
||||
className,
|
||||
selectedDevice = 'desktop',
|
||||
onDeviceChange
|
||||
onDeviceChange,
|
||||
initialPrompt
|
||||
}: {
|
||||
className?: string
|
||||
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
||||
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
||||
initialPrompt?: string
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [prompt, setPrompt] = useState(
|
||||
@ -23,6 +25,7 @@ export function PromptSidePanel({
|
||||
)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking')
|
||||
const autoTriggeredRef = useRef(false)
|
||||
|
||||
const examples = useMemo(
|
||||
() => [
|
||||
@ -66,6 +69,25 @@ export function PromptSidePanel({
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Update prompt from parent when provided
|
||||
useEffect(() => {
|
||||
if (typeof initialPrompt === 'string' && initialPrompt.trim().length > 0) {
|
||||
setPrompt(initialPrompt)
|
||||
}
|
||||
}, [initialPrompt])
|
||||
|
||||
// Auto-generate once when an initial prompt arrives
|
||||
useEffect(() => {
|
||||
if (!autoTriggeredRef.current && typeof initialPrompt === 'string' && initialPrompt.trim().length > 0) {
|
||||
autoTriggeredRef.current = true
|
||||
// Slight delay to ensure canvas listeners are mounted
|
||||
const id = setTimeout(() => {
|
||||
dispatchGenerate(initialPrompt)
|
||||
}, 300)
|
||||
return () => clearTimeout(id)
|
||||
}
|
||||
}, [initialPrompt])
|
||||
|
||||
const dispatchGenerate = async (text: string) => {
|
||||
setIsGenerating(true)
|
||||
|
||||
@ -208,7 +230,7 @@ export function PromptSidePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-1">
|
||||
{/* <div className="pt-1">
|
||||
<p className="text-xs font-medium mb-2">Examples</p>
|
||||
<ScrollArea className="h-40 border rounded">
|
||||
<ul className="p-2 space-y-2">
|
||||
@ -226,7 +248,7 @@ export function PromptSidePanel({
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* AI Features Info */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
|
||||
@ -1375,6 +1375,19 @@ export default function WireframeCanvas({
|
||||
|
||||
return (
|
||||
<div className={cn("relative h-full w-full flex flex-col", className)}>
|
||||
{/* Centered loading overlay during generation */}
|
||||
{busy && (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex items-center justify-center bg-white/60 dark:bg-black/60"
|
||||
aria-busy="true"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Generating wireframe…</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="absolute top-4 left-4 right-4 z-50 bg-red-50 border border-red-200 rounded-lg p-3 shadow-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -1391,7 +1404,7 @@ export default function WireframeCanvas({
|
||||
)}
|
||||
|
||||
{busy && (
|
||||
<div className="absolute top-4 left-4 right-4 z-50 bg-blue-50 border border-blue-200 rounded-lg p-3 shadow-lg">
|
||||
<div className="absolute top-4 left-4 right-4 z-40 bg-blue-50 border border-blue-200 rounded-lg p-3 shadow-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
||||
<span className="text-blue-600 text-sm font-medium">Generating wireframe with AI...</span>
|
||||
|
||||
@ -409,9 +409,31 @@ export const featureApi = {
|
||||
technical_requirements?: Record<string, unknown>;
|
||||
created_by_user_session?: string;
|
||||
}): Promise<{ data: AdminFeature; similarityInfo?: Record<string, unknown> }> => {
|
||||
const normalizeJsonField = (value: unknown): unknown => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const t = value.trim();
|
||||
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
|
||||
return JSON.parse(t);
|
||||
}
|
||||
return value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const cleaned = {
|
||||
...featureData,
|
||||
business_rules: normalizeJsonField(featureData.business_rules) as Record<string, unknown>,
|
||||
technical_requirements: normalizeJsonField(featureData.technical_requirements) as Record<string, unknown>,
|
||||
};
|
||||
|
||||
const response = await apiCall<AdminFeature>('/api/features/custom', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(featureData),
|
||||
body: JSON.stringify(cleaned),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -442,9 +442,37 @@ class TemplateService {
|
||||
}
|
||||
|
||||
async createFeature(featureData: CreateFeaturePayload): Promise<TemplateFeature> {
|
||||
// Normalize potentially stringified JSON fields to proper JSON before sending
|
||||
const normalizeJsonField = (value: unknown): unknown => {
|
||||
if (value === undefined || value === null || value === '') return undefined
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const trimmed = value.trim()
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
return JSON.parse(trimmed)
|
||||
}
|
||||
// If it's a plain string not JSON, leave as-is for non-JSONB fields
|
||||
return value
|
||||
} catch {
|
||||
// Leave as-is; backend will reject invalid JSON, but we prefer to avoid sending broken JSONB
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const cleanedFeatureData: CreateFeaturePayload = {
|
||||
...featureData,
|
||||
// Only include normalized fields if valid
|
||||
business_rules: normalizeJsonField(featureData.business_rules) as CreateFeaturePayload['business_rules'],
|
||||
logic_rules: normalizeJsonField(featureData.logic_rules) as CreateFeaturePayload['logic_rules'],
|
||||
// @ts-expect-error: allow passthrough of optional technical_requirements if present in callers
|
||||
technical_requirements: normalizeJsonField((featureData as any).technical_requirements),
|
||||
}
|
||||
|
||||
if (
|
||||
featureData &&
|
||||
(featureData.feature_type === 'custom')
|
||||
cleanedFeatureData &&
|
||||
(cleanedFeatureData.feature_type === 'custom')
|
||||
) {
|
||||
const customHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
const customToken = getAccessToken()
|
||||
@ -454,7 +482,7 @@ class TemplateService {
|
||||
const response = await fetch(`${BACKEND_URL}/api/features/custom`, {
|
||||
method: 'POST',
|
||||
headers: customHeaders,
|
||||
body: JSON.stringify(featureData),
|
||||
body: JSON.stringify(cleanedFeatureData),
|
||||
})
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
const data = await response.json()
|
||||
@ -469,7 +497,7 @@ class TemplateService {
|
||||
const response = await fetch(`${BACKEND_URL}/api/features`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(featureData),
|
||||
body: JSON.stringify(cleanedFeatureData),
|
||||
})
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
const data = await response.json()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user