From 23c8d5be1c6e52d4390b8c4108fe103ede6a3882 Mon Sep 17 00:00:00 2001 From: Subina Date: Thu, 7 Sep 2023 16:38:51 +0545 Subject: [PATCH] Add table of content selection and sorting --- backend | 2 +- src/components/PillarSelectInput/index.tsx | 15 +- src/components/TocList/index.module.css | 21 +- src/components/TocList/index.tsx | 161 +++-- src/utils/common.ts | 47 +- .../DateQuestionForm/index.tsx | 6 +- .../FileQuestionForm/index.tsx | 6 +- .../ImageQuestionForm/index.tsx | 6 +- .../IntegerQuestionForm/index.tsx | 6 +- .../NoteQuestionForm/index.tsx | 6 +- .../QuestionList/index.module.css | 61 ++ .../QuestionnaireEdit/QuestionList/index.tsx | 344 ++++++++++ .../QuestionPreview/index.module.css | 29 +- .../QuestionPreview/index.tsx | 244 ++++--- .../RankQuestionForm/index.tsx | 6 +- .../SelectMultipleQuestionForm/index.tsx | 6 +- .../SelectOneQuestionForm/index.tsx | 6 +- .../TextQuestionForm/index.tsx | 6 +- .../TimeQuestionForm/index.tsx | 6 +- src/views/QuestionnaireEdit/index.module.css | 1 + src/views/QuestionnaireEdit/index.tsx | 616 +++++++++++------- src/views/QuestionnaireEdit/queries.ts | 19 + 22 files changed, 1136 insertions(+), 484 deletions(-) create mode 100644 src/views/QuestionnaireEdit/QuestionList/index.module.css create mode 100644 src/views/QuestionnaireEdit/QuestionList/index.tsx diff --git a/backend b/backend index fb96451..5c0f1d9 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit fb96451fe4548ce2d40f93fa2782fcac22c00a71 +Subproject commit 5c0f1d997ffe5f6cb5d34e651f8a529e2a0439c6 diff --git a/src/components/PillarSelectInput/index.tsx b/src/components/PillarSelectInput/index.tsx index 9f9649a..e887068 100644 --- a/src/components/PillarSelectInput/index.tsx +++ b/src/components/PillarSelectInput/index.tsx @@ -18,9 +18,13 @@ const PILLARS = gql` name type category1 + category1Display category2 + category2Display category3 + category3Display category4 + category4Display order } } @@ -32,7 +36,12 @@ const PILLARS = gql` type Pillar = NonNullable['projectScope']>['questionnaire']>['leafGroups'][number]; const pillarKeySelector = (data: Pillar) => data.id; -const pillarLabelSelector = (data: Pillar) => data.name; +const pillarLabelSelector = (data: Pillar) => { + if (data.type === 'MATRIX_1D') { + return data.category2Display; + } + return data.category4Display ?? '??'; +}; interface PillarProps{ projectId: string; @@ -41,6 +50,7 @@ interface PillarProps{ value: string | null | undefined; error: string | undefined; onChange: (value: string | undefined, name: T) => void; + disabled?: boolean; } function PillarSelectInput(props: PillarProps) { @@ -51,6 +61,7 @@ function PillarSelectInput(props: PillarProps) { error, onChange, name, + disabled, } = props; const pillarsVariables = useMemo(() => { @@ -89,7 +100,7 @@ function PillarSelectInput(props: PillarProps) { keySelector={pillarKeySelector} labelSelector={pillarLabelSelector} options={pillarsOptions} - disabled={pillarsLoading} + disabled={pillarsLoading || disabled} /> ); } diff --git a/src/components/TocList/index.module.css b/src/components/TocList/index.module.css index 35158c2..82b5ef8 100644 --- a/src/components/TocList/index.module.css +++ b/src/components/TocList/index.module.css @@ -4,18 +4,29 @@ .group-item { display: flex; - padding: var(--dui-spacing-small); + background-color: transparent; + padding: var(--dui-spacing-medium) var(--dui-spacing-small); + padding-right: 0; + .group-item { + border-left: var(--dui-width-separator-medium) solid var(--dui-color-separator); + } + .heading { + font-weight: var(--dui-font-weight-regular); + } + + .header { + background-color: transparent; + .heading { + color: var(--text-color-dark); + } + } .header-icons { display: flex; align-items: center; padding-right: var(--dui-spacing-extra-small); } - .heading { - font-weight: var(--dui-font-weight-regular); - } - .content { display: flex; flex-direction: column; diff --git a/src/components/TocList/index.tsx b/src/components/TocList/index.tsx index 558e38a..a4a5f7c 100644 --- a/src/components/TocList/index.tsx +++ b/src/components/TocList/index.tsx @@ -1,94 +1,133 @@ -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { GrDrag, } from 'react-icons/gr'; import { - isNotDefined, _cs, isDefined, } from '@togglecorp/fujs'; import { + ExpandableContainer, Container, Checkbox, QuickActionButton, } from '@the-deep/deep-ui'; import SortableList, { Attributes, Listeners } from '#components/SortableList'; +import { + TocItem, + getChildren, +} from '#utils/common'; import styles from './index.module.css'; -type QuestionGroup = { - key: string; - parentKeys: string[]; - label: string; - nodes: QuestionGroup[]; -}; - interface TocProps { - key: string; + itemKey: string; mainIndex: number; - item: QuestionGroup; + item: TocItem; attributes?: Attributes; listeners?: Listeners; + onOrderedOptionsChange: (newVal: TocItem[] | undefined, index: number) => void; + selectedGroups: string[]; - onOrderedOptionsChange: (newVal: QuestionGroup[] | undefined, index: number) => void; - onSelectedGroupsChange: React.Dispatch>; - onActiveTabChange: React.Dispatch>; + onSelectedGroupsChange: (newValue: boolean, id: string[]) => void; } function TocRenderer(props: TocProps) { const { - key, + itemKey, mainIndex, item, selectedGroups, onOrderedOptionsChange, onSelectedGroupsChange, - onActiveTabChange, attributes, listeners, } = props; const handleGroupSelect = useCallback((val: boolean) => { - onSelectedGroupsChange((oldVal) => { - if (val) { - return ([...oldVal, key]); - } - const newVal = [...oldVal]; - newVal.splice(oldVal.indexOf(key), 1); - - return newVal; - }); - onActiveTabChange((oldActiveTab) => { - if (isNotDefined(oldActiveTab) && val) { - return key; - } - if (!val && oldActiveTab === key) { - return undefined; - } - return oldActiveTab; - }); + if (!item.leafNode) { + const childIds = getChildren(item); + onSelectedGroupsChange(val, childIds); + return; + } + + onSelectedGroupsChange(val, [item.id]); }, [ - onActiveTabChange, + item, onSelectedGroupsChange, - key, ]); - const nodeList = item.nodes; + const nodeList = item.leafNode ? [] : item.nodes; - const handleListChange = useCallback((newVal: QuestionGroup[] | undefined) => { + const handleListChange = useCallback((newVal: TocItem[] | undefined) => { onOrderedOptionsChange(newVal, mainIndex); }, [ mainIndex, onOrderedOptionsChange, ]); + const childIds = getChildren(item); + const inputValue = item.leafNode + ? selectedGroups.includes(item.id) + : childIds.every((g) => selectedGroups.includes(g)); + + const indeterminate = item.leafNode + ? false + : childIds.some((g) => selectedGroups.includes(g)); + + if (item.leafNode) { + return ( + + + + + + + )} + contentClassName={styles.content} + heading={item.label} + headingClassName={styles.heading} + headingSize="extraSmall" + spacing="none" + > + {isDefined(nodeList) && nodeList.length > 0 && ( + // eslint-disable-next-line @typescript-eslint/no-use-before-define + + )} + + ); + } return ( - @@ -110,7 +151,9 @@ function TocRenderer(props: TocProps) { heading={item.label} headingClassName={styles.heading} headingSize="extraSmall" + headerClassName={styles.header} spacing="none" + withoutBorder > {isDefined(nodeList) && nodeList.length > 0 && ( // eslint-disable-next-line @typescript-eslint/no-use-before-define @@ -120,22 +163,20 @@ function TocRenderer(props: TocProps) { onOrderedOptionsChange={handleListChange} selectedGroups={selectedGroups} onSelectedGroupsChange={onSelectedGroupsChange} - onActiveTabChange={onActiveTabChange} /> )} - + ); } -const keySelector = (g: QuestionGroup) => g.key; +const keySelector = (g: TocItem) => g.key; interface Props { className?: string; - orderedOptions: QuestionGroup[] | undefined; - onOrderedOptionsChange: (newVal: QuestionGroup[] | undefined) => void; + orderedOptions: TocItem[] | undefined; + onOrderedOptionsChange: (newVal: TocItem[] | undefined) => void; selectedGroups: string[]; - onSelectedGroupsChange: React.Dispatch>; - onActiveTabChange: React.Dispatch>; + onSelectedGroupsChange: (newValue: boolean, id: string[]) => void; } function TocList(props: Props) { @@ -145,22 +186,24 @@ function TocList(props: Props) { onOrderedOptionsChange, selectedGroups, onSelectedGroupsChange, - onActiveTabChange, } = props; const handleChildrenOrderChange = useCallback(( - newValue: QuestionGroup[] | undefined, + newValue: TocItem[] | undefined, parentIndex: number, ) => { if (!newValue) { return; } - const newList = [...orderedOptions ?? []]; - newList[parentIndex] = { - ...newList[parentIndex], - nodes: newValue, - }; - onOrderedOptionsChange(newList); + const node = orderedOptions?.[parentIndex]; + if (node && !node.leafNode) { + const newList = [...orderedOptions]; + newList[parentIndex] = { + ...node, + nodes: newValue, + }; + onOrderedOptionsChange(newList); + } }, [ orderedOptions, onOrderedOptionsChange, @@ -168,21 +211,19 @@ function TocList(props: Props) { const tocRendererParams = useCallback(( key: string, - datum: QuestionGroup, + datum: TocItem, mainIndex: number, ): TocProps => ({ onOrderedOptionsChange: handleChildrenOrderChange, onSelectedGroupsChange, - onActiveTabChange, selectedGroups, - key, + itemKey: key, mainIndex, item: datum, }), [ handleChildrenOrderChange, selectedGroups, onSelectedGroupsChange, - onActiveTabChange, ]); return ( diff --git a/src/utils/common.ts b/src/utils/common.ts index 4c56941..9055504 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,20 +1,43 @@ -import { isDefined } from '@togglecorp/fujs'; - // eslint-disable-next-line import/prefer-default-export export function flatten( - lst: A[], + list: A[], valueSelector: (item: A) => K, - childSelector: (item: A) => A[] | undefined, + childSelector: (item: A) => A[], ): K[] { - if (lst.length <= 0) { - return []; + const items = list.map((item) => { + const a = flatten(childSelector(item), valueSelector, childSelector); + if (childSelector(item).length > 0) { + return a; + } + return [valueSelector(item)]; + }); + + return items.flat(); +} + +export type LeafTocItem = { + key: string; + parentKeys: string[]; + label: string; + leafNode: true; + id: string; + isHidden: boolean; +}; +export type NonLeafTocItem = { + key: string; + parentKeys: string[]; + label: string; + leafNode?: false; + nodes: TocItem[]; +} + +export type TocItem = LeafTocItem | NonLeafTocItem; + +export function getChildren(item: TocItem): string[] { + if (item.leafNode) { + return [item.id]; } - const itemsByParent = lst.map(valueSelector); - const itemsByChildren = lst.map(childSelector).filter(isDefined).flat(); - return [ - ...itemsByParent, - ...flatten(itemsByChildren, valueSelector, childSelector), - ]; + return item.nodes.flatMap(getChildren); } type DeepNonNullable = T extends object ? ( diff --git a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx index cfb6596..1b19088 100644 --- a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx @@ -123,6 +123,7 @@ interface Props { questionnaireId: string; questionId?: string; onSuccess: (questionId: string | undefined) => void; + selectedLeafGroupId: string; } function DateQuestionForm(props: Props) { @@ -131,6 +132,7 @@ function DateQuestionForm(props: Props) { questionnaireId, questionId, onSuccess, + selectedLeafGroupId, } = props; const alert = useAlert(); @@ -138,6 +140,7 @@ function DateQuestionForm(props: Props) { const initialFormValue: FormType = { type: 'DATE' as QuestionTypeEnum, questionnaire: questionnaireId, + leafGroup: selectedLeafGroupId, }; const { @@ -317,9 +320,10 @@ function DateQuestionForm(props: Props) { name="leafGroup" projectId={projectId} questionnaireId={questionnaireId} - value={formValue.leafGroup} + value={selectedLeafGroupId} error={fieldError?.leafGroup} onChange={setFieldValue} + disabled /> - )} /> - @@ -592,88 +700,100 @@ export function Component() { pending={false} /> )} -
- {(selectedQuestionType === 'TEXT') && ( - - )} - {(selectedQuestionType === 'INTEGER') && ( - - )} - {(selectedQuestionType === 'RANK') && ( - - )} - {(selectedQuestionType === 'SELECT_ONE') && ( - - )} - {(selectedQuestionType === 'SELECT_MULTIPLE') && ( - - )} - {(selectedQuestionType === 'DATE') && ( - - )} - {(selectedQuestionType === 'TIME') && ( - - )} - {(selectedQuestionType === 'NOTE') && ( - - )} - {(selectedQuestionType === 'FILE') && ( - - )} - {(selectedQuestionType === 'IMAGE') && ( - - )} -
+ {activeLeafGroupId && ( +
+ {(selectedQuestionType === 'TEXT') && ( + + )} + {(selectedQuestionType === 'INTEGER') && ( + + )} + {(selectedQuestionType === 'RANK') && ( + + )} + {(selectedQuestionType === 'SELECT_ONE') && ( + + )} + {(selectedQuestionType === 'SELECT_MULTIPLE') && ( + + )} + {(selectedQuestionType === 'DATE') && ( + + )} + {(selectedQuestionType === 'TIME') && ( + + )} + {(selectedQuestionType === 'NOTE') && ( + + )} + {(selectedQuestionType === 'FILE') && ( + + )} + {(selectedQuestionType === 'IMAGE') && ( + + )} +
+ )} )} diff --git a/src/views/QuestionnaireEdit/queries.ts b/src/views/QuestionnaireEdit/queries.ts index c06e88b..589ae3e 100644 --- a/src/views/QuestionnaireEdit/queries.ts +++ b/src/views/QuestionnaireEdit/queries.ts @@ -18,6 +18,25 @@ export const QUESTION_FRAGMENT = gql` } `; +export const LEAF_GROUPS_FRAGMENT = gql` + fragment LeafGroups on QuestionLeafGroupType { + id + name + order + category1 + category1Display + category2 + category2Display + category3 + category3Display + category4 + category4Display + type + typeDisplay + isHidden + } +`; + export const QUESTION_INFO = gql` query QuestionInfo ( $projectId: ID!,