Skip to content

Commit f98f696

Browse files
os-zhuangclaude
andauthored
feat(components,app-shell): enable column drag-reorder in Studio + persist field order (#2092)
The data-table already implements column drag-reorder; this enables it in the Studio Data grid (design mode) and persists the new order to the object's FIELD METADATA (field display order = metadata order), per spec — no drag re-implemented. - GridFieldAuthoring context gains onReorderFields(orderedFieldNames). Providing it enables the table's built-in reorder (reorderEnabled) and routes the drop's new column order to the host. - data-table calls fieldAuthoring.onReorderFields on column drop (alongside its existing onColumnsReorder) and gates the drag affordances on reorderEnabled. - DataPillar reorders the object's fields to match — keeping system/hidden fields (not shown as columns) in place, reordering only the visible fields among their slots — then save + publish + remount. Verified live (showcase_account): drag Industry to the front → effective field order [owner_id,name,industry,...] → [industry,owner_id,name,...]; system fields (organization_id/created_at/...) stay first; persisted to the env overlay. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 186f5a1 commit f98f696

3 files changed

Lines changed: 56 additions & 8 deletions

File tree

packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,40 @@ function DataPillar({ packageId }: { packageId: string }): React.ReactElement {
731731
}
732732
}, [adapter, client, current]);
733733

734+
// Drag-reorder columns → reorder the object's `fields` metadata (field display
735+
// order follows metadata order), then publish so the new order persists.
736+
const doReorderFields = React.useCallback(
737+
async (orderedNames: string[]) => {
738+
if (!current) return;
739+
const view = readFields(objDraft.fields);
740+
// Reorder only the visible fields among their own slots; keep system /
741+
// hidden fields (not shown as columns) in their original positions.
742+
const visible = new Set(orderedNames);
743+
const visibleInOrder = orderedNames
744+
.map((n) => view.entries.find((e) => e.name === n))
745+
.filter((e): e is (typeof view.entries)[number] => Boolean(e));
746+
let vi = 0;
747+
const entries = view.entries.map((e) => (visible.has(e.name) ? visibleInOrder[vi++] : e));
748+
const body = { ...objDraft, fields: writeFields({ ...view, entries }) };
749+
setObjDraft(body);
750+
setSaving('publish');
751+
setError(null);
752+
try {
753+
await client.save('object', current.name, body, { mode: 'draft' });
754+
await client.publish('object', current.name);
755+
(adapter as { clearCache?: () => void } | null)?.clearCache?.();
756+
setHasDraft(false);
757+
setDirty(false);
758+
setGridVer((v) => v + 1); // remount so the grid reflects the persisted order
759+
} catch (e) {
760+
setError(e instanceof Error ? e.message : String(e));
761+
} finally {
762+
setSaving(false);
763+
}
764+
},
765+
[client, current, objDraft, adapter],
766+
);
767+
734768
const inspector = getMetadataInspector('object');
735769

736770
return (
@@ -799,6 +833,7 @@ function DataPillar({ packageId }: { packageId: string }): React.ReactElement {
799833
}
800834
},
801835
editColumnLabel: '编辑字段属性',
836+
onReorderFields: doReorderFields,
802837
}}
803838
>
804839
<SchemaRenderer

packages/components/src/context/gridFieldAuthoring.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ export interface GridFieldAuthoring {
3333
onEditColumn?: (fieldName: string) => void;
3434
/** Optional tooltip/aria-label for the edit-field button (defaults to "Edit field"). */
3535
editColumnLabel?: string;
36+
/**
37+
* Invoked when the user drag-reorders columns. Receives the new column order
38+
* as accessorKeys (= field names, including any non-field columns). Providing
39+
* this also ENABLES the table's built-in column drag-reorder (design mode), so
40+
* the host can persist the order to the object's field metadata. Omit to leave
41+
* reordering to the table's own `reorderableColumns`/`onColumnsReorder`.
42+
*/
43+
onReorderFields?: (orderedFieldNames: string[]) => void;
3644
}
3745

3846
const GridFieldAuthoringContext = React.createContext<GridFieldAuthoring | null>(null);

packages/components/src/renderers/complex/data-table.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
238238
const fieldAuthoring = useGridFieldAuthoring();
239239
const addColumnEnabled = !!fieldAuthoring?.onAddColumn;
240240
const editColumnEnabled = !!fieldAuthoring?.onEditColumn;
241+
// The table already implements column drag-reorder; a design host enables it by
242+
// providing onReorderFields (to persist the order to the object's field metadata).
243+
const reorderEnabled = reorderableColumns || !!fieldAuthoring?.onReorderFields;
241244

242245
// i18n support for pagination labels
243246
const { t, language } = useTableTranslation();
@@ -562,22 +565,22 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
562565

563566
// Column reordering handlers
564567
const handleColumnDragStart = (e: React.DragEvent, index: number) => {
565-
if (!reorderableColumns) return;
568+
if (!reorderEnabled) return;
566569
setDraggedColumn(index);
567570
e.dataTransfer.effectAllowed = 'move';
568571
};
569572

570573
const handleColumnDragOver = (e: React.DragEvent, index: number) => {
571-
if (!reorderableColumns) return;
574+
if (!reorderEnabled) return;
572575
e.preventDefault();
573576
e.dataTransfer.dropEffect = 'move';
574577
setDragOverColumn(index);
575578
};
576579

577580
const handleColumnDrop = (e: React.DragEvent, dropIndex: number) => {
578-
if (!reorderableColumns || draggedColumn === null) return;
581+
if (!reorderEnabled || draggedColumn === null) return;
579582
e.preventDefault();
580-
583+
581584
if (draggedColumn === dropIndex) {
582585
setDraggedColumn(null);
583586
setDragOverColumn(null);
@@ -587,15 +590,17 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
587590
const newColumns = [...columns];
588591
const [removed] = newColumns.splice(draggedColumn, 1);
589592
newColumns.splice(dropIndex, 0, removed);
590-
593+
591594
setColumns(newColumns);
592595
setDraggedColumn(null);
593596
setDragOverColumn(null);
594-
597+
595598
// Call callback if provided
596599
if (schema.onColumnsReorder) {
597600
schema.onColumnsReorder(newColumns);
598601
}
602+
// Design host: persist the new order to the object's field metadata.
603+
fieldAuthoring?.onReorderFields?.(newColumns.map((c) => c.accessorKey));
599604
};
600605

601606
const handleColumnDragEnd = () => {
@@ -927,7 +932,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
927932
minWidth: columnWidth,
928933
...(isFrozen && { left: frozenOffset }),
929934
}}
930-
draggable={reorderableColumns}
935+
draggable={reorderEnabled}
931936
onDragStart={(e) => handleColumnDragStart(e, index)}
932937
onDragOver={(e) => handleColumnDragOver(e, index)}
933938
onDrop={(e) => handleColumnDrop(e, index)}
@@ -940,7 +945,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
940945
col.align === 'right' ? 'justify-end' : 'justify-between'
941946
)}>
942947
<div className="flex items-center gap-1">
943-
{reorderableColumns && (
948+
{reorderEnabled && (
944949
<GripVertical className="h-4 w-4 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing shrink-0" />
945950
)}
946951
{col.headerIcon && (

0 commit comments

Comments
 (0)