Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions app/hooks/useDashboardReorder.tsx
Original file line number Diff line number Diff line change
@@ -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 <DragDropLineIcon title="Drag to reorder" />;
}

export function createDragHandleColumn<T>() {
return createElementColumn<T, string | number, object>(
'dragHandle',
'',
DragHandleCell,
() => ({}),
{ columnWidth: 40 },
);
}

interface ReorderItem {
id: string;
no: string;
order: number;
}

function useDashboardReorder<T extends ReorderItem>(
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<number | undefined>(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<HTMLElement>);
}, [tableData, handleRowDrop]);

return { tableData, rowModifier };
}

export default useDashboardReorder;
13 changes: 13 additions & 0 deletions app/queries.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
`;
24 changes: 19 additions & 5 deletions app/views/CapacityAndResources/ResourceDashboards/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
});
Expand All @@ -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) => {
Expand All @@ -117,6 +122,7 @@ function ResourceDashboards() {
);

const columns = useMemo(() => [
createDragHandleColumn<DashboardListItem>(),
createStringColumn<DashboardListItem, string | number>(
'no',
'No.',
Expand All @@ -137,6 +143,11 @@ function ResourceDashboards() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
createStringColumn<DashboardListItem, string | number>(
'order',
'Display Order',
(item) => (isDefined(item.order) ? String(item.order) : '-'),
),
createElementColumn<DashboardListItem, string | number, { isActive: boolean }>(
'status',
'Status',
Expand All @@ -152,7 +163,7 @@ function ResourceDashboards() {
(_, datum) => ({
id: id ?? '',
dashboard: datum.id,
onDelete: onDeleteClick,
onDelete: () => onDeleteClick(datum.id),
itemTitle: datum.title,
to: 'editResourceDashboard',
}),
Expand All @@ -169,7 +180,9 @@ function ResourceDashboards() {
return (
<Container
withPadding
heading={`${detailData?.capacityAndResource?.title} Dashboards`}
heading={isDefined(detailData?.capacityAndResource?.title)
? `${detailData.capacityAndResource.title} Dashboards`
: 'Dashboards'}
headerDescription="Track, organize, and update the dashboards for this capacity and resource"
filters={(
<ResourceDashboardsFilters
Expand Down Expand Up @@ -201,7 +214,8 @@ function ResourceDashboards() {
columns={columns}
data={tableData}
filtered={filtered}
pending={fetching || detailFetching}
pending={fetching}
rowModifier={rowModifier}
/>
</Container>
);
Expand Down
4 changes: 2 additions & 2 deletions app/views/CapacityAndResources/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion app/views/OurWorks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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) => {
Expand All @@ -119,6 +122,7 @@ function OurWorks() {
);

const columns = useMemo(() => [
createDragHandleColumn<WorksListItem>(),
createStringColumn<WorksListItem, string | number>(
'no',
'No.',
Expand All @@ -139,6 +143,11 @@ function OurWorks() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
createStringColumn<WorksListItem, string | number>(
'order',
'Display Order',
(item) => (isDefined(item.order) ? String(item.order) : '-'),
),
createElementColumn<WorksListItem, string | number, { isActive: boolean }>(
'status',
'Status',
Expand Down Expand Up @@ -201,6 +210,7 @@ function OurWorks() {
data={tableData}
filtered={filtered}
pending={fetching}
rowModifier={rowModifier}
/>
</Container>
);
Expand Down
12 changes: 11 additions & 1 deletion app/views/Preparedness/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand All @@ -117,6 +120,7 @@ function PreparednessList() {
);

const columns = useMemo(() => [
createDragHandleColumn<PreparednessListItem>(),
createStringColumn<PreparednessListItem, string | number>(
'no',
'No.',
Expand All @@ -137,6 +141,11 @@ function PreparednessList() {
'Region',
(item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'),
),
createStringColumn<PreparednessListItem, string | number>(
'order',
'Display Order',
(item) => (isDefined(item.order) ? String(item.order) : '-'),
),
createElementColumn<PreparednessListItem, string | number, { isActive: boolean }>(
'status',
'Status',
Expand Down Expand Up @@ -199,6 +208,7 @@ function PreparednessList() {
data={tableData}
filtered={filtered}
pending={fetching}
rowModifier={rowModifier}
/>
</Container>
);
Expand Down
2 changes: 1 addition & 1 deletion backend
Loading