diff --git a/app/hooks/useDashboardReorder.tsx b/app/hooks/useDashboardReorder.tsx
new file mode 100644
index 0000000..652c9fd
--- /dev/null
+++ b/app/hooks/useDashboardReorder.tsx
@@ -0,0 +1,110 @@
+import {
+ cloneElement,
+ useCallback,
+ useRef,
+ useState,
+} from 'react';
+import { DragDropLineIcon } from '@ifrc-go/icons';
+import { createElementColumn } from '@ifrc-go/ui/utils';
+
+import { useBulkUpdateExternalDashboardsMutation } from '#generated/types/graphql';
+import useAlert from '#hooks/useAlert';
+import { errorMessage } from '#utils/common';
+
+// eslint-disable-next-line react-refresh/only-export-components
+function DragHandleCell() {
+ return ;
+}
+
+export function createDragHandleColumn() {
+ return createElementColumn(
+ 'dragHandle',
+ '',
+ DragHandleCell,
+ () => ({}),
+ { columnWidth: 40 },
+ );
+}
+
+interface ReorderItem {
+ id: string;
+ no: string;
+ order: number;
+}
+
+function useDashboardReorder(
+ serverData: T[],
+ page: number,
+ limit: number,
+) {
+ const alert = useAlert();
+ const [, bulkUpdateExternalDashboards] = useBulkUpdateExternalDashboardsMutation();
+
+ const [tableData, setTableData] = useState(serverData);
+ const [prevServerData, setPrevServerData] = useState(serverData);
+ if (serverData !== prevServerData) {
+ setPrevServerData(serverData);
+ setTableData(serverData);
+ }
+
+ const dragIndexRef = useRef(undefined);
+
+ const handleRowDrop = useCallback((dropIndex: number) => {
+ const dragIndex = dragIndexRef.current;
+ dragIndexRef.current = undefined;
+ if (dragIndex === undefined || dragIndex === dropIndex) {
+ return;
+ }
+ const newData = [...tableData];
+ const [moved] = newData.splice(dragIndex, 1);
+ newData.splice(dropIndex, 0, moved);
+ const reorderedData = newData.map((item, index) => ({
+ ...item,
+ no: String((page - 1) * limit + index + 1),
+ order: (page - 1) * limit + index + 1,
+ }));
+ setTableData(reorderedData);
+ bulkUpdateExternalDashboards({
+ data: reorderedData.map((item) => ({
+ id: item.id,
+ order: item.order,
+ })),
+ }).then((resp) => {
+ const result = resp.data?.bulkUpdateExternalDashboards;
+ if (result?.ok) {
+ alert.show('Dashboard order updated successfully', { variant: 'success' });
+ } else {
+ setTableData(serverData);
+ alert.show(errorMessage, { variant: 'danger' });
+ }
+ }).catch(() => {
+ setTableData(serverData);
+ alert.show(errorMessage, { variant: 'danger' });
+ });
+ }, [tableData, serverData, page, limit, bulkUpdateExternalDashboards, alert]);
+
+ const rowModifier = useCallback(({ row, datum }: {
+ row: React.ReactElement;
+ datum: T;
+ }) => {
+ const index = tableData.indexOf(datum);
+ return cloneElement(row, {
+ draggable: true,
+ style: { cursor: 'grab' },
+ title: 'Drag to reorder',
+ onDragStart: () => {
+ dragIndexRef.current = index;
+ },
+ onDragOver: (e: React.DragEvent) => {
+ e.preventDefault();
+ },
+ onDrop: () => {
+ handleRowDrop(index);
+ },
+ } as React.HTMLAttributes);
+ }, [tableData, handleRowDrop]);
+
+ return { tableData, rowModifier };
+}
+
+export default useDashboardReorder;
diff --git a/app/queries.ts b/app/queries.ts
new file mode 100644
index 0000000..086a3e8
--- /dev/null
+++ b/app/queries.ts
@@ -0,0 +1,13 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { gql } from 'urql';
+
+const BULK_UPDATE_EXTERNAL_DASHBOARDS = gql`
+ mutation BulkUpdateExternalDashboards($data: [ExternalDashboardOrderInput!]!) {
+ bulkUpdateExternalDashboards(data: $data) {
+ ... on ExternalDashboardTypeListMutationResponseType {
+ ok
+ errors
+ }
+ }
+ }
+`;
diff --git a/app/views/CapacityAndResources/ResourceDashboards/index.tsx b/app/views/CapacityAndResources/ResourceDashboards/index.tsx
index 8370a2e..79e929b 100644
--- a/app/views/CapacityAndResources/ResourceDashboards/index.tsx
+++ b/app/views/CapacityAndResources/ResourceDashboards/index.tsx
@@ -21,12 +21,14 @@ import StatusCell from '#components/StatusCell';
import {
AdminAreaLevel,
type ExternalDashboardFilter,
+ Ordering,
type ResourceDashboardsQuery,
useCapacityAndResourceDetailQuery,
useDeleteResourceDashboardMutation,
useResourceDashboardsQuery,
} from '#generated/types/graphql';
import useAlert from '#hooks/useAlert';
+import useDashboardReorder, { createDragHandleColumn } from '#hooks/useDashboardReorder';
import useFilterState from '#hooks/useFilterState';
import useRegionMap from '#hooks/useRegionMap';
import useRouting from '#hooks/useRouting';
@@ -70,7 +72,7 @@ function ResourceDashboards() {
const regionMap = useRegionMap(AdminAreaLevel.Region);
- const [{ data: detailData, fetching: detailFetching }] = useCapacityAndResourceDetailQuery({
+ const [{ data: detailData }] = useCapacityAndResourceDetailQuery({
variables: { id: id ?? '' },
pause: !id,
});
@@ -88,17 +90,20 @@ function ResourceDashboards() {
limit,
offset,
},
+ order: { order: Ordering.Asc },
},
pause: !id,
});
- const tableData: DashboardListItem[] = useMemo(() => (
+ const serverData: DashboardListItem[] = useMemo(() => (
(data?.externalDashboards?.results ?? []).map((dashboard, index) => ({
...dashboard,
no: String((page - 1) * limit + index + 1),
}))
), [page, data, limit]);
+ const { tableData, rowModifier } = useDashboardReorder(serverData, page, limit);
+
const onDeleteClick = useCallback(
(dashboardId: string) => {
deleteResourceDashboard({ id: dashboardId }).then((resp) => {
@@ -117,6 +122,7 @@ function ResourceDashboards() {
);
const columns = useMemo(() => [
+ createDragHandleColumn(),
createStringColumn(
'no',
'No.',
@@ -137,6 +143,11 @@ function ResourceDashboards() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
+ createStringColumn(
+ 'order',
+ 'Display Order',
+ (item) => (isDefined(item.order) ? String(item.order) : '-'),
+ ),
createElementColumn(
'status',
'Status',
@@ -152,7 +163,7 @@ function ResourceDashboards() {
(_, datum) => ({
id: id ?? '',
dashboard: datum.id,
- onDelete: onDeleteClick,
+ onDelete: () => onDeleteClick(datum.id),
itemTitle: datum.title,
to: 'editResourceDashboard',
}),
@@ -169,7 +180,9 @@ function ResourceDashboards() {
return (
);
diff --git a/app/views/CapacityAndResources/query.ts b/app/views/CapacityAndResources/query.ts
index 14664e3..bcc2031 100644
--- a/app/views/CapacityAndResources/query.ts
+++ b/app/views/CapacityAndResources/query.ts
@@ -74,8 +74,8 @@ const UPDATE_CAPACITY_AND_RESOURCE = gql`
`;
const RESOURCE_DASHBOARDS = gql`
- query ResourceDashboards($pagination: OffsetPaginationInput, $filters: ExternalDashboardFilter) {
- externalDashboards(pagination: $pagination, filters: $filters) {
+ query ResourceDashboards($pagination: OffsetPaginationInput, $filters: ExternalDashboardFilter, $order: ExternalDashboardOrder) {
+ externalDashboards(pagination: $pagination, filters: $filters, order: $order) {
results {
id
title
diff --git a/app/views/OurWorks/index.tsx b/app/views/OurWorks/index.tsx
index e597d92..fed9a07 100644
--- a/app/views/OurWorks/index.tsx
+++ b/app/views/OurWorks/index.tsx
@@ -26,6 +26,7 @@ import {
useExternalDashboardsQuery,
} from '#generated/types/graphql';
import useAlert from '#hooks/useAlert';
+import useDashboardReorder, { createDragHandleColumn } from '#hooks/useDashboardReorder';
import useFilterState from '#hooks/useFilterState';
import useRegionMap from '#hooks/useRegionMap';
import useRouting from '#hooks/useRouting';
@@ -92,7 +93,7 @@ function OurWorks() {
});
const [, deleteExternalDashboard] = useDeleteExternalDashboardMutation();
- const tableData = useMemo(() => (
+ const serverData = useMemo(() => (
(data?.externalDashboards?.results ?? []).map((dashboard, index) => {
const no = (page - 1) * limit + index + 1;
return {
@@ -101,6 +102,8 @@ function OurWorks() {
};
}) as unknown as WorksListItem[]), [page, data, limit]);
+ const { tableData, rowModifier } = useDashboardReorder(serverData, page, limit);
+
const onDeleteClick = useCallback(
(id: string) => {
deleteExternalDashboard({ id }).then((resp) => {
@@ -119,6 +122,7 @@ function OurWorks() {
);
const columns = useMemo(() => [
+ createDragHandleColumn(),
createStringColumn(
'no',
'No.',
@@ -139,6 +143,11 @@ function OurWorks() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
+ createStringColumn(
+ 'order',
+ 'Display Order',
+ (item) => (isDefined(item.order) ? String(item.order) : '-'),
+ ),
createElementColumn(
'status',
'Status',
@@ -201,6 +210,7 @@ function OurWorks() {
data={tableData}
filtered={filtered}
pending={fetching}
+ rowModifier={rowModifier}
/>
);
diff --git a/app/views/Preparedness/index.tsx b/app/views/Preparedness/index.tsx
index ef6a0a1..09eae66 100644
--- a/app/views/Preparedness/index.tsx
+++ b/app/views/Preparedness/index.tsx
@@ -26,6 +26,7 @@ import {
usePreparednessExternalDashboardsQuery,
} from '#generated/types/graphql';
import useAlert from '#hooks/useAlert';
+import useDashboardReorder, { createDragHandleColumn } from '#hooks/useDashboardReorder';
import useFilterState from '#hooks/useFilterState';
import useRegionMap from '#hooks/useRegionMap';
import useRouting from '#hooks/useRouting';
@@ -92,13 +93,15 @@ function PreparednessList() {
});
const [, deleteExternalDashboard] = usePreparednessDeleteExternalDashboardMutation();
- const tableData: PreparednessListItem[] = useMemo(() => (
+ const serverData: PreparednessListItem[] = useMemo(() => (
(data?.externalDashboards?.results ?? []).map((dashboard, index) => ({
...dashboard,
no: String((page - 1) * limit + index + 1),
}))
), [page, data, limit]);
+ const { tableData, rowModifier } = useDashboardReorder(serverData, page, limit);
+
const onDeleteClick = useCallback(
(id: string) => {
deleteExternalDashboard({ id }).then((resp) => {
@@ -117,6 +120,7 @@ function PreparednessList() {
);
const columns = useMemo(() => [
+ createDragHandleColumn(),
createStringColumn(
'no',
'No.',
@@ -137,6 +141,11 @@ function PreparednessList() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
+ createStringColumn(
+ 'order',
+ 'Display Order',
+ (item) => (isDefined(item.order) ? String(item.order) : '-'),
+ ),
createElementColumn(
'status',
'Status',
@@ -199,6 +208,7 @@ function PreparednessList() {
data={tableData}
filtered={filtered}
pending={fetching}
+ rowModifier={rowModifier}
/>
);
diff --git a/backend b/backend
index cca87cb..f8cb4e9 160000
--- a/backend
+++ b/backend
@@ -1 +1 @@
-Subproject commit cca87cbf1b50ad0436a34efe48e74a3cb17a01b3
+Subproject commit f8cb4e979b3139696d59849216891c10541b5d59