fix: UX deep dive — 28 fixes across authoring, navigation, consistency, and cleanup #86
@@ -37,15 +37,16 @@ function handleGlobalError(error: AxiosError) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
// Rate limit — always worth notifying
|
||||
if (status === 429) {
|
||||
toast.error(detail || 'Too many requests — please try again shortly')
|
||||
return
|
||||
}
|
||||
|
||||
// Client errors (4xx) — show backend detail if present
|
||||
// Client errors (4xx) — don't toast globally.
|
||||
// Pages handle their own 4xx errors (permission checks, validation, not-found)
|
||||
// and many are caught silently. Global toasts here cause noisy duplicates.
|
||||
if (status >= 400 && status < 500) {
|
||||
toast.error(detail || 'Invalid request')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -40,14 +40,15 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Initialize/reset draft when nodeId changes
|
||||
// Initialize/reset draft when nodeId changes or when node type changes
|
||||
// (e.g., answer stub → decision/action/solution via type picker)
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
setDraft(cloneWithoutChildren(node))
|
||||
setIsDirty(false)
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
}, [nodeId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleDraftUpdate = useCallback((updates: Partial<TreeStructure>) => {
|
||||
setDraft(prev => prev ? { ...prev, ...updates } : prev)
|
||||
@@ -60,7 +61,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
updateNode(nodeId, draftWithoutChildren)
|
||||
|
||||
// Auto-create answer stubs for new decision options without next_node_id
|
||||
if (draft.options) {
|
||||
if (draft.type === 'decision' && draft.options) {
|
||||
const options = draft.options.filter(o => o.label.trim())
|
||||
const stubsCreated: Array<{ optId: string; stubId: string }> = []
|
||||
|
||||
@@ -81,6 +82,13 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-create answer stub for action node without next_node_id
|
||||
if (draft.type === 'action' && !draft.next_node_id) {
|
||||
const stubId = addNode(nodeId, 'answer')
|
||||
updateNode(stubId, { title: 'Next Step' })
|
||||
updateNode(nodeId, { next_node_id: stubId })
|
||||
}
|
||||
|
||||
setIsDirty(false)
|
||||
}, [draft, node, nodeId, updateNode, addNode])
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { DynamicArrayField } from './DynamicArrayField'
|
||||
import { NodePicker } from './NodePicker'
|
||||
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||
import { InfoTip } from '@/components/common/InfoTip'
|
||||
@@ -20,9 +19,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
e => e.nodeId === node.id && e.field === 'title'
|
||||
)
|
||||
|
||||
const nextNodeError = validationErrors.find(
|
||||
e => e.nodeId === node.id && e.field === 'next_node_id'
|
||||
)
|
||||
const hasNextNode = !!node.next_node_id
|
||||
|
||||
const handleAddCommand = () => {
|
||||
onUpdate({
|
||||
@@ -161,16 +158,16 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Next Node */}
|
||||
<NodePicker
|
||||
value={node.next_node_id || ''}
|
||||
onChange={(nodeId) => onUpdate({ next_node_id: nodeId })}
|
||||
parentNodeId={node.id}
|
||||
excludeNodeId={node.id}
|
||||
label="Next Node (after action)"
|
||||
placeholder="Select or create next node..."
|
||||
error={nextNodeError?.message}
|
||||
/>
|
||||
{/* Next step hint */}
|
||||
{hasNextNode ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Next step is linked — click it on the canvas to edit.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-yellow-400/70">
|
||||
Save to create a placeholder for the next step.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -198,59 +198,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Sonner Toast Customization */
|
||||
/* Sonner Toast Customization — outside @layer for higher specificity */
|
||||
[data-sonner-toast] {
|
||||
background-color: hsl(var(--card)) !important;
|
||||
color: hsl(var(--card-foreground)) !important;
|
||||
border: 1px solid hsl(var(--border)) !important;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3) !important;
|
||||
border-radius: 0.75rem;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-title] {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="success"] {
|
||||
border-color: rgba(52, 211, 153, 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="success"] [data-icon] {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="error"] {
|
||||
border-color: rgba(248, 113, 113, 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="error"] [data-icon] {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="info"] {
|
||||
border-color: hsl(var(--border)) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="info"] [data-icon] {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="warning"] {
|
||||
border-color: rgba(251, 191, 36, 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="warning"] [data-icon] {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-close-button] {
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-radius: 0.375rem;
|
||||
transition: color 150ms, background-color 150ms;
|
||||
}
|
||||
[data-sonner-toast] [data-close-button]:hover {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
|
||||
/* React Day Picker Customization */
|
||||
@layer components {
|
||||
:where([data-sonner-toast]) {
|
||||
@apply bg-card text-card-foreground;
|
||||
@apply border border-border shadow-lg;
|
||||
@apply rounded-xl;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) [data-title] {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-type="success"]) {
|
||||
border-color: rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="success"]) [data-icon] {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-type="error"]) {
|
||||
border-color: rgba(248, 113, 113, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="error"]) [data-icon] {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-type="info"]) {
|
||||
@apply border-border;
|
||||
}
|
||||
:where([data-sonner-toast][data-type="info"]) [data-icon] {
|
||||
@apply text-muted-foreground;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-type="warning"]) {
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="warning"]) [data-icon] {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) [data-close-button] {
|
||||
@apply text-muted-foreground hover:bg-accent hover:text-accent-foreground;
|
||||
@apply rounded-md transition-colors;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) [data-icon][data-loading] {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
/* React Day Picker Customization */
|
||||
.rdp-custom {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ createRoot(document.getElementById('root')!).render(
|
||||
closeButton
|
||||
visibleToasts={3}
|
||||
gap={8}
|
||||
theme="dark"
|
||||
toastOptions={{
|
||||
className: 'sonner-toast-custom',
|
||||
}}
|
||||
/>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
|
||||
Reference in New Issue
Block a user