Flow builder save buttons and category sub-creator
Made-with: Cursor
This commit is contained in:
parent
089c1ac869
commit
63f0ff9157
|
|
@ -24,6 +24,8 @@ export function CourseCategoryPage() {
|
||||||
const [newCategoryName, setNewCategoryName] = useState("")
|
const [newCategoryName, setNewCategoryName] = useState("")
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [parentCategoryId, setParentCategoryId] = useState<number | null>(null)
|
const [parentCategoryId, setParentCategoryId] = useState<number | null>(null)
|
||||||
|
const [newSubCategoryName, setNewSubCategoryName] = useState("")
|
||||||
|
const [pendingSubCategories, setPendingSubCategories] = useState<string[]>([])
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -154,7 +156,7 @@ export function CourseCategoryPage() {
|
||||||
|
|
||||||
{/* Create category dialog */}
|
{/* Create category dialog */}
|
||||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Plus className="h-4 w-4 text-brand-500" />
|
<Plus className="h-4 w-4 text-brand-500" />
|
||||||
|
|
@ -165,6 +167,7 @@ export function CourseCategoryPage() {
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-4 grid gap-6 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1.4fr)]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-grayScale-600">
|
<label className="mb-1.5 block text-sm font-medium text-grayScale-600">
|
||||||
|
|
@ -193,6 +196,79 @@ export function CourseCategoryPage() {
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
<p className="mt-1 text-[11px] text-grayScale-400">
|
||||||
|
When left empty, this becomes a parent category. Any sub categories you add on the
|
||||||
|
right will be created under it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="text-xs font-semibold text-grayScale-600">
|
||||||
|
Sub categories for this category (optional)
|
||||||
|
</p>
|
||||||
|
{pendingSubCategories.length > 0 && (
|
||||||
|
<span className="text-[11px] text-grayScale-400">
|
||||||
|
{pendingSubCategories.length} sub categor
|
||||||
|
{pendingSubCategories.length === 1 ? "y" : "ies"} to create
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. Grammar basics, Speaking, Exam practice"
|
||||||
|
value={newSubCategoryName}
|
||||||
|
onChange={(e) => setNewSubCategoryName(e.target.value)}
|
||||||
|
className="h-9 text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 px-3 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
const name = newSubCategoryName.trim()
|
||||||
|
if (!name) return
|
||||||
|
if (pendingSubCategories.includes(name)) {
|
||||||
|
setNewSubCategoryName("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPendingSubCategories((prev) => [...prev, name])
|
||||||
|
setNewSubCategoryName("")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-[3.5rem] rounded-lg border border-dashed border-grayScale-200 bg-grayScale-50/60 p-2">
|
||||||
|
{pendingSubCategories.length === 0 ? (
|
||||||
|
<p className="text-[11px] leading-relaxed text-grayScale-400">
|
||||||
|
Added sub categories will appear here so you can visually confirm the structure
|
||||||
|
before saving. This is optional.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{pendingSubCategories.map((name) => (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
type="button"
|
||||||
|
className="group inline-flex items-center gap-1 rounded-full bg-white px-2 py-0.5 text-[11px] text-grayScale-600 shadow-sm ring-1 ring-grayScale-200 hover:bg-red-50 hover:text-red-600 hover:ring-red-200"
|
||||||
|
onClick={() =>
|
||||||
|
setPendingSubCategories((prev) =>
|
||||||
|
prev.filter((subName) => subName !== name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="max-w-[160px] truncate">{name}</span>
|
||||||
|
<span className="text-[10px] text-grayScale-300 group-hover:text-red-400">
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -202,6 +278,9 @@ export function CourseCategoryPage() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCreateOpen(false)
|
setCreateOpen(false)
|
||||||
setNewCategoryName("")
|
setNewCategoryName("")
|
||||||
|
setParentCategoryId(null)
|
||||||
|
setNewSubCategoryName("")
|
||||||
|
setPendingSubCategories([])
|
||||||
}}
|
}}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
>
|
>
|
||||||
|
|
@ -214,15 +293,48 @@ export function CourseCategoryPage() {
|
||||||
if (!newCategoryName.trim()) return
|
if (!newCategoryName.trim()) return
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
try {
|
try {
|
||||||
await createCourseCategory({
|
const name = newCategoryName.trim()
|
||||||
|
const parentPayloadId = parentCategoryId ?? null
|
||||||
|
const parentRes = await createCourseCategory({
|
||||||
name: newCategoryName.trim(),
|
name: newCategoryName.trim(),
|
||||||
parent_id: parentCategoryId ?? null,
|
parent_id: parentPayloadId,
|
||||||
})
|
})
|
||||||
|
let createdCategoryId: number | null = null
|
||||||
|
try {
|
||||||
|
const data: any = parentRes?.data
|
||||||
|
createdCategoryId =
|
||||||
|
data?.data?.category?.id ??
|
||||||
|
data?.data?.id ??
|
||||||
|
data?.category?.id ??
|
||||||
|
data?.id ??
|
||||||
|
null
|
||||||
|
} catch {
|
||||||
|
createdCategoryId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdCategoryId && pendingSubCategories.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
pendingSubCategories.map((subName) =>
|
||||||
|
createCourseCategory({
|
||||||
|
name: subName,
|
||||||
|
parent_id: createdCategoryId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
toast.success("Category created", {
|
toast.success("Category created", {
|
||||||
description: `"${newCategoryName.trim()}" has been added.`,
|
description:
|
||||||
|
pendingSubCategories.length > 0
|
||||||
|
? `"${name}" and ${pendingSubCategories.length} sub categor${
|
||||||
|
pendingSubCategories.length === 1 ? "y" : "ies"
|
||||||
|
} have been added.`
|
||||||
|
: `"${name}" has been added.`,
|
||||||
})
|
})
|
||||||
setNewCategoryName("")
|
setNewCategoryName("")
|
||||||
setParentCategoryId(null)
|
setParentCategoryId(null)
|
||||||
|
setNewSubCategoryName("")
|
||||||
|
setPendingSubCategories([])
|
||||||
setCreateOpen(false)
|
setCreateOpen(false)
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ export function CourseFlowBuilderPage() {
|
||||||
// Order of sub category ids for the selected parent (scope = sub)
|
// Order of sub category ids for the selected parent (scope = sub)
|
||||||
const [subCategoryOrder, setSubCategoryOrder] = useState<string[]>([])
|
const [subCategoryOrder, setSubCategoryOrder] = useState<string[]>([])
|
||||||
const [dragCategoryId, setDragCategoryId] = useState<string | null>(null)
|
const [dragCategoryId, setDragCategoryId] = useState<string | null>(null)
|
||||||
|
const [parentOrderDirty, setParentOrderDirty] = useState(false)
|
||||||
|
const [subOrderDirty, setSubOrderDirty] = useState(false)
|
||||||
|
const [stepsDirty, setStepsDirty] = useState(false)
|
||||||
|
|
||||||
const parentCategories = useMemo(
|
const parentCategories = useMemo(
|
||||||
() => categories.filter((c) => !c.parent_id),
|
() => categories.filter((c) => !c.parent_id),
|
||||||
|
|
@ -147,6 +150,7 @@ export function CourseFlowBuilderPage() {
|
||||||
const parsed: string[] = JSON.parse(raw)
|
const parsed: string[] = JSON.parse(raw)
|
||||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
setParentCategoryOrder(parsed)
|
setParentCategoryOrder(parsed)
|
||||||
|
setParentOrderDirty(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,14 +158,9 @@ export function CourseFlowBuilderPage() {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
setParentCategoryOrder(parentCategories.map((c) => String(c.id)))
|
setParentCategoryOrder(parentCategories.map((c) => String(c.id)))
|
||||||
|
setParentOrderDirty(false)
|
||||||
}, [parentCategories.length])
|
}, [parentCategories.length])
|
||||||
|
|
||||||
// Persist parent category order
|
|
||||||
useEffect(() => {
|
|
||||||
if (parentCategoryOrder.length === 0) return
|
|
||||||
window.localStorage.setItem(PARENT_ORDER_KEY, JSON.stringify(parentCategoryOrder))
|
|
||||||
}, [parentCategoryOrder])
|
|
||||||
|
|
||||||
// Load sub category order for selected parent
|
// Load sub category order for selected parent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedParentCategoryId || subCategoriesForParent.length === 0) {
|
if (!selectedParentCategoryId || subCategoriesForParent.length === 0) {
|
||||||
|
|
@ -175,6 +174,7 @@ export function CourseFlowBuilderPage() {
|
||||||
const parsed: string[] = JSON.parse(raw)
|
const parsed: string[] = JSON.parse(raw)
|
||||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
setSubCategoryOrder(parsed)
|
setSubCategoryOrder(parsed)
|
||||||
|
setSubOrderDirty(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,21 +182,14 @@ export function CourseFlowBuilderPage() {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
setSubCategoryOrder(subCategoriesForParent.map((c) => String(c.id)))
|
setSubCategoryOrder(subCategoriesForParent.map((c) => String(c.id)))
|
||||||
|
setSubOrderDirty(false)
|
||||||
}, [selectedParentCategoryId, subCategoriesForParent.length])
|
}, [selectedParentCategoryId, subCategoriesForParent.length])
|
||||||
|
|
||||||
// Persist sub category order for selected parent
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedParentCategoryId || subCategoryOrder.length === 0) return
|
|
||||||
window.localStorage.setItem(
|
|
||||||
`${SUB_ORDER_KEY_PREFIX}${selectedParentCategoryId}`,
|
|
||||||
JSON.stringify(subCategoryOrder),
|
|
||||||
)
|
|
||||||
}, [selectedParentCategoryId, subCategoryOrder])
|
|
||||||
|
|
||||||
// Load flow steps for selected sub category only (sub category structure)
|
// Load flow steps for selected sub category only (sub category structure)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scope !== "sub" || !selectedSubCategoryId) {
|
if (scope !== "sub" || !selectedSubCategoryId) {
|
||||||
setSteps([])
|
setSteps([])
|
||||||
|
setStepsDirty(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const key = `subcategory_flow_${selectedSubCategoryId}`
|
const key = `subcategory_flow_${selectedSubCategoryId}`
|
||||||
|
|
@ -205,6 +198,7 @@ export function CourseFlowBuilderPage() {
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed: FlowStep[] = JSON.parse(raw)
|
const parsed: FlowStep[] = JSON.parse(raw)
|
||||||
setSteps(parsed)
|
setSteps(parsed)
|
||||||
|
setStepsDirty(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -212,20 +206,35 @@ export function CourseFlowBuilderPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: FlowStep[] = [
|
const defaults: FlowStep[] = [
|
||||||
{ id: `${selectedSubCategoryId}-lesson`, type: "lesson", title: "Core lessons", description: "Main learning content for this sub category." },
|
{
|
||||||
{ id: `${selectedSubCategoryId}-practice`, type: "practice", title: "Practice sessions", description: "Speaking or practice activities to reinforce learning." },
|
id: `${selectedSubCategoryId}-lesson`,
|
||||||
{ id: `${selectedSubCategoryId}-exam`, type: "exam", title: "Exam / Assessment", description: "Formal evaluation of student understanding." },
|
type: "lesson",
|
||||||
{ id: `${selectedSubCategoryId}-feedback`, type: "feedback", title: "Feedback loop", description: "Collect feedback and share results with learners." },
|
title: "Core lessons",
|
||||||
|
description: "Main learning content for this sub category.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${selectedSubCategoryId}-practice`,
|
||||||
|
type: "practice",
|
||||||
|
title: "Practice sessions",
|
||||||
|
description: "Speaking or practice activities to reinforce learning.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${selectedSubCategoryId}-exam`,
|
||||||
|
type: "exam",
|
||||||
|
title: "Exam / Assessment",
|
||||||
|
description: "Formal evaluation of student understanding.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${selectedSubCategoryId}-feedback`,
|
||||||
|
type: "feedback",
|
||||||
|
title: "Feedback loop",
|
||||||
|
description: "Collect feedback and share results with learners.",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
setSteps(defaults)
|
setSteps(defaults)
|
||||||
|
setStepsDirty(true)
|
||||||
}, [scope, selectedSubCategoryId])
|
}, [scope, selectedSubCategoryId])
|
||||||
|
|
||||||
// Persist flow steps for selected sub category
|
|
||||||
useEffect(() => {
|
|
||||||
if (scope !== "sub" || !selectedSubCategoryId) return
|
|
||||||
window.localStorage.setItem(`subcategory_flow_${selectedSubCategoryId}`, JSON.stringify(steps))
|
|
||||||
}, [steps, scope, selectedSubCategoryId])
|
|
||||||
|
|
||||||
const handleReorder = (targetId: string) => {
|
const handleReorder = (targetId: string) => {
|
||||||
if (!dragStepId || dragStepId === targetId) return
|
if (!dragStepId || dragStepId === targetId) return
|
||||||
setSteps((prev) => {
|
setSteps((prev) => {
|
||||||
|
|
@ -238,6 +247,7 @@ export function CourseFlowBuilderPage() {
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
setDragStepId(null)
|
setDragStepId(null)
|
||||||
|
setStepsDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReorderParentCategory = (targetId: string) => {
|
const handleReorderParentCategory = (targetId: string) => {
|
||||||
|
|
@ -252,6 +262,7 @@ export function CourseFlowBuilderPage() {
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
setDragCategoryId(null)
|
setDragCategoryId(null)
|
||||||
|
setParentOrderDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReorderSubCategory = (targetId: string) => {
|
const handleReorderSubCategory = (targetId: string) => {
|
||||||
|
|
@ -266,6 +277,28 @@ export function CourseFlowBuilderPage() {
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
setDragCategoryId(null)
|
setDragCategoryId(null)
|
||||||
|
setSubOrderDirty(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveParentOrder = () => {
|
||||||
|
if (orderedParentCategories.length === 0 || parentCategoryOrder.length === 0) return
|
||||||
|
window.localStorage.setItem(PARENT_ORDER_KEY, JSON.stringify(parentCategoryOrder))
|
||||||
|
setParentOrderDirty(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSubOrder = () => {
|
||||||
|
if (!selectedParentCategoryId || subCategoryOrder.length === 0) return
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${SUB_ORDER_KEY_PREFIX}${selectedParentCategoryId}`,
|
||||||
|
JSON.stringify(subCategoryOrder),
|
||||||
|
)
|
||||||
|
setSubOrderDirty(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSteps = () => {
|
||||||
|
if (scope !== "sub" || !selectedSubCategoryId) return
|
||||||
|
window.localStorage.setItem(`subcategory_flow_${selectedSubCategoryId}`, JSON.stringify(steps))
|
||||||
|
setStepsDirty(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultDescription = (type: StepType): string => {
|
const getDefaultDescription = (type: StepType): string => {
|
||||||
|
|
@ -299,14 +332,17 @@ export function CourseFlowBuilderPage() {
|
||||||
description: getDefaultDescription(type),
|
description: getDefaultDescription(type),
|
||||||
}
|
}
|
||||||
setSteps((prev) => [...prev, newStep])
|
setSteps((prev) => [...prev, newStep])
|
||||||
|
setStepsDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateStep = (id: string, changes: Partial<FlowStep>) => {
|
const handleUpdateStep = (id: string, changes: Partial<FlowStep>) => {
|
||||||
setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, ...changes } : s)))
|
setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, ...changes } : s)))
|
||||||
|
setStepsDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveStep = (id: string) => {
|
const handleRemoveStep = (id: string) => {
|
||||||
setSteps((prev) => prev.filter((s) => s.id !== id))
|
setSteps((prev) => prev.filter((s) => s.id !== id))
|
||||||
|
setStepsDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -424,12 +460,26 @@ export function CourseFlowBuilderPage() {
|
||||||
{scope === "parent" && (
|
{scope === "parent" && (
|
||||||
<Card className="shadow-soft">
|
<Card className="shadow-soft">
|
||||||
<CardHeader className="border-b border-grayScale-200 pb-3">
|
<CardHeader className="border-b border-grayScale-200 pb-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
<CardTitle className="text-base font-semibold text-grayScale-600">
|
<CardTitle className="text-base font-semibold text-grayScale-600">
|
||||||
Parent category sequence
|
Parent category sequence
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="mt-1 text-xs text-grayScale-400">
|
<p className="mt-1 text-xs text-grayScale-400">
|
||||||
Drag to reorder the sequence in which parent categories appear. No courses or steps—order only.
|
Drag to reorder the sequence in which parent categories appear. No courses or
|
||||||
|
steps—order only.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-3 text-[11px]"
|
||||||
|
disabled={!parentOrderDirty || orderedParentCategories.length === 0}
|
||||||
|
onClick={handleSaveParentOrder}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 pt-4">
|
<CardContent className="space-y-2 pt-4">
|
||||||
{orderedParentCategories.length === 0 ? (
|
{orderedParentCategories.length === 0 ? (
|
||||||
|
|
@ -466,12 +516,25 @@ export function CourseFlowBuilderPage() {
|
||||||
<>
|
<>
|
||||||
<Card className="shadow-soft">
|
<Card className="shadow-soft">
|
||||||
<CardHeader className="border-b border-grayScale-200 pb-3">
|
<CardHeader className="border-b border-grayScale-200 pb-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
<CardTitle className="text-base font-semibold text-grayScale-600">
|
<CardTitle className="text-base font-semibold text-grayScale-600">
|
||||||
Sub category sequence
|
Sub category sequence
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="mt-1 text-xs text-grayScale-400">
|
<p className="mt-1 text-xs text-grayScale-400">
|
||||||
Drag to reorder sub categories under this parent.
|
Drag to reorder sub categories under this parent.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-3 text-[11px]"
|
||||||
|
disabled={!subOrderDirty || orderedSubCategories.length === 0}
|
||||||
|
onClick={handleSaveSubOrder}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 pt-4">
|
<CardContent className="space-y-2 pt-4">
|
||||||
{orderedSubCategories.length === 0 ? (
|
{orderedSubCategories.length === 0 ? (
|
||||||
|
|
@ -507,12 +570,25 @@ export function CourseFlowBuilderPage() {
|
||||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
|
<div className="grid gap-4 md:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
|
||||||
<Card className="shadow-soft">
|
<Card className="shadow-soft">
|
||||||
<CardHeader className="border-b border-grayScale-200 pb-3">
|
<CardHeader className="border-b border-grayScale-200 pb-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
<CardTitle className="text-base font-semibold text-grayScale-600">
|
<CardTitle className="text-base font-semibold text-grayScale-600">
|
||||||
Sub category structure
|
Sub category structure
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="mt-1 text-xs text-grayScale-400">
|
<p className="mt-1 text-xs text-grayScale-400">
|
||||||
Courses, questions, and feedback steps for “{selectedSubCategory.name}”.
|
Courses, questions, and feedback steps for “{selectedSubCategory.name}”.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-3 text-[11px]"
|
||||||
|
disabled={!stepsDirty || steps.length === 0}
|
||||||
|
onClick={handleSaveSteps}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 pt-4">
|
<CardContent className="space-y-3 pt-4">
|
||||||
{steps.length === 0 && (
|
{steps.length === 0 && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user