From 7ecba715dc50dc577d31e858f2190b861c372e97 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 27 Aug 2025 19:44:52 +0200 Subject: [PATCH 1/9] fix: drag and drop implementation --- package.json | 1 + .../EditScorecardPage/EditScorecardPage.tsx | 113 ++++++++- .../components/ScorecardGroupForm.tsx | 126 ++++++---- .../components/ScorecardQuestionForm.tsx | 238 ++++++++++-------- .../components/ScorecardSectionForm.tsx | 126 ++++++---- yarn.lock | 64 +++-- 6 files changed, 421 insertions(+), 247 deletions(-) diff --git a/package.json b/package.json index b4e1046d0..9c7f92812 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@datadog/browser-logs": "^4.21.2", + "@hello-pangea/dnd": "^18.0.1", "@heroicons/react": "^1.0.6", "@hookform/resolvers": "^4.1.2", "@popperjs/core": "^2.11.8", diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx index 22388fa34..6ee37bb25 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx @@ -4,13 +4,14 @@ import { FormProvider, useForm } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' +import { DragDropContext, DropResult } from '@hello-pangea/dnd' import { yupResolver } from '@hookform/resolvers/yup' import { Button, LinkButton } from '~/libs/ui' -import { useFetchScorecard } from '../../../lib/hooks/useFetchScorecard' import { saveScorecard } from '../../../lib/services' import { rootRoute } from '../../../config/routes.config' import { PageWrapper } from '../../../lib' +import { useFetchScorecard } from '../../../lib/hooks/useFetchScorecard' import { getEmptyScorecard } from './utils' import { EditScorecardPageContextProvider } from './EditScorecardPage.context' @@ -50,7 +51,7 @@ const EditScorecardPage: FC = () => { } }, [scorecardQuery.scorecard, scorecardQuery.isValidating]) - const handleSubmit = useCallback(async (value: any) => { + const handleSubmit = useCallback(async (value: any): Promise => { setSaving(true) try { const response = await saveScorecard(value) @@ -60,12 +61,103 @@ const EditScorecardPage: FC = () => { } } catch (e: any) { toast.error(`Couldn't save scorecard! ${e.message}`) - console.error('Couldn\'t save scorecard!', e) + console.error("Couldn't save scorecard!", e) } finally { setSaving(false) } }, [params.scorecardId, navigate]) + // Helper function to reorder array items + const reorder = (list: any[], startIndex: number, endIndex: number): any[] => { + const result = Array.from(list) + const [removed] = result.splice(startIndex, 1) + result.splice(endIndex, 0, removed) + return result + } + + // Helper function to move item between arrays + const move = ( + source: any[], + destination: any[], + sourceIndex: number, + destinationIndex: number, + ): { source: any[]; destination: any[] } => { + const sourceClone = Array.from(source) + const destClone = Array.from(destination) + const [removed] = sourceClone.splice(sourceIndex, 1) + destClone.splice(destinationIndex, 0, removed) + return { + destination: destClone, + source: sourceClone, + } + } + + const onDragEnd = (result: DropResult): void => { + if (!result.destination) return + + const { source, destination, type }: DropResult = result + + if (type === 'group') { + const newGroups = reorder(editForm.getValues('scorecardGroups'), source.index, destination.index) + editForm.setValue('scorecardGroups', newGroups, { shouldDirty: true, shouldValidate: true }) + } else if (type === 'section') { + const groups = editForm.getValues('scorecardGroups') + const sourceGroupIndex = Number(source.droppableId.split('.')[1]) + const destGroupIndex = Number(destination.droppableId.split('.')[1]) + + if (sourceGroupIndex === destGroupIndex) { + // Reorder sections within the same group + const newSections = reorder(groups[sourceGroupIndex].sections, source.index, destination.index) + groups[sourceGroupIndex].sections = newSections + } else { + // Move section between groups + const { source: newSourceSections, destination: newDestSections } = move( + groups[sourceGroupIndex].sections, + groups[destGroupIndex].sections, + source.index, + destination.index, + ) + groups[sourceGroupIndex].sections = newSourceSections + groups[destGroupIndex].sections = newDestSections + } + editForm.setValue('scorecardGroups', groups, { shouldValidate: true, shouldDirty: true }) + } else if (type === 'question') { + const groups = editForm.getValues('scorecardGroups') + + // Parse droppableIds: format is like "scorecardGroups.0.sections.1.questions" + const parseDroppableId = (id: string): { groupIndex: number; sectionIndex: number } => { + const parts = id.split('.') + return { + groupIndex: Number(parts[1]), + sectionIndex: Number(parts[3]), + } + } + + const sourceIds = parseDroppableId(source.droppableId) + const destIds = parseDroppableId(destination.droppableId) + + if (sourceIds.groupIndex === destIds.groupIndex && sourceIds.sectionIndex === destIds.sectionIndex) { + // Reorder questions within the same section + const questions = groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions + const newQuestions = reorder(questions, source.index, destination.index) + groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions = newQuestions + } else { + // Move question between sections + const sourceQuestions = groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions + const destQuestions = groups[destIds.groupIndex].sections[destIds.sectionIndex].questions + const { source: newSourceQuestions, destination: newDestQuestions } = move( + sourceQuestions, + destQuestions, + source.index, + destination.index, + ) + groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions = newSourceQuestions + groups[destIds.groupIndex].sections[destIds.sectionIndex].questions = newDestQuestions + } + editForm.setValue('scorecardGroups', groups, { shouldValidate: true, shouldDirty: true }) + } + } + if (scorecardQuery.isValidating) { return <> } @@ -78,20 +170,21 @@ const EditScorecardPage: FC = () => { >
+ +

1. Scorecard Information

+ -

1. Scorecard Information

- - -

2. Evaluation Structure

- +

2. Evaluation Structure

+ +

- + Cancel -
diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx index 49ca3a4ab..9c4d3c089 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx @@ -1,5 +1,6 @@ import * as yup from 'yup' import { FC, useCallback } from 'react' +import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd' import { useFieldArray, useFormContext } from 'react-hook-form' import classNames from 'classnames' @@ -34,6 +35,7 @@ export const scorecardGroupSchema = { .test(...weightsSum('groups')), } + const ScorecardGroupForm: FC = () => { const form = useFormContext() const ctx = usePageContext() @@ -63,62 +65,80 @@ const ScorecardGroupForm: FC = () => { }, [formGroupsArray]) return ( -
- {!formGroupsArray.fields.length && ( -
At least one group is required
- )} - {formGroupsArray.fields.map((groupField, index) => ( -
-
-
- Group - {' '} - {index + 1} -
-
- - - - - - - + {(provided) => ( +
+ {!formGroupsArray.fields.length && ( +
At least one group is required
+ )} + {formGroupsArray.fields.map((groupField, index) => ( + + {(draggableProvided, snapshot) => ( +
+
+
+ Group {index + 1} +
+
+ + + + + + + +
+
+
+ +
+
+ )} +
+ ))} + {provided.placeholder} +
+ + {formGroupsArray.fields.length > 0 && ( + -
-
-
- + )}
- ))} -
- - - {formGroupsArray.fields.length > 0 && ( - - )} -
-
+ )} + ) } diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx index 6997d638b..e56be9f93 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx @@ -1,6 +1,7 @@ import * as yup from 'yup' import { get } from 'lodash' import { ChangeEvent, ChangeEventHandler, FC, useCallback, useMemo } from 'react' +import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd' import { useFieldArray, useFormContext } from 'react-hook-form' import classNames from 'classnames' @@ -57,6 +58,7 @@ interface ScorecardQuestionFormProps { prefix: string; } + const ScorecardQuestionForm: FC = props => { const form = useFormContext() const ctx = usePageContext() @@ -100,119 +102,137 @@ const ScorecardQuestionForm: FC = props => { }, [form]) return ( -
- {!formQuestionsArray.fields.length && ( -
At least one question is required
- )} - {formQuestionsArray.fields.map((questionField, index) => ( -
-
- Question - {' '} - {props.sectionIndex} - . - {index + 1} -
- - - - - - - - - -