diff --git a/.circleci/config.yml b/.circleci/config.yml index eefbc793c..8b7af77db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,7 +223,7 @@ workflows: - CORE-635 - feat/review - feat/system-admin - - pm-1503_2 + - pm-1722_1 - deployQa: context: org-global 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/lib/components/ScorecardDetails/ScorecardDetails.tsx b/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx index ec28536fa..5acae01dd 100644 --- a/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx +++ b/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx @@ -165,7 +165,7 @@ export const ScorecardDetails: FC = (props: Props) => { (questionResult, question) => { let questionPoint = 0 const initialAnswer - = mapingResult[question.id] + = mapingResult[question.id as string] if ( question.type === 'YES_NO' && initialAnswer === 'Yes' @@ -338,7 +338,7 @@ export const ScorecardDetails: FC = (props: Props) => { ) => { const reviewItemInfo = mappingReviewInfo[ - question.id + question.id as string ] if ( !reviewItemInfo diff --git a/src/apps/review/src/lib/models/ScorecardQuestion.model.ts b/src/apps/review/src/lib/models/ScorecardQuestion.model.ts index ff06c7b2c..64ace68ed 100644 --- a/src/apps/review/src/lib/models/ScorecardQuestion.model.ts +++ b/src/apps/review/src/lib/models/ScorecardQuestion.model.ts @@ -2,7 +2,7 @@ * Scorecard question info */ export interface ScorecardQuestion { - id: string + id?: string type: 'SCALE' | 'YES_NO' | 'TEST_CASE' description: string guidelines: string @@ -10,4 +10,5 @@ export interface ScorecardQuestion { scaleMin: number scaleMax: number requiresUpload: boolean + sortOrder: number } diff --git a/src/apps/review/src/lib/models/ScorecardSection.model.ts b/src/apps/review/src/lib/models/ScorecardSection.model.ts index d9aea23d0..540dc32e4 100644 --- a/src/apps/review/src/lib/models/ScorecardSection.model.ts +++ b/src/apps/review/src/lib/models/ScorecardSection.model.ts @@ -4,7 +4,7 @@ import { ScorecardQuestion } from './ScorecardQuestion.model' * Scorcard section info */ export interface ScorecardSection { - id: string + id?: string name: string weight: number sortOrder: number diff --git a/src/apps/review/src/mock-datas/MockScorecard.ts b/src/apps/review/src/mock-datas/MockScorecard.ts index 17448e8dc..a22f2c7eb 100644 --- a/src/apps/review/src/mock-datas/MockScorecard.ts +++ b/src/apps/review/src/mock-datas/MockScorecard.ts @@ -36,6 +36,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 0, type: 'YES_NO', weight: 25, }, @@ -47,6 +48,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 1, type: 'YES_NO', weight: 25, }, @@ -57,6 +59,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 2, type: 'SCALE', weight: 25, }, @@ -67,6 +70,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 3, type: 'SCALE', weight: 25, }, @@ -86,6 +90,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 0, type: 'SCALE', weight: 50, }, @@ -97,6 +102,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 1, type: 'SCALE', weight: 50, }, @@ -124,6 +130,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 0, type: 'SCALE', weight: 50, }, @@ -135,6 +142,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 1, type: 'SCALE', weight: 50, }, @@ -154,6 +162,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 0, type: 'SCALE', weight: 50, }, @@ -166,6 +175,7 @@ export const MockScorecard: ScorecardInfo = { requiresUpload: true, scaleMax: 9, scaleMin: 1, + sortOrder: 1, type: 'SCALE', weight: 50, }, diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.module.scss b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.module.scss index fbf84dd2b..1f6641938 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.module.scss +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.module.scss @@ -88,6 +88,10 @@ .headerArea { background: #0F172A; color: $tc-white; + .title { + display: flex; + align-items: center; + } :global(.input-error) { font-family: "Nunito Sans", sans-serif; @@ -116,6 +120,11 @@ background: $teal-160; color: $tc-white; + .title { + display: flex; + align-items: center; + } + :global(.input-error) { font-family: "Nunito Sans", sans-serif; background: #E2CBC0; @@ -147,6 +156,11 @@ grid-template-columns: 1fr 7.85% 3.5%; gap: $sp-4; + .title { + display: flex; + align-items: center; + } + :global(.main-group) { grid-column: 1; } diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx index 22388fa34..f1872f009 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/EditScorecardPage.tsx @@ -1,16 +1,19 @@ +/* eslint-disable import/no-extraneous-dependencies */ import * as yup from 'yup' import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' -import { yupResolver } from '@hookform/resolvers/yup' import { Button, LinkButton } from '~/libs/ui' +import { DragDropContext, DropResult } from '@hello-pangea/dnd' +import { yupResolver } from '@hookform/resolvers/yup' +import { PageWrapper } from '../../../lib' import { useFetchScorecard } from '../../../lib/hooks/useFetchScorecard' import { saveScorecard } from '../../../lib/services' import { rootRoute } from '../../../config/routes.config' -import { PageWrapper } from '../../../lib' +import type { ScorecardQuestion, ScorecardSection } from '../../../lib/models' import { getEmptyScorecard } from './utils' import { EditScorecardPageContextProvider } from './EditScorecardPage.context' @@ -50,7 +53,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 +63,141 @@ 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]) + 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 + } + + 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, + } + } + + function 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) { + const newSections = reorder(groups[sourceGroupIndex].sections, source.index, destination.index) + newSections.forEach((section, index) => { + section.sortOrder = index + }) + groups[sourceGroupIndex].sections = newSections + } else { + const { + source: newSourceSections, + destination: newDestSections, + }: { source: ScorecardSection[], destination: ScorecardSection[]} = move( + groups[sourceGroupIndex].sections, + groups[destGroupIndex].sections, + source.index, + destination.index, + ) + + const movedSection = newDestSections[destination.index] + if (movedSection) { + delete movedSection.id + + movedSection.questions.forEach((question: any) => { + delete question.id + }) + } + + newSourceSections.forEach((section, index) => { + section.sortOrder = index + }) + + newDestSections.forEach((section, index) => { + section.sortOrder = index + }) + + groups[sourceGroupIndex].sections = newSourceSections + groups[destGroupIndex].sections = newDestSections + } + + editForm.setValue('scorecardGroups', groups, { shouldDirty: true, shouldValidate: true }) + } else if (type === 'question') { + const groups = editForm.getValues('scorecardGroups') + 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) { + const questions = groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions + const newQuestions = reorder(questions, source.index, destination.index) + newQuestions.forEach((question, index) => { + question.sortOrder = index + }) + groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions = newQuestions + } else { + const sourceQuestions = groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions + const destQuestions = groups[destIds.groupIndex].sections[destIds.sectionIndex].questions + const { + source: newSourceQuestions, + destination: newDestQuestions, + }: { source: ScorecardQuestion[], destination: ScorecardQuestion[]} = move( + sourceQuestions, + destQuestions, + source.index, + destination.index, + ) + + const movedQuestion = newDestQuestions[destination.index] + if (movedQuestion) { + delete movedQuestion.id + } + + newSourceQuestions.forEach((question, index) => { + question.sortOrder = index + }) + + newDestQuestions.forEach((question, index) => { + question.sortOrder = index + }) + + groups[sourceIds.groupIndex].sections[sourceIds.sectionIndex].questions = newSourceQuestions + groups[destIds.groupIndex].sections[destIds.sectionIndex].questions = newDestQuestions + } + + editForm.setValue('scorecardGroups', groups, { shouldDirty: true, shouldValidate: true }) + } + } + if (scorecardQuery.isValidating) { return <> } @@ -78,12 +210,13 @@ const EditScorecardPage: FC = () => { >
+ +

1. Scorecard Information

+ -

1. Scorecard Information

- - -

2. Evaluation Structure

- +

2. Evaluation Structure

+ +

diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/DragIcon.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/DragIcon.tsx new file mode 100644 index 000000000..9e9368431 --- /dev/null +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/DragIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +const DragIcon: React.FC = () => ( + + Draggable + + + + + + + +) + +export default DragIcon 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..f3f38a6c8 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardGroupForm.tsx @@ -1,9 +1,11 @@ +/* eslint-disable import/no-extraneous-dependencies */ import * as yup from 'yup' import { FC, useCallback } from 'react' import { useFieldArray, useFormContext } from 'react-hook-form' import classNames from 'classnames' import { Button } from '~/libs/ui' +import { Draggable, Droppable } from '@hello-pangea/dnd' import { TrashIcon } from '@heroicons/react/outline' import { usePageContext } from '../EditScorecardPage.context' @@ -11,6 +13,7 @@ import { getEmptyScorecardGroup, weightsSum } from '../utils' import styles from '../EditScorecardPage.module.scss' import CalculatedWeightsSum from './CalculatedWeightsSum' +import DragIcon from './DragIcon' import InputWrapper from './InputWrapper' import ScorecardSectionForm, { scorecardSectionSchema } from './ScorecardSectionForm' @@ -63,62 +66,86 @@ 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 => ( +
+
+
+ +
+ 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..cf5f481b4 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardQuestionForm.tsx @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import * as yup from 'yup' import { get } from 'lodash' import { ChangeEvent, ChangeEventHandler, FC, useCallback, useMemo } from 'react' @@ -5,6 +6,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form' import classNames from 'classnames' import { TrashIcon } from '@heroicons/react/outline' +import { Draggable, Droppable } from '@hello-pangea/dnd' import { Button } from '~/libs/ui' import { ScorecardScales } from '~/apps/review/src/lib/models' @@ -14,6 +16,7 @@ import styles from '../EditScorecardPage.module.scss' import BasicSelect from './BasicSelect' import CalculatedWeightsSum from './CalculatedWeightsSum' +import DragIcon from './DragIcon' import InputWrapper from './InputWrapper' const scorecardScaleOptions = Object.entries(ScorecardScales) @@ -100,119 +103,143 @@ 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} -
- - - - - - - - - -