diff --git a/src/features/views/components/ViewDataTable/index.tsx b/src/features/views/components/ViewDataTable/index.tsx index 22f5883088..0c313b9486 100644 --- a/src/features/views/components/ViewDataTable/index.tsx +++ b/src/features/views/components/ViewDataTable/index.tsx @@ -53,6 +53,7 @@ import { } from 'utils/types/zetkin'; import messageIds from 'features/views/l10n/messageIds'; +import useDebounce from 'utils/hooks/useDebounce'; declare module '@mui/x-data-grid-pro' { interface ColumnMenuPropsOverrides { @@ -314,6 +315,27 @@ const ViewDataTable: FunctionComponent = ({ width: 50, }; + const debouncedUpdateColumnOrder = useDebounce((order: number[]) => { + return model.updateColumnOrder(order); + }, 1000); + + const moveColumn = (field: string, targetIndex: number) => { + // The column index is offset by 2 compared to the API (avatar and checkbox) + targetIndex -= 2; + const columnId = colIdFromFieldName(field); + const origIndex = columns.findIndex((col) => col.id == columnId); + const columnOrder = columns.map((col) => col.id); + + // Remove column and place it in new location + columnOrder.splice(origIndex, 1); + const newColumnOrder = [ + ...columnOrder.slice(0, targetIndex), + columnId, + ...columnOrder.slice(targetIndex), + ]; + debouncedUpdateColumnOrder(newColumnOrder); + }; + const unConfiguredGridColumns = [ avatarColumn, ...columns.map((col) => ({ @@ -329,6 +351,7 @@ const ViewDataTable: FunctionComponent = ({ ...columnTypes[col.type].getColDef(col, accessLevel), })), ]; + const { columns: gridColumns, setColumnWidth } = useConfigurableDataGridColumns('viewInstances', unConfiguredGridColumns); @@ -457,6 +480,9 @@ const ViewDataTable: FunctionComponent = ({ } } }} + onColumnOrderChange={(params) => { + moveColumn(params.column.field, params.targetIndex); + }} onColumnResize={(params) => { setColumnWidth(params.colDef.field, params.width); }} diff --git a/src/features/views/models/ViewDataModel.ts b/src/features/views/models/ViewDataModel.ts index a6b21513a0..8498d34849 100644 --- a/src/features/views/models/ViewDataModel.ts +++ b/src/features/views/models/ViewDataModel.ts @@ -88,6 +88,10 @@ export default class ViewDataModel extends ModelBase { return this._repo.updateColumn(this._orgId, this._viewId, columnId, data); } + updateColumnOrder(columnOrder: number[]): Promise { + return this._repo.updateColumnOrder(this._orgId, this._viewId, columnOrder); + } + updateContentQuery(query: Pick) { return this._repo.updateViewContentQuery(this._orgId, this._viewId, query); } diff --git a/src/features/views/repos/ViewDataRepo.ts b/src/features/views/repos/ViewDataRepo.ts index b7fc189105..190e9c748f 100644 --- a/src/features/views/repos/ViewDataRepo.ts +++ b/src/features/views/repos/ViewDataRepo.ts @@ -7,6 +7,7 @@ import { cellUpdated, columnAdded, columnDeleted, + columnOrderUpdated, columnsLoad, columnsLoaded, columnUpdated, @@ -174,6 +175,18 @@ export default class ViewDataRepo { this._store.dispatch(columnUpdated([viewId, column])); } + async updateColumnOrder( + orgId: number, + viewId: number, + columnOrder: number[] + ) { + await this._apiClient.patch<{ order: number[] }>( + `/api/orgs/${orgId}/people/views/${viewId}/column_order`, + { order: columnOrder } + ); + this._store.dispatch(columnOrderUpdated([viewId, columnOrder])); + } + updateView( orgId: number, viewId: number, diff --git a/src/features/views/store.ts b/src/features/views/store.ts index 49c29e0c8d..e3f3882b42 100644 --- a/src/features/views/store.ts +++ b/src/features/views/store.ts @@ -2,6 +2,7 @@ import { Call } from 'features/callAssignments/apiTypes'; import { callUpdated } from 'features/callAssignments/store'; import columnTypes from './components/ViewDataTable/columnTypes'; import { DeleteFolderReport } from './rpc/deleteFolder'; +import notEmpty from 'utils/notEmpty'; import { ViewTreeData } from 'pages/api/views/tree'; import { ZetkinObjectAccess } from 'core/api/types'; import { @@ -152,12 +153,10 @@ const viewsSlice = createSlice({ } }, columnAdded: (state, action: PayloadAction<[number, ZetkinViewColumn]>) => { - const [viewId, column] = action.payload; + const [viewId] = action.payload; const colList = state.columnsByViewId[viewId]; if (colList) { - colList.items = colList.items.concat([ - remoteItem(column.id, { data: column }), - ]); + colList.isStale = true; const rowList = state.rowsByViewId[viewId]; if (rowList) { @@ -180,6 +179,48 @@ const viewsSlice = createSlice({ } } }, + columnOrderUpdated: (state, action: PayloadAction<[number, number[]]>) => { + const [viewId, columnOrder] = action.payload; + // Re-arrange columns + const colList = state.columnsByViewId[viewId]; + if (colList) { + const newColListItems = columnOrder + .map((colId) => { + const col = colList.items.find((col) => col.id == colId); + if (col) { + return col; + } else { + colList.isStale = true; + } + }) + .filter(notEmpty); + + // Re-arrange columns of data-rows + const rowList = state.rowsByViewId[viewId]; + if (rowList) { + const newRowListItems = rowList.items.map((row) => { + if (row.data) { + return { + ...row, + data: { + content: columnOrder.map((colId) => { + const idx = colList.items.findIndex( + (col) => col.id == colId + )!; + return row.data?.content[idx]; + }), + id: row.data.id, + }, + }; + } else { + return row; + } + }); + state.columnsByViewId[viewId].items = newColListItems; + state.rowsByViewId[viewId].items = newRowListItems; + } + } + }, columnUpdated: ( state, action: PayloadAction<[number, ZetkinViewColumn]> @@ -484,6 +525,7 @@ export const { cellUpdated, columnAdded, columnDeleted, + columnOrderUpdated, columnUpdated, columnsLoad, columnsLoaded,