diff --git a/packages/admin/src/components/ContentTypeEditor.tsx b/packages/admin/src/components/ContentTypeEditor.tsx index 5c65d9a..d020ad9 100644 --- a/packages/admin/src/components/ContentTypeEditor.tsx +++ b/packages/admin/src/components/ContentTypeEditor.tsx @@ -1,4 +1,21 @@ import { Badge, Button, Input, InputArea, Label, Select, buttonVariants } from "@cloudflare/kumo"; +import { + DndContext, + closestCenter, + type DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { ArrowLeft, Plus, @@ -114,7 +131,7 @@ export function ContentTypeEditor({ onAddField, onUpdateField, onDeleteField, - onReorderFields: _onReorderFields, + onReorderFields, }: ContentTypeEditorProps) { const _navigate = useNavigate(); @@ -268,6 +285,21 @@ export function ContentTypeEditor({ const isFromCode = collection?.source === "code"; const fields = collection?.fields ?? []; + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + const oldIndex = fields.findIndex((f) => f.id === active.id); + const newIndex = fields.findIndex((f) => f.id === over.id); + if (oldIndex === -1 || newIndex === -1) return; + const reordered = arrayMove(fields, oldIndex, newIndex); + onReorderFields?.(reordered.map((f) => f.slug)); + }; + return (
{/* Header */} @@ -561,17 +593,28 @@ export function ContentTypeEditor({
Custom Fields
-
- {fields.map((field) => ( - handleEditField(field)} - onDelete={() => setDeleteFieldTarget(field)} - /> - ))} -
+ + f.id)} + strategy={verticalListSortingStrategy} + > +
+ {fields.map((field) => ( + handleEditField(field)} + onDelete={() => setDeleteFieldTarget(field)} + /> + ))} +
+
+
)}
@@ -620,9 +663,31 @@ interface FieldRowProps { } function FieldRow({ field, isFromCode, onEdit, onDelete }: FieldRowProps) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: field.id, + disabled: isFromCode, + }); + const style = { transform: CSS.Transform.toString(transform), transition }; + return ( -
- {!isFromCode && } +
+ {!isFromCode && ( + + )}
{field.label} diff --git a/packages/admin/src/router.tsx b/packages/admin/src/router.tsx index c8ffb58..e88a2dd 100644 --- a/packages/admin/src/router.tsx +++ b/packages/admin/src/router.tsx @@ -75,6 +75,7 @@ import { createField, updateField, deleteField, + reorderFields, fetchOrphanedTables, registerOrphanedTable, fetchUsers, @@ -1456,6 +1457,16 @@ function ContentTypesEditPage() { }, }); + const reorderFieldsMutation = useMutation({ + mutationFn: (fieldSlugs: string[]) => reorderFields(slug, fieldSlugs), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["schema", "collections", slug], + }); + void queryClient.invalidateQueries({ queryKey: ["manifest"] }); + }, + }); + if (error) { return ; } @@ -1472,6 +1483,7 @@ function ContentTypesEditPage() { onAddField={(input) => addFieldMutation.mutateAsync(input)} onUpdateField={(fieldSlug, input) => updateFieldMutation.mutateAsync({ fieldSlug, input })} onDeleteField={(fieldSlug) => deleteFieldMutation.mutate(fieldSlug)} + onReorderFields={(fieldSlugs) => reorderFieldsMutation.mutate(fieldSlugs)} /> ); }