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)}
/>
);
}