From 5c630b61b45f484c8f545d558990e18224ab6ce0 Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 16 Aug 2023 14:31:57 +0545 Subject: [PATCH 1/6] Add table of contents --- package.json | 4 + src/components/SortableList/index.tsx | 326 ++++++++++++++++++++++++++ src/views/QuestionnaireEdit/index.tsx | 87 ++++++- yarn.lock | 39 +++ 4 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 src/components/SortableList/index.tsx diff --git a/package.json b/package.json index a188986..a9d7402 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,10 @@ }, "dependencies": { "@apollo/client": "^3.7.15", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@hcaptcha/react-hcaptcha": "^1.8.1", "@the-deep/deep-ui": "^2.0.3", "@togglecorp/fujs": "^2.0.0", diff --git a/src/components/SortableList/index.tsx b/src/components/SortableList/index.tsx new file mode 100644 index 0000000..e10fc3a --- /dev/null +++ b/src/components/SortableList/index.tsx @@ -0,0 +1,326 @@ +import React, { useState, memo, useMemo, useCallback } from 'react'; +import { + Portal, + ListView, + ListViewProps, +} from '@the-deep/deep-ui'; +import { + DragOverlay, + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragStartEvent, + DraggableSyntheticListeners, +} from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import { + restrictToHorizontalAxis, + restrictToVerticalAxis, +} from '@dnd-kit/modifiers'; +import { + useSortable, + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + horizontalListSortingStrategy, + verticalListSortingStrategy, + rectSortingStrategy, +} from '@dnd-kit/sortable'; +import { listToMap, isDefined } from '@togglecorp/fujs'; + +export const genericMemo: ((c: T) => T) = memo; + +type OptionKey = string | number; +interface GroupCommonProps { + className?: string; + children: React.ReactNode; +} + +export type Listeners = DraggableSyntheticListeners; +export type NodeRef = (node: HTMLElement | null) => void; + +export interface Attributes { + role: string; + tabIndex: number; + 'aria-pressed': boolean | undefined; + 'aria-roledescription': string; + 'aria-describedby': string; +} + +interface SortableItemProps< + D, + P, + K extends OptionKey, + ItemContainerParams, +> { + keySelector: (data: D) => K; + datum: D; + renderer: (props: P & { + listeners?: Listeners; + attributes?: Attributes; + }) => JSX.Element; + rendererParams: P; + itemContainerParams?: ItemContainerParams; +} + +function SortableItem< + D, + P, + K extends OptionKey, + ItemContainerParams, +>(props: SortableItemProps) { + const { + keySelector, + renderer: Renderer, + datum, + rendererParams, + itemContainerParams, + } = props; + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: String(keySelector(datum)) }); + + const style: React.CSSProperties = useMemo(() => ({ + transform: CSS.Transform.toString({ + x: transform?.x ?? 0, + y: transform?.y ?? 0, + scaleX: 1, + scaleY: 1, + }), + transition: transition ?? undefined, + }), [transition, transform]); + + return ( +
+ +
+ ); +} + +const MemoizedSortableItem = genericMemo(SortableItem); + +export type Props< + N extends string, + D, + P, + K extends OptionKey, + GP extends GroupCommonProps, + GK extends OptionKey, + ItemContainerParams, +> = Omit, 'keySelector' | 'renderer' | 'direction' | 'pending' | 'filtered' | 'errored'> & { + name: N; + keySelector: (val: D) => K; + renderer: (props: P & { + listeners?: Listeners; + attributes?: Attributes; + setNodeRef?: NodeRef; + style?: React.CSSProperties; + }) => JSX.Element; + onChange?: (newList: D[], name: N) => void; + direction: 'vertical' | 'horizontal' | 'rect'; + showDragOverlay?: boolean; + itemContainerParams?: (key: K, datum: D, index: number, data: D[]) => ItemContainerParams; + pending?: boolean; + filtered?: boolean; + errored?: boolean; +} + +function SortableList< + N extends string, + D, + P, + K extends OptionKey, + GP extends GroupCommonProps, + GK extends OptionKey, + ItemContainerParams, +>(props: Props) { + const { + className, + name, + onChange, + data, + keySelector, + rendererParams, + renderer: Renderer, + direction, + showDragOverlay, + itemContainerParams, + pending = false, + filtered = false, + errored = false, + ...otherProps + } = props; + const [activeId, setActiveId] = useState(); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + // NOTE: Sortable context requires list of items + const items = useMemo(() => ( + data?.map((d) => String(keySelector(d))) ?? [] + ), [data, keySelector]); + + const handleDragStart = useCallback((event: DragStartEvent) => { + const { active } = event; + setActiveId(active.id); + }, []); + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + setActiveId(undefined); + + if (active.id && over?.id && active.id !== over?.id && items && onChange) { + const oldIndex = items.indexOf(active.id); + const newIndex = items.indexOf(over.id); + + const newItems = arrayMove(items, oldIndex, newIndex); + const dataMap = listToMap( + data ?? [], + (d) => String(keySelector(d)), + (d) => d, + ); + const newData = newItems.map((item) => dataMap[item]).filter(isDefined); + onChange(newData, name); + } + }, [keySelector, items, data, onChange, name]); + + const DragItem = useMemo(() => { + if (!activeId || !data || !showDragOverlay) { + return null; + } + const activeIndex = data.findIndex( + (d) => String(keySelector(d)) === activeId, + ); + if (!activeIndex) { + return null; + } + const params = rendererParams( + keySelector(data[activeIndex]), + data[activeIndex], + activeIndex, + data, + ); + if (!params) { + return null; + } + return ( + + ); + }, [ + activeId, + Renderer, + keySelector, + rendererParams, + data, + showDragOverlay, + ]); + + const modifiedRendererParams = useCallback(( + _: K, + datum: D, + index: number, + dataFromArgs: D[], + ) => { + const params = rendererParams( + keySelector(datum), + datum, + index, + dataFromArgs, + ); + + const containerParams = itemContainerParams && itemContainerParams( + keySelector(datum), + datum, + index, + dataFromArgs, + ); + + return ({ + rendererParams: params, + itemContainerParams: containerParams, + datum, + keySelector, + renderer: Renderer, + }); + }, [keySelector, rendererParams, Renderer, itemContainerParams]); + + const sortingStrategy = useMemo(() => { + if (direction === 'rect') { + return rectSortingStrategy; + } + if (direction === 'vertical') { + return verticalListSortingStrategy; + } + return horizontalListSortingStrategy; + }, [direction]); + + const modifiers = useMemo(() => { + if (direction === 'rect') { + return undefined; + } + return [ + direction === 'horizontal' ? restrictToHorizontalAxis : restrictToVerticalAxis, + ]; + }, [direction]); + + return ( + + + + + {showDragOverlay && ( + + + {DragItem} + + + )} + + ); +} + +export default genericMemo(SortableList); diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index 198ba3e..5adf90d 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -12,6 +12,7 @@ import { MdOutlineAbc, MdOutlineChecklist, } from 'react-icons/md'; +import { GrDrag } from 'react-icons/gr'; import { isNotDefined, isDefined, @@ -31,6 +32,7 @@ import { } from '@the-deep/deep-ui'; import SubNavbar from '#components/SubNavbar'; +import SortableList, { Attributes, Listeners } from '#components/SortableList'; import { QuestionnaireQuery, QuestionnaireQueryVariables, @@ -91,12 +93,66 @@ const QUESTIONNAIRE = gql` questionnaireId } } + groups(filters: { + questionnaire: { + pk: $questionnaireId + } + }) { + items { + id + parentId + relevant + questionnaireId + label + name + } + } } } } `; + type Question = NonNullable['projectScope']>['questions']>['items']>[number]; +type QuestionGroup = NonNullable['projectScope']>['groups']>['items']>[number]; const questionKeySelector = (q: Question) => q.id; +const questionGroupKeySelector = (g: QuestionGroup) => g.id; + +interface QuestionGroupProps { + id: string; + item: QuestionGroup; + attributes?: Attributes; + listeners?: Listeners; +} + +function QuestionGroupItem(props: QuestionGroupProps) { + const { + id, + item, + attributes, + listeners, + } = props; + + return ( + + + + )} + className={styles.groupItem} + heading={item.label} + headingSize="extraSmall" + /> + ); +} const questionTypes: QuestionType[] = [ { @@ -129,6 +185,10 @@ const questionTypes: QuestionType[] = [ const questionTypeKeySelector = (q: QuestionType) => q.key; const PAGE_SIZE = 15; +function reorder(data: T[]) { + return data.map((v, i) => ({ ...v, order: i + 1 })); +} + // FIXME: The type is not right interface QuestionnaireParams { projectId: string | undefined; @@ -189,6 +249,9 @@ export function Component() { const projectTitle = questionnaireResponse?.private.projectScope?.project.title; const questionsData = questionnaireResponse?.private.projectScope?.questions?.items; + const questionGroups = questionnaireResponse?.private.projectScope?.groups.items; + const filteredParentGroups = questionGroups?.filter((group) => group.parentId === null); + const questionTypeRendererParams = useCallback((key: string, data: QuestionType) => ({ questionType: data, name: key, @@ -200,6 +263,15 @@ export function Component() { }), [ ]); + const tocRendererParams = useCallback((key: string, data: QuestionGroup) => ({ + id: key, + item: data, + }), []); + + const handleGroupOrderChange = useCallback((...args) => { + console.warn('here', args); + }, []); + if (isNotDefined(projectId) || isNotDefined(questionnaireId)) { return undefined; } @@ -226,7 +298,20 @@ export function Component() { heading="Select Questions" contentClassName={styles.leftContent} > - Here will be the list of sections +
Date: Wed, 16 Aug 2023 16:11:25 +0545 Subject: [PATCH 2/6] temp --- src/components/TocList/index.module.css | 0 src/components/TocList/index.tsx | 70 +++++++++++++++++++++++++ src/views/QuestionnaireEdit/index.tsx | 41 +++++++-------- 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 src/components/TocList/index.module.css create mode 100644 src/components/TocList/index.tsx diff --git a/src/components/TocList/index.module.css b/src/components/TocList/index.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TocList/index.tsx b/src/components/TocList/index.tsx new file mode 100644 index 0000000..78291a5 --- /dev/null +++ b/src/components/TocList/index.tsx @@ -0,0 +1,70 @@ +import { useState, useCallback } from 'react'; +import SortableList, { Attributes, Listeners } from '#components/SortableList'; + +import styles from './index.module.css'; + +interface QuestionGroup { + id: string; + label: string; + name: string; + parentId: string; + questionnaireId: string; + relevant?: string; +} + +function reorder(data: T[]) { + return data.map((v, i) => ({ ...v, order: i + 1 })); +} + +const keySelector = (g: QuestionGroup) => g.id; + +interface Props

{ + parentId: string | null; + options: QuestionGroup[]; + renderer: (props: P & { + listeners?: Listeners; + attributes?: Attributes; + }) => JSX.Element; + rendererParams: P; +} + +function TocList

(props: Props

) { + const { + parentId, + options, + renderer, + rendererParams, + } = props; + + const filteredOptions = options?.filter( + (group: QuestionGroup) => group.parentId === parentId, + ); + + const [ + orderedFilteredOptions, + setFilteredOrderedOptions, + ] = useState(filteredOptions); + + const handleGroupOrderChange = useCallback((...args: QuestionGroup[]) => { + setFilteredOrderedOptions(args); + }, []); + + return ( + + ); +} + +export default TocList; diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index 5adf90d..3394e7a 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -33,6 +33,7 @@ import { import SubNavbar from '#components/SubNavbar'; import SortableList, { Attributes, Listeners } from '#components/SortableList'; +import TocList from '#components/TocList'; import { QuestionnaireQuery, QuestionnaireQueryVariables, @@ -122,6 +123,7 @@ interface QuestionGroupProps { item: QuestionGroup; attributes?: Attributes; listeners?: Listeners; + options: QuestionGroup[]; } function QuestionGroupItem(props: QuestionGroupProps) { @@ -130,8 +132,15 @@ function QuestionGroupItem(props: QuestionGroupProps) { item, attributes, listeners, + options, } = props; + const rendererParams = useCallback((key: string, datum: QuestionGroup) => ({ + id: key, + item: datum, + options, + }), [options]); + return ( + > + + ); } @@ -185,10 +201,6 @@ const questionTypes: QuestionType[] = [ const questionTypeKeySelector = (q: QuestionType) => q.key; const PAGE_SIZE = 15; -function reorder(data: T[]) { - return data.map((v, i) => ({ ...v, order: i + 1 })); -} - // FIXME: The type is not right interface QuestionnaireParams { projectId: string | undefined; @@ -250,7 +262,6 @@ export function Component() { const questionsData = questionnaireResponse?.private.projectScope?.questions?.items; const questionGroups = questionnaireResponse?.private.projectScope?.groups.items; - const filteredParentGroups = questionGroups?.filter((group) => group.parentId === null); const questionTypeRendererParams = useCallback((key: string, data: QuestionType) => ({ questionType: data, @@ -268,10 +279,6 @@ export function Component() { item: data, }), []); - const handleGroupOrderChange = useCallback((...args) => { - console.warn('here', args); - }, []); - if (isNotDefined(projectId) || isNotDefined(questionnaireId)) { return undefined; } @@ -298,19 +305,11 @@ export function Component() { heading="Select Questions" contentClassName={styles.leftContent} > -

From 9b3df11829d524c5f18d62664fd4ad200223860c Mon Sep 17 00:00:00 2001 From: Subina Date: Fri, 18 Aug 2023 16:10:15 +0545 Subject: [PATCH 3/6] Add table of contents - Add tab view in question listing - Group questions based on selected question groups - Add pull request template for github --- .github/pull_request_template.md | 25 ++ src/components/Avatar/index.module.css | 4 +- src/components/SortableList/index.tsx | 22 +- src/components/TocList/index.module.css | 30 +++ src/components/TocList/index.tsx | 192 +++++++++++--- .../UserSelectInput/index.module.css | 0 src/components/UserSelectInput/index.tsx | 2 +- src/views/Home/index.module.css | 4 +- .../QuestionPreview/index.module.css | 3 +- .../QuestionPreview/index.tsx | 22 +- src/views/QuestionnaireEdit/index.module.css | 1 + src/views/QuestionnaireEdit/index.tsx | 244 ++++++++++-------- src/views/Root/index.module.css | 13 - src/views/Root/index.tsx | 2 - 14 files changed, 392 insertions(+), 172 deletions(-) create mode 100644 .github/pull_request_template.md delete mode 100644 src/components/UserSelectInput/index.module.css delete mode 100644 src/views/Root/index.module.css diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..dab328c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +- Addresses XXX +- Depends on XXX + +## Changes + +* Detailed list or prose of changes +* Breaking changes +* Changes to configurations + +## This PR doesn't introduce any: + +- [ ] temporary files, auto-generated files or secret keys +- [ ] build works +- [ ] eslint issues +- [ ] typescript issues +- [ ] codegen errors +- [ ] `console.log` meant for debugging +- [ ] typos +- [ ] unwanted comments +- [ ] conflict markers + +## This PR contains valid: + +- [ ] permission checks + diff --git a/src/components/Avatar/index.module.css b/src/components/Avatar/index.module.css index e65a43a..add0422 100644 --- a/src/components/Avatar/index.module.css +++ b/src/components/Avatar/index.module.css @@ -8,7 +8,7 @@ height: var(--size); .image { - border: var(--width-separator-thin) solid var(--color-separator); + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); border-radius: 50%; width: 100%; height: 100%; @@ -20,7 +20,7 @@ display: flex; align-items: center; justify-content: center; - border: var(--width-separator-thin) solid var(---color-separator); + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); border-radius: 50%; width: 100%; height: 100%; diff --git a/src/components/SortableList/index.tsx b/src/components/SortableList/index.tsx index e10fc3a..ac2157f 100644 --- a/src/components/SortableList/index.tsx +++ b/src/components/SortableList/index.tsx @@ -1,4 +1,9 @@ -import React, { useState, memo, useMemo, useCallback } from 'react'; +import React, { + useState, + memo, + useMemo, + useCallback, +} from 'react'; import { Portal, ListView, @@ -67,6 +72,7 @@ interface SortableItemProps< itemContainerParams?: ItemContainerParams; } +// eslint-disable-next-line react-refresh/only-export-components function SortableItem< D, P, @@ -103,17 +109,20 @@ function SortableItem<
); } +// eslint-disable-next-line react-refresh/only-export-components const MemoizedSortableItem = genericMemo(SortableItem); export type Props< @@ -142,6 +151,7 @@ export type Props< errored?: boolean; } +// eslint-disable-next-line react-refresh/only-export-components function SortableList< N extends string, D, @@ -183,7 +193,7 @@ function SortableList< const handleDragStart = useCallback((event: DragStartEvent) => { const { active } = event; - setActiveId(active.id); + setActiveId(active.id.toString()); }, []); const handleDragEnd = useCallback((event: DragEndEvent) => { @@ -191,8 +201,8 @@ function SortableList< setActiveId(undefined); if (active.id && over?.id && active.id !== over?.id && items && onChange) { - const oldIndex = items.indexOf(active.id); - const newIndex = items.indexOf(over.id); + const oldIndex = items.indexOf(active.id.toString()); + const newIndex = items.indexOf(over.id.toString()); const newItems = arrayMove(items, oldIndex, newIndex); const dataMap = listToMap( @@ -226,6 +236,7 @@ function SortableList< } return ( ); @@ -299,7 +310,7 @@ function SortableList< strategy={sortingStrategy} > ['projectScope']>['groups']>['items']>[number]; + +interface TocProps { id: string; - label: string; - name: string; - parentId: string; - questionnaireId: string; - relevant?: string; + item: QuestionGroup; + attributes?: Attributes; + listeners?: Listeners; + selectedGroups: string[]; + orderedOptions: QuestionGroup[] | undefined; + onOrderedOptionsChange: React.Dispatch>; + onSelectedGroupsChange: React.Dispatch>; + onActiveTabChange: React.Dispatch>; } -function reorder(data: T[]) { - return data.map((v, i) => ({ ...v, order: i + 1 })); +function TocRenderer(props: TocProps) { + const { + id, + item, + orderedOptions, + selectedGroups, + onOrderedOptionsChange, + onSelectedGroupsChange, + onActiveTabChange, + attributes, + listeners, + } = props; + + const handleGroupSelect = useCallback((val: boolean) => { + onSelectedGroupsChange((oldVal) => { + if (val) { + return ([...oldVal, id]); + } + const newVal = [...oldVal]; + newVal.splice(oldVal.indexOf(id), 1); + + return newVal; + }); + onActiveTabChange((oldActiveTab) => { + if (isNotDefined(oldActiveTab) && val) { + return id; + } + if (!val && oldActiveTab === id) { + return undefined; + } + return oldActiveTab; + }); + }, [ + onActiveTabChange, + onSelectedGroupsChange, + id, + ]); + + const filteredOptions = orderedOptions?.filter((group) => id === group.parentId); + + return ( + + + + + +
+ )} + contentClassName={styles.content} + heading={item.label} + headingClassName={styles.heading} + headingSize="extraSmall" + spacing="none" + > + {isDefined(filteredOptions) && filteredOptions.length > 0 && ( + // eslint-disable-next-line @typescript-eslint/no-use-before-define + + )} + + ); } const keySelector = (g: QuestionGroup) => g.id; -interface Props

{ +interface Props { + className?: string; parentId: string | null; - options: QuestionGroup[]; - renderer: (props: P & { - listeners?: Listeners; - attributes?: Attributes; - }) => JSX.Element; - rendererParams: P; + orderedOptions: QuestionGroup[] | undefined; + onOrderedOptionsChange: React.Dispatch> + selectedGroups: string[]; + onSelectedGroupsChange: React.Dispatch>; + onActiveTabChange: React.Dispatch>; } -function TocList

(props: Props

) { +function TocList(props: Props) { const { + className, parentId, - options, - renderer, - rendererParams, + orderedOptions, + onOrderedOptionsChange, + selectedGroups, + onSelectedGroupsChange, + onActiveTabChange, } = props; - const filteredOptions = options?.filter( + const filteredOptions = orderedOptions?.filter( (group: QuestionGroup) => group.parentId === parentId, ); - const [ - orderedFilteredOptions, - setFilteredOrderedOptions, - ] = useState(filteredOptions); + const tocRendererParams = useCallback((key: string, datum: QuestionGroup): TocProps => ({ + orderedOptions, + onOrderedOptionsChange, + onSelectedGroupsChange, + onActiveTabChange, + selectedGroups, + id: key, + item: datum, + }), [ + orderedOptions, + selectedGroups, + onSelectedGroupsChange, + onOrderedOptionsChange, + onActiveTabChange, + ]); - const handleGroupOrderChange = useCallback((...args: QuestionGroup[]) => { - setFilteredOrderedOptions(args); - }, []); + const handleGroupOrderChange = useCallback((oldValue: QuestionGroup[]) => { + const nonParentOptions = orderedOptions?.filter((group) => !oldValue.includes(group)) ?? []; + onOrderedOptionsChange([ + ...nonParentOptions, + ...oldValue, + ]); + }, [ + onOrderedOptionsChange, + orderedOptions, + ]); return ( ); diff --git a/src/components/UserSelectInput/index.module.css b/src/components/UserSelectInput/index.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/UserSelectInput/index.tsx b/src/components/UserSelectInput/index.tsx index dee4681..4b1b2a8 100644 --- a/src/components/UserSelectInput/index.tsx +++ b/src/components/UserSelectInput/index.tsx @@ -137,7 +137,7 @@ function UserSelectInput(props: UserSelectI return ( ['projectScope']>['questions']>['items']>[number]; +type Question = NonNullable['projectScope']>['questions']>['items']>[number]; interface QuestionProps { question: Question; @@ -18,8 +25,15 @@ function QuestionPreview(props: QuestionProps) { question, } = props; + if (isNotDefined(question.groupId)) { + return null; + } + return ( -

+ {(question.type === 'TEXT') && ( )} -
+ ); } diff --git a/src/views/QuestionnaireEdit/index.module.css b/src/views/QuestionnaireEdit/index.module.css index 96fb7a0..11c0784 100644 --- a/src/views/QuestionnaireEdit/index.module.css +++ b/src/views/QuestionnaireEdit/index.module.css @@ -57,6 +57,7 @@ flex-direction: column; flex-grow: 1; margin: 0 var(--dui-spacing-extra-large); + overflow-y: auto; gap: var(--dui-spacing-large); .header { diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index 3394e7a..c30a78b 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -12,7 +12,6 @@ import { MdOutlineAbc, MdOutlineChecklist, } from 'react-icons/md'; -import { GrDrag } from 'react-icons/gr'; import { isNotDefined, isDefined, @@ -27,16 +26,19 @@ import { Header, ListView, QuickActionButton, + Tab, + Tabs, TextOutput, useModalState, } from '@the-deep/deep-ui'; import SubNavbar from '#components/SubNavbar'; -import SortableList, { Attributes, Listeners } from '#components/SortableList'; import TocList from '#components/TocList'; import { QuestionnaireQuery, QuestionnaireQueryVariables, + QuestionsByGroupQuery, + QuestionsByGroupQueryVariables, } from '#generated/types'; import TextQuestionForm from './TextQuestionForm'; @@ -53,8 +55,6 @@ const QUESTIONNAIRE = gql` query Questionnaire( $projectId: ID!, $questionnaireId: ID!, - $limit: Int, - $offset: Int, ) { private { projectScope(pk: $projectId) { @@ -67,19 +67,45 @@ const QUESTIONNAIRE = gql` id title } + groups(filters: { + questionnaire: { + pk: $questionnaireId + } + }) { + items { + id + parentId + relevant + questionnaireId + label + name + } + } + } + } + } +`; + +const QUESTIONS_BY_GROUP = gql` + query QuestionsByGroup( + $projectId: ID!, + $questionnaireId: ID!, + $groupId: DjangoModelFilterInput, + ) { + private { + projectScope(pk: $projectId) { + id questions( filters: { questionnaire: { pk: $questionnaireId, - } + }, + group: $groupId, + includeChildGroup: true, } order: { createdAt: ASC } - pagination: { - limit: $limit, - offset: $offset, - } ) { count limit @@ -88,87 +114,22 @@ const QUESTIONNAIRE = gql` createdAt hint id + groupId label name type questionnaireId } } - groups(filters: { - questionnaire: { - pk: $questionnaireId - } - }) { - items { - id - parentId - relevant - questionnaireId - label - name - } - } } } } `; -type Question = NonNullable['projectScope']>['questions']>['items']>[number]; +type Question = NonNullable['projectScope']>['questions']>['items']>[number]; type QuestionGroup = NonNullable['projectScope']>['groups']>['items']>[number]; const questionKeySelector = (q: Question) => q.id; -const questionGroupKeySelector = (g: QuestionGroup) => g.id; - -interface QuestionGroupProps { - id: string; - item: QuestionGroup; - attributes?: Attributes; - listeners?: Listeners; - options: QuestionGroup[]; -} - -function QuestionGroupItem(props: QuestionGroupProps) { - const { - id, - item, - attributes, - listeners, - options, - } = props; - - const rendererParams = useCallback((key: string, datum: QuestionGroup) => ({ - id: key, - item: datum, - options, - }), [options]); - - return ( - - - - )} - className={styles.groupItem} - heading={item.label} - headingSize="extraSmall" - > - - - ); -} +const groupTabKeySelector = (g: QuestionGroup) => g.id; const questionTypes: QuestionType[] = [ { @@ -201,18 +162,12 @@ const questionTypes: QuestionType[] = [ const questionTypeKeySelector = (q: QuestionType) => q.key; const PAGE_SIZE = 15; -// FIXME: The type is not right -interface QuestionnaireParams { - projectId: string | undefined; - questionnaireId: string | undefined; -} - // eslint-disable-next-line import/prefer-default-export export function Component() { const { projectId, questionnaireId, - } = useParams(); + } = useParams<{projectId: string, questionnaireId: string}>(); const [ addQuestionPaneShown, @@ -225,6 +180,11 @@ export function Component() { setSelectedQuestionType, ] = useState(); + const [ + selectedGroups, + setSelectedGroups, + ] = useState([]); + const handleRightPaneClose = useCallback(() => { hideAddQuestionPane(); setSelectedQuestionType(undefined); @@ -236,6 +196,7 @@ export function Component() { if (isNotDefined(projectId) || isNotDefined(questionnaireId)) { return undefined; } + return ({ projectId, questionnaireId, @@ -247,6 +208,11 @@ export function Component() { questionnaireId, ]); + const [ + orderedOptions, + setOrderedOptions, + ] = useState([]); + const { data: questionnaireResponse, } = useQuery( @@ -254,15 +220,60 @@ export function Component() { { skip: isNotDefined(questionnaireVariables), variables: questionnaireVariables, + onCompleted: (response) => { + const questionGroups = response?.private.projectScope?.groups?.items; + setOrderedOptions(questionGroups ?? []); + }, }, ); const questionnaireTitle = questionnaireResponse?.private.projectScope?.questionnaire?.title; const projectTitle = questionnaireResponse?.private.projectScope?.project.title; - const questionsData = questionnaireResponse?.private.projectScope?.questions?.items; - const questionGroups = questionnaireResponse?.private.projectScope?.groups.items; + const parentQuestionGroups = orderedOptions?.filter( + (item) => item.parentId === null, + ); + const selectedParentQuestionGroups = parentQuestionGroups?.filter( + (group) => selectedGroups.includes(group.id), + ); + + const [activeGroupTab, setActiveGroupTab] = useState( + selectedParentQuestionGroups?.[0]?.name, + ); + + // NOTE: If none of the tabs are selected, 1st group should be selected + const finalSelectedTab = activeGroupTab ?? selectedGroups[0]; + + const questionsVariables = useMemo(() => { + if (isNotDefined(projectId) + || isNotDefined(questionnaireId) + || isNotDefined(finalSelectedTab)) { + return undefined; + } + + return ({ + projectId, + questionnaireId, + activeGroupTab: finalSelectedTab, + }); + }, [ + projectId, + questionnaireId, + finalSelectedTab, + ]); + + const { + data: questionsResponse, + } = useQuery( + QUESTIONS_BY_GROUP, + { + skip: isNotDefined(questionsVariables), + variables: questionsVariables, + }, + ); + + const questionsData = questionsResponse?.private.projectScope?.questions?.items; const questionTypeRendererParams = useCallback((key: string, data: QuestionType) => ({ questionType: data, name: key, @@ -271,16 +282,15 @@ export function Component() { const questionRendererParams = useCallback((_: string, data: Question) => ({ question: data, - }), [ - ]); + }), []); - const tocRendererParams = useCallback((key: string, data: QuestionGroup) => ({ - id: key, - item: data, + const groupTabRenderParams = useCallback((_: string, datum: QuestionGroup) => ({ + children: datum.label, + name: datum.id, }), []); if (isNotDefined(projectId) || isNotDefined(questionnaireId)) { - return undefined; + return null; } return ( @@ -306,10 +316,12 @@ export function Component() { contentClassName={styles.leftContent} >
@@ -327,19 +339,35 @@ export function Component() { )} /> - + + + +
{addQuestionPaneShown && (
diff --git a/src/views/Root/index.module.css b/src/views/Root/index.module.css deleted file mode 100644 index 6bd370f..0000000 --- a/src/views/Root/index.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.page { - display: flex; - flex-direction: column; - - .navbar { - --height-navbar: 4rem; - padding: var(--dui-spacing-medium) var(--dui-spacing-super-large); - height: var(--height-navbar); - .logo { - width: 5rem; - } - } -} diff --git a/src/views/Root/index.tsx b/src/views/Root/index.tsx index b12f51d..8e80a9e 100644 --- a/src/views/Root/index.tsx +++ b/src/views/Root/index.tsx @@ -1,7 +1,5 @@ import { Outlet } from 'react-router-dom'; -// import styles from './index.module.css'; - // eslint-disable-next-line import/prefer-default-export export function Component() { return ( From 4c8e59ef94ee1950b2a10583fa88f44c209356d3 Mon Sep 17 00:00:00 2001 From: Subina Date: Tue, 22 Aug 2023 12:08:25 +0545 Subject: [PATCH 4/6] Add more question type mutations - Date type questions - Time type questions - File type questions - Image type questions - Note type questions --- .../DateQuestionPreview/index.module.css | 6 + .../DateQuestionPreview/index.tsx | 46 +++++ .../FileQuestionPreview/index.module.css | 26 +++ .../FileQuestionPreview/index.tsx | 58 ++++++ .../ImageQuestionPreview/index.module.css | 26 +++ .../ImageQuestionPreview/index.tsx | 58 ++++++ .../NoteQuestionPreview/index.module.css | 6 + .../NoteQuestionPreview/index.tsx | 43 ++++ .../TimeQuestionPreview/index.module.css | 6 + .../TimeQuestionPreview/index.tsx | 46 +++++ .../DateQuestionForm/index.module.css | 15 ++ .../DateQuestionForm/index.tsx | 192 ++++++++++++++++++ .../FileQuestionForm/index.module.css | 16 ++ .../FileQuestionForm/index.tsx | 191 +++++++++++++++++ .../ImageQuestionForm/index.module.css | 16 ++ .../ImageQuestionForm/index.tsx | 191 +++++++++++++++++ .../NoteQuestionForm/index.module.css | 16 ++ .../NoteQuestionForm/index.tsx | 182 +++++++++++++++++ .../QuestionPreview/index.tsx | 16 ++ .../RankQuestionForm/index.tsx | 2 +- .../TimeQuestionForm/index.module.css | 15 ++ .../TimeQuestionForm/index.tsx | 192 ++++++++++++++++++ src/views/QuestionnaireEdit/index.tsx | 67 ++++++ 23 files changed, 1431 insertions(+), 1 deletion(-) create mode 100644 src/components/questionPreviews/DateQuestionPreview/index.module.css create mode 100644 src/components/questionPreviews/DateQuestionPreview/index.tsx create mode 100644 src/components/questionPreviews/FileQuestionPreview/index.module.css create mode 100644 src/components/questionPreviews/FileQuestionPreview/index.tsx create mode 100644 src/components/questionPreviews/ImageQuestionPreview/index.module.css create mode 100644 src/components/questionPreviews/ImageQuestionPreview/index.tsx create mode 100644 src/components/questionPreviews/NoteQuestionPreview/index.module.css create mode 100644 src/components/questionPreviews/NoteQuestionPreview/index.tsx create mode 100644 src/components/questionPreviews/TimeQuestionPreview/index.module.css create mode 100644 src/components/questionPreviews/TimeQuestionPreview/index.tsx create mode 100644 src/views/QuestionnaireEdit/DateQuestionForm/index.module.css create mode 100644 src/views/QuestionnaireEdit/DateQuestionForm/index.tsx create mode 100644 src/views/QuestionnaireEdit/FileQuestionForm/index.module.css create mode 100644 src/views/QuestionnaireEdit/FileQuestionForm/index.tsx create mode 100644 src/views/QuestionnaireEdit/ImageQuestionForm/index.module.css create mode 100644 src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx create mode 100644 src/views/QuestionnaireEdit/NoteQuestionForm/index.module.css create mode 100644 src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx create mode 100644 src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css create mode 100644 src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx diff --git a/src/components/questionPreviews/DateQuestionPreview/index.module.css b/src/components/questionPreviews/DateQuestionPreview/index.module.css new file mode 100644 index 0000000..ec321db --- /dev/null +++ b/src/components/questionPreviews/DateQuestionPreview/index.module.css @@ -0,0 +1,6 @@ +.preview { + display: flex; + flex-direction: column; + padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); + gap: var(--dui-spacing-medium); +} diff --git a/src/components/questionPreviews/DateQuestionPreview/index.tsx b/src/components/questionPreviews/DateQuestionPreview/index.tsx new file mode 100644 index 0000000..83901fb --- /dev/null +++ b/src/components/questionPreviews/DateQuestionPreview/index.tsx @@ -0,0 +1,46 @@ +import { + MdOutlineCalendarMonth, +} from 'react-icons/md'; +import { + _cs, +} from '@togglecorp/fujs'; +import { + TextOutput, + DateInput, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + className?: string; + label?: string; + hint?: string | null; +} + +function DateQuestionPreview(props: Props) { + const { + className, + label, + hint, + } = props; + + return ( +
+ + } + readOnly + /> +
+ ); +} + +export default DateQuestionPreview; diff --git a/src/components/questionPreviews/FileQuestionPreview/index.module.css b/src/components/questionPreviews/FileQuestionPreview/index.module.css new file mode 100644 index 0000000..23548fe --- /dev/null +++ b/src/components/questionPreviews/FileQuestionPreview/index.module.css @@ -0,0 +1,26 @@ +.preview { + display: flex; + flex-direction: column; + padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); + gap: var(--dui-spacing-medium); + + .upload-preview-wrapper { + align-items: flex-start; + gap: var(--dui-spacing-extra-large); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } + + .upload-preview { + flex-direction: column; + justify-content: center; + border: dashed var(--dui-width-separator-thin) var(--dui-color-secondary); + border-radius: var(--dui-border-radius-card); + padding: var(--dui-spacing-large) 0; + text-align: center; + } + } + +} diff --git a/src/components/questionPreviews/FileQuestionPreview/index.tsx b/src/components/questionPreviews/FileQuestionPreview/index.tsx new file mode 100644 index 0000000..a67099e --- /dev/null +++ b/src/components/questionPreviews/FileQuestionPreview/index.tsx @@ -0,0 +1,58 @@ +import { + IoDocumentTextOutline, + IoCloudUploadSharp, +} from 'react-icons/io5'; +import { + _cs, +} from '@togglecorp/fujs'; +import { + Button, + Element, + TextOutput, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + className?: string; + label?: string; + hint?: string | null; +} + +function FileQuestionPreview(props: Props) { + const { + className, + label, + hint, + } = props; + + return ( +
+ + } + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + + Drag and drop files here +
+ Or + +
+
+ ); +} + +export default FileQuestionPreview; diff --git a/src/components/questionPreviews/ImageQuestionPreview/index.module.css b/src/components/questionPreviews/ImageQuestionPreview/index.module.css new file mode 100644 index 0000000..23548fe --- /dev/null +++ b/src/components/questionPreviews/ImageQuestionPreview/index.module.css @@ -0,0 +1,26 @@ +.preview { + display: flex; + flex-direction: column; + padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); + gap: var(--dui-spacing-medium); + + .upload-preview-wrapper { + align-items: flex-start; + gap: var(--dui-spacing-extra-large); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } + + .upload-preview { + flex-direction: column; + justify-content: center; + border: dashed var(--dui-width-separator-thin) var(--dui-color-secondary); + border-radius: var(--dui-border-radius-card); + padding: var(--dui-spacing-large) 0; + text-align: center; + } + } + +} diff --git a/src/components/questionPreviews/ImageQuestionPreview/index.tsx b/src/components/questionPreviews/ImageQuestionPreview/index.tsx new file mode 100644 index 0000000..252380d --- /dev/null +++ b/src/components/questionPreviews/ImageQuestionPreview/index.tsx @@ -0,0 +1,58 @@ +import { + IoCameraOutline, + IoCloudUploadSharp, +} from 'react-icons/io5'; +import { + _cs, +} from '@togglecorp/fujs'; +import { + Button, + Element, + TextOutput, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + className?: string; + label?: string; + hint?: string | null; +} + +function ImageQuestionPreview(props: Props) { + const { + className, + label, + hint, + } = props; + + return ( +
+ + } + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + + Drag and drop photo here +
+ Or + +
+
+ ); +} + +export default ImageQuestionPreview; diff --git a/src/components/questionPreviews/NoteQuestionPreview/index.module.css b/src/components/questionPreviews/NoteQuestionPreview/index.module.css new file mode 100644 index 0000000..ec321db --- /dev/null +++ b/src/components/questionPreviews/NoteQuestionPreview/index.module.css @@ -0,0 +1,6 @@ +.preview { + display: flex; + flex-direction: column; + padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); + gap: var(--dui-spacing-medium); +} diff --git a/src/components/questionPreviews/NoteQuestionPreview/index.tsx b/src/components/questionPreviews/NoteQuestionPreview/index.tsx new file mode 100644 index 0000000..a8650a5 --- /dev/null +++ b/src/components/questionPreviews/NoteQuestionPreview/index.tsx @@ -0,0 +1,43 @@ +import { + MdOutlineAbc, +} from 'react-icons/md'; +import { + _cs, +} from '@togglecorp/fujs'; +import { + TextOutput, + TextInput, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + className?: string; + label?: string; +} + +function NoteQuestionPreview(props: Props) { + const { + className, + label, + } = props; + + return ( +
+ + } + readOnly + /> +
+ ); +} + +export default NoteQuestionPreview; diff --git a/src/components/questionPreviews/TimeQuestionPreview/index.module.css b/src/components/questionPreviews/TimeQuestionPreview/index.module.css new file mode 100644 index 0000000..ec321db --- /dev/null +++ b/src/components/questionPreviews/TimeQuestionPreview/index.module.css @@ -0,0 +1,6 @@ +.preview { + display: flex; + flex-direction: column; + padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); + gap: var(--dui-spacing-medium); +} diff --git a/src/components/questionPreviews/TimeQuestionPreview/index.tsx b/src/components/questionPreviews/TimeQuestionPreview/index.tsx new file mode 100644 index 0000000..60150fe --- /dev/null +++ b/src/components/questionPreviews/TimeQuestionPreview/index.tsx @@ -0,0 +1,46 @@ +import { + MdOutlineSchedule, +} from 'react-icons/md'; +import { + _cs, +} from '@togglecorp/fujs'; +import { + TextOutput, + TimeInput, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + className?: string; + label?: string; + hint?: string | null; +} + +function DateQuestionPreview(props: Props) { + const { + className, + label, + hint, + } = props; + + return ( +
+ + } + readOnly + /> +
+ ); +} + +export default DateQuestionPreview; diff --git a/src/views/QuestionnaireEdit/DateQuestionForm/index.module.css b/src/views/QuestionnaireEdit/DateQuestionForm/index.module.css new file mode 100644 index 0000000..77597e1 --- /dev/null +++ b/src/views/QuestionnaireEdit/DateQuestionForm/index.module.css @@ -0,0 +1,15 @@ +.question { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-large); + + .preview { + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + } + .edit-section { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-medium); + } +} diff --git a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx new file mode 100644 index 0000000..bcff3d0 --- /dev/null +++ b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx @@ -0,0 +1,192 @@ +import { useCallback } from 'react'; +import { + randomString, +} from '@togglecorp/fujs'; +import { + Button, + TextInput, + useAlert, +} from '@the-deep/deep-ui'; +import { gql, useMutation } from '@apollo/client'; +import { + ObjectSchema, + createSubmitHandler, + requiredStringCondition, + getErrorObject, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { + CreateDateQuestionMutation, + CreateDateQuestionMutationVariables, + QuestionCreateInput, + QuestionTypeEnum, +} from '#generated/types'; +import DateQuestionPreview from '#components/questionPreviews/DateQuestionPreview'; + +import styles from './index.module.css'; + +const CREATE_DATE_QUESTION = gql` + mutation CreateDateQuestion( + $projectId: ID!, + $input: QuestionCreateInput!, + ){ + private { + projectScope(pk: $projectId) { + createQuestion( + data: $input, + ) { + ok + errors + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: () : FormSchemaFields => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + type: { + required: true, + requiredValidation: requiredStringCondition, + }, + questionnaire: { + required: true, + requiredValidation: requiredStringCondition, + }, + label: { + required: true, + requiredValidation: requiredStringCondition, + }, + hint: {}, + }), +}; + +interface Props { + projectId: string; + questionnaireId: string; +} + +function DateQuestionForm(props: Props) { + const { + projectId, + questionnaireId, + } = props; + + const alert = useAlert(); + + const [ + triggerQuestionCreate, + { loading: createQuestionPending }, + ] = useMutation( + CREATE_DATE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.createQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question created successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + }, + }, + ); + const initialFormValue: FormType = { + type: 'DATE' as QuestionTypeEnum, + questionnaire: questionnaireId, + name: randomString(), + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const handleQuestionSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (valueFromForm) => { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + }, + ); + handler(); + }, [ + triggerQuestionCreate, + projectId, + setError, + validate, + ]); + + return ( +
+ +
+
+ + +
+ + + ); +} + +export default DateQuestionForm; diff --git a/src/views/QuestionnaireEdit/FileQuestionForm/index.module.css b/src/views/QuestionnaireEdit/FileQuestionForm/index.module.css new file mode 100644 index 0000000..88a597a --- /dev/null +++ b/src/views/QuestionnaireEdit/FileQuestionForm/index.module.css @@ -0,0 +1,16 @@ +.question { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-large); + + .preview { + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + } + + .edit-section { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-medium); + } +} diff --git a/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx new file mode 100644 index 0000000..67bf2ce --- /dev/null +++ b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx @@ -0,0 +1,191 @@ +import { useCallback } from 'react'; +import { + randomString, +} from '@togglecorp/fujs'; +import { + TextInput, + Button, + useAlert, +} from '@the-deep/deep-ui'; +import { gql, useMutation } from '@apollo/client'; +import { + ObjectSchema, + createSubmitHandler, + requiredStringCondition, + getErrorObject, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { + CreateFileQuestionMutation, + CreateFileQuestionMutationVariables, + QuestionCreateInput, + QuestionTypeEnum, +} from '#generated/types'; +import FileQuestionPreview from '#components/questionPreviews/FileQuestionPreview'; + +import styles from './index.module.css'; + +const CREATE_FILE_QUESTION = gql` + mutation CreateFileQuestion( + $projectId: ID!, + $input: QuestionCreateInput!, + ){ + private { + projectScope(pk: $projectId) { + createQuestion( + data: $input, + ) { + ok + errors + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: () : FormSchemaFields => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + type: { + required: true, + requiredValidation: requiredStringCondition, + }, + questionnaire: { + required: true, + requiredValidation: requiredStringCondition, + }, + label: { + required: true, + requiredValidation: requiredStringCondition, + }, + hint: {}, + }), +}; + +interface Props { + projectId: string; + questionnaireId: string; +} + +function FileQuestionForm(props: Props) { + const { + projectId, + questionnaireId, + } = props; + + const alert = useAlert(); + + const [ + triggerQuestionCreate, + { loading: createQuestionPending }, + ] = useMutation( + CREATE_FILE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.createQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question created successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + }, + }, + ); + const initialFormValue: FormType = { + type: 'FILE' as QuestionTypeEnum, + questionnaire: questionnaireId, + name: randomString(), + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const handleQuestionSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (valueFromForm) => { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + }, + ); + handler(); + }, [ + triggerQuestionCreate, + projectId, + setError, + validate, + ]); + + return ( +
+ +
+ + +
+ + + ); +} + +export default FileQuestionForm; diff --git a/src/views/QuestionnaireEdit/ImageQuestionForm/index.module.css b/src/views/QuestionnaireEdit/ImageQuestionForm/index.module.css new file mode 100644 index 0000000..88a597a --- /dev/null +++ b/src/views/QuestionnaireEdit/ImageQuestionForm/index.module.css @@ -0,0 +1,16 @@ +.question { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-large); + + .preview { + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + } + + .edit-section { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-medium); + } +} diff --git a/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx new file mode 100644 index 0000000..f85133c --- /dev/null +++ b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx @@ -0,0 +1,191 @@ +import { useCallback } from 'react'; +import { + randomString, +} from '@togglecorp/fujs'; +import { + TextInput, + Button, + useAlert, +} from '@the-deep/deep-ui'; +import { gql, useMutation } from '@apollo/client'; +import { + ObjectSchema, + createSubmitHandler, + requiredStringCondition, + getErrorObject, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { + CreateImageQuestionMutation, + CreateImageQuestionMutationVariables, + QuestionCreateInput, + QuestionTypeEnum, +} from '#generated/types'; +import ImageQuestionPreview from '#components/questionPreviews/ImageQuestionPreview'; + +import styles from './index.module.css'; + +const CREATE_IMAGE_QUESTION = gql` + mutation CreateImageQuestion( + $projectId: ID!, + $input: QuestionCreateInput!, + ){ + private { + projectScope(pk: $projectId) { + createQuestion( + data: $input, + ) { + ok + errors + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: () : FormSchemaFields => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + type: { + required: true, + requiredValidation: requiredStringCondition, + }, + questionnaire: { + required: true, + requiredValidation: requiredStringCondition, + }, + label: { + required: true, + requiredValidation: requiredStringCondition, + }, + hint: {}, + }), +}; + +interface Props { + projectId: string; + questionnaireId: string; +} + +function ImageQuestionForm(props: Props) { + const { + projectId, + questionnaireId, + } = props; + + const alert = useAlert(); + + const [ + triggerQuestionCreate, + { loading: createQuestionPending }, + ] = useMutation( + CREATE_IMAGE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.createQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question created successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + }, + }, + ); + const initialFormValue: FormType = { + type: 'IMAGE' as QuestionTypeEnum, + questionnaire: questionnaireId, + name: randomString(), + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const handleQuestionSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (valueFromForm) => { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + }, + ); + handler(); + }, [ + triggerQuestionCreate, + projectId, + setError, + validate, + ]); + + return ( +
+ +
+ + +
+ + + ); +} + +export default ImageQuestionForm; diff --git a/src/views/QuestionnaireEdit/NoteQuestionForm/index.module.css b/src/views/QuestionnaireEdit/NoteQuestionForm/index.module.css new file mode 100644 index 0000000..88a597a --- /dev/null +++ b/src/views/QuestionnaireEdit/NoteQuestionForm/index.module.css @@ -0,0 +1,16 @@ +.question { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-large); + + .preview { + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + } + + .edit-section { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-medium); + } +} diff --git a/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx new file mode 100644 index 0000000..54af7fd --- /dev/null +++ b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx @@ -0,0 +1,182 @@ +import { useCallback } from 'react'; +import { + randomString, +} from '@togglecorp/fujs'; +import { + TextInput, + Button, + useAlert, +} from '@the-deep/deep-ui'; +import { gql, useMutation } from '@apollo/client'; +import { + ObjectSchema, + createSubmitHandler, + requiredStringCondition, + getErrorObject, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { + CreateNoteQuestionMutation, + CreateNoteQuestionMutationVariables, + QuestionCreateInput, + QuestionTypeEnum, +} from '#generated/types'; +import NoteQuestionPreview from '#components/questionPreviews/NoteQuestionPreview'; + +import styles from './index.module.css'; + +const CREATE_NOTE_QUESTION = gql` + mutation CreateNoteQuestion( + $projectId: ID!, + $input: QuestionCreateInput!, + ){ + private { + projectScope(pk: $projectId) { + createQuestion( + data: $input, + ) { + ok + errors + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: () : FormSchemaFields => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + type: { + required: true, + requiredValidation: requiredStringCondition, + }, + questionnaire: { + required: true, + requiredValidation: requiredStringCondition, + }, + label: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +interface Props { + projectId: string; + questionnaireId: string; +} + +function NoteQuestionForm(props: Props) { + const { + projectId, + questionnaireId, + } = props; + + const alert = useAlert(); + + const [ + triggerQuestionCreate, + { loading: createQuestionPending }, + ] = useMutation( + CREATE_NOTE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.createQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question created successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + }, + }, + ); + const initialFormValue: FormType = { + type: 'NOTE' as QuestionTypeEnum, + questionnaire: questionnaireId, + name: randomString(), + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const handleQuestionSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (valueFromForm) => { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + }, + ); + handler(); + }, [ + triggerQuestionCreate, + projectId, + setError, + validate, + ]); + + return ( +
+ +
+ +
+ + + ); +} + +export default NoteQuestionForm; diff --git a/src/views/QuestionnaireEdit/QuestionPreview/index.tsx b/src/views/QuestionnaireEdit/QuestionPreview/index.tsx index 6ce8f1b..7ef1a3a 100644 --- a/src/views/QuestionnaireEdit/QuestionPreview/index.tsx +++ b/src/views/QuestionnaireEdit/QuestionPreview/index.tsx @@ -11,6 +11,8 @@ import { import TextQuestionPreview from '#components/questionPreviews/TextQuestionPreview'; import IntegerQuestionPreview from '#components/questionPreviews/IntegerQuestionPreview'; import RankQuestionPreview from '#components/questionPreviews/RankQuestionPreview'; +import DateQuestionPreview from '#components/questionPreviews/DateQuestionPreview'; +import TimeQuestionPreview from '#components/questionPreviews/TimeQuestionPreview'; import styles from './index.module.css'; @@ -55,6 +57,20 @@ function QuestionPreview(props: QuestionProps) { hint={question.hint} /> )} + {(question.type === 'DATE') && ( + + )} + {(question.type === 'TIME') && ( + + )} ); } diff --git a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx index a536da5..1a7995c 100644 --- a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx @@ -116,7 +116,7 @@ function RankQuestionForm(props: Props) { }, ); const initialFormValue: FormType = { - type: 'TEXT' as QuestionTypeEnum, + type: 'RANK' as QuestionTypeEnum, questionnaire: questionnaireId, name: randomString(), }; diff --git a/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css b/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css new file mode 100644 index 0000000..77597e1 --- /dev/null +++ b/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css @@ -0,0 +1,15 @@ +.question { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-large); + + .preview { + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + } + .edit-section { + display: flex; + flex-direction: column; + gap: var(--dui-spacing-medium); + } +} diff --git a/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx new file mode 100644 index 0000000..910e7b9 --- /dev/null +++ b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx @@ -0,0 +1,192 @@ +import { useCallback } from 'react'; +import { + randomString, +} from '@togglecorp/fujs'; +import { + Button, + TextInput, + useAlert, +} from '@the-deep/deep-ui'; +import { gql, useMutation } from '@apollo/client'; +import { + ObjectSchema, + createSubmitHandler, + requiredStringCondition, + getErrorObject, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { + CreateTimeQuestionMutation, + CreateTimeQuestionMutationVariables, + QuestionCreateInput, + QuestionTypeEnum, +} from '#generated/types'; +import TimeQuestionPreview from '#components/questionPreviews/TimeQuestionPreview'; + +import styles from './index.module.css'; + +const CREATE_TIME_QUESTION = gql` + mutation CreateTimeQuestion( + $projectId: ID!, + $input: QuestionCreateInput!, + ){ + private { + projectScope(pk: $projectId) { + createQuestion( + data: $input, + ) { + ok + errors + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: () : FormSchemaFields => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + type: { + required: true, + requiredValidation: requiredStringCondition, + }, + questionnaire: { + required: true, + requiredValidation: requiredStringCondition, + }, + label: { + required: true, + requiredValidation: requiredStringCondition, + }, + hint: {}, + }), +}; + +interface Props { + projectId: string; + questionnaireId: string; +} + +function TimeQuestionForm(props: Props) { + const { + projectId, + questionnaireId, + } = props; + + const alert = useAlert(); + + const [ + triggerQuestionCreate, + { loading: createQuestionPending }, + ] = useMutation( + CREATE_TIME_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.createQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question created successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to create question.', + { variant: 'error' }, + ); + }, + }, + ); + const initialFormValue: FormType = { + type: 'TIME' as QuestionTypeEnum, + questionnaire: questionnaireId, + name: randomString(), + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const handleQuestionSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (valueFromForm) => { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + }, + ); + handler(); + }, [ + triggerQuestionCreate, + projectId, + setError, + validate, + ]); + + return ( +
+ +
+
+ + +
+ + + ); +} + +export default TimeQuestionForm; diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index c30a78b..d1218b9 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -4,13 +4,18 @@ import { } from 'react-router-dom'; import { IoAdd, + IoCameraOutline, IoCloseOutline, + IoDocumentTextOutline, IoRadioButtonOn, } from 'react-icons/io5'; import { MdOutline123, MdOutlineAbc, + MdOutlineCalendarMonth, MdOutlineChecklist, + MdOutlineEditNote, + MdOutlineSchedule, } from 'react-icons/md'; import { isNotDefined, @@ -44,7 +49,14 @@ import { import TextQuestionForm from './TextQuestionForm'; import IntegerQuestionForm from './IntegerQuestionForm'; import RankQuestionForm from './RankQuestionForm'; +import DateQuestionForm from './DateQuestionForm'; +import TimeQuestionForm from './TimeQuestionForm'; +import NoteQuestionForm from './NoteQuestionForm'; +import FileQuestionForm from './FileQuestionForm'; +import ImageQuestionForm from './ImageQuestionForm'; import SelectOneQuestionForm from './SelectOneQuestionForm'; +import SelectMultipleQuestionForm from './SelectMultipleQuestionForm'; + import QuestionTypeItem, { QuestionType } from './QuestionTypeItem'; import QuestionPreview from './QuestionPreview'; import SelectMultipleQuestionForm from './SelectMultipleQuestionForm'; @@ -157,6 +169,31 @@ const questionTypes: QuestionType[] = [ name: 'Rank', icon: , }, + { + key: 'DATE', + name: 'Date', + icon: , + }, + { + key: 'TIME', + name: 'Time', + icon: , + }, + { + key: 'IMAGE', + name: 'Image', + icon: , + }, + { + key: 'FILE', + name: 'File', + icon: , + }, + { + key: 'NOTE', + name: 'Note', + icon: , + }, ]; const questionTypeKeySelector = (q: QuestionType) => q.key; @@ -427,6 +464,36 @@ export function Component() { questionnaireId={questionnaireId} /> )} + {(selectedQuestionType === 'DATE') && ( + + )} + {(selectedQuestionType === 'TIME') && ( + + )} + {(selectedQuestionType === 'NOTE') && ( + + )} + {(selectedQuestionType === 'FILE') && ( + + )} + {(selectedQuestionType === 'IMAGE') && ( + + )}
)} From 3f727215ca6adb63ce4e2fa1b0e73abf719b9430 Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 23 Aug 2023 13:59:53 +0545 Subject: [PATCH 5/6] Add edit functionality in question types - Integer question types - Text question types - Rank question types - Note question types - Date question types - Time question types - File question types - Image question types --- .../DateQuestionForm/index.tsx | 195 ++++++++++++++--- .../FileQuestionForm/index.tsx | 195 ++++++++++++++--- .../ImageQuestionForm/index.tsx | 196 ++++++++++++++--- .../IntegerQuestionForm/index.tsx | 191 ++++++++++++++--- .../NoteQuestionForm/index.tsx | 196 ++++++++++++++--- .../QuestionPreview/index.module.css | 6 + .../QuestionPreview/index.tsx | 157 ++++++++++---- .../RankQuestionForm/index.tsx | 197 +++++++++++++++--- .../TextQuestionForm/index.tsx | 192 ++++++++++++++--- .../TimeQuestionForm/index.tsx | 196 ++++++++++++++--- src/views/QuestionnaireEdit/index.tsx | 44 ++-- src/views/QuestionnaireEdit/queries.ts | 54 +++++ 12 files changed, 1569 insertions(+), 250 deletions(-) create mode 100644 src/views/QuestionnaireEdit/queries.ts diff --git a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx index bcff3d0..2cca976 100644 --- a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { Button, TextInput, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateDateQuestionMutation, CreateDateQuestionMutationVariables, + UpdateDateQuestionMutation, + UpdateDateQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import DateQuestionPreview from '#components/questionPreviews/DateQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_DATE_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateDateQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,12 +55,38 @@ const CREATE_DATE_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } } `; +const UPDATE_DATE_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateDateQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } + } + } + } + } +`; type FormType = PartialForm; type FormSchema = ObjectSchema; type FormSchemaFields = ReturnType; @@ -67,6 +109,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +120,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function DateQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'DATE' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +212,67 @@ function DateQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'DATE' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_DATE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -176,12 +301,32 @@ function DateQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + +
diff --git a/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx index 67bf2ce..b8e7884 100644 --- a/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { TextInput, Button, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateFileQuestionMutation, CreateFileQuestionMutationVariables, + UpdateFileQuestionMutation, + UpdateFileQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import FileQuestionPreview from '#components/questionPreviews/FileQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_FILE_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateFileQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,12 +55,38 @@ const CREATE_FILE_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } } `; +const UPDATE_FILE_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateFileQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } + } + } + } + } +`; type FormType = PartialForm; type FormSchema = ObjectSchema; type FormSchemaFields = ReturnType; @@ -67,6 +109,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +120,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function FileQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'FILE' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +212,67 @@ function FileQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'FILE' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_FILE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -175,12 +300,32 @@ function FileQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + +
diff --git a/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx index f85133c..1cf0ecc 100644 --- a/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { TextInput, Button, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateImageQuestionMutation, CreateImageQuestionMutationVariables, + UpdateImageQuestionMutation, + UpdateImageQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import ImageQuestionPreview from '#components/questionPreviews/ImageQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_IMAGE_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateImageQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,12 +55,38 @@ const CREATE_IMAGE_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } } `; +const UPDATE_IMAGE_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateImageQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } + } + } + } + } +`; type FormType = PartialForm; type FormSchema = ObjectSchema; type FormSchemaFields = ReturnType; @@ -67,6 +109,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +120,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function ImageQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'IMAGE' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +212,66 @@ function ImageQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'IMAGE' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_IMAGE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -175,12 +299,32 @@ function ImageQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx b/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx index ae5b097..6d25e31 100644 --- a/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx @@ -1,13 +1,14 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isNotDefined, + isDefined, } from '@togglecorp/fujs'; import { Button, TextInput, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { gql, useMutation, useQuery } from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +21,25 @@ import { import { CreateIntegerQuestionMutation, CreateIntegerQuestionMutationVariables, + UpdateIntegerQuestionMutation, + UpdateIntegerQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import IntegerQuestionPreview from '#components/questionPreviews/IntegerQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_INTEGER_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateIntegerQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,12 +51,38 @@ const CREATE_INTEGER_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } } `; +const UPDATE_INTEGER_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateIntegerQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } + } + } + } + } +`; type FormType = PartialForm; type FormSchema = ObjectSchema; type FormSchemaFields = ReturnType; @@ -67,6 +105,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +116,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function IntegerQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'INTEGER' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +208,67 @@ function IntegerQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'INTEGER' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_INTEGER_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -176,12 +297,32 @@ function IntegerQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx index 54af7fd..56ae964 100644 --- a/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { TextInput, Button, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateNoteQuestionMutation, CreateNoteQuestionMutationVariables, + UpdateNoteQuestionMutation, + UpdateNoteQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import NoteQuestionPreview from '#components/questionPreviews/NoteQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_NOTE_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateNoteQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,6 +55,33 @@ const CREATE_NOTE_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } + } + } + } + } +`; + +const UPDATE_NOTE_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateNoteQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } } } } @@ -67,22 +110,77 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, }), }; interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function NoteQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'NOTE' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -114,39 +212,67 @@ function NoteQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'NOTE' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_NOTE_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -166,12 +292,32 @@ function NoteQuestionForm(props: Props) { error={fieldError?.label} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/QuestionPreview/index.module.css b/src/views/QuestionnaireEdit/QuestionPreview/index.module.css index 4398b11..c8f3629 100644 --- a/src/views/QuestionnaireEdit/QuestionPreview/index.module.css +++ b/src/views/QuestionnaireEdit/QuestionPreview/index.module.css @@ -1,9 +1,15 @@ .preview { border: none !important; background-color: var(--dui-color-background); + overflow-y: auto; gap: var(--dui-spacing-large); + .question-wrapper { + padding: var(--dui-spacing-medium); + } + .question-item { + flex-grow: 1; padding: var(--dui-spacing-extra-large); } } diff --git a/src/views/QuestionnaireEdit/QuestionPreview/index.tsx b/src/views/QuestionnaireEdit/QuestionPreview/index.tsx index 7ef1a3a..8f78eb7 100644 --- a/src/views/QuestionnaireEdit/QuestionPreview/index.tsx +++ b/src/views/QuestionnaireEdit/QuestionPreview/index.tsx @@ -1,8 +1,16 @@ +import { useCallback } from 'react'; +import { + IoEllipsisVertical, +} from 'react-icons/io5'; import { isNotDefined, + isDefined, } from '@togglecorp/fujs'; import { TabPanel, + Element, + QuickActionDropdownMenu, + DropdownMenuItem, } from '@the-deep/deep-ui'; import { @@ -13,6 +21,11 @@ import IntegerQuestionPreview from '#components/questionPreviews/IntegerQuestion import RankQuestionPreview from '#components/questionPreviews/RankQuestionPreview'; import DateQuestionPreview from '#components/questionPreviews/DateQuestionPreview'; import TimeQuestionPreview from '#components/questionPreviews/TimeQuestionPreview'; +import NoteQuestionPreview from '#components/questionPreviews/NoteQuestionPreview'; +import ImageQuestionPreview from '#components/questionPreviews/ImageQuestionPreview'; +import FileQuestionPreview from '#components/questionPreviews/FileQuestionPreview'; +import SelectOneQuestionPreview from '#components/questionPreviews/SelectOneQuestionPreview'; +import SelectMultipleQuestionPreview from '#components/questionPreviews/SelectMultipleQuestionPreview'; import styles from './index.module.css'; @@ -20,13 +33,32 @@ type Question = NonNullable void; + setSelectedQuestionType: React.Dispatch>; + projectId: string | undefined; + setActiveQuestionId: React.Dispatch>; } function QuestionPreview(props: QuestionProps) { const { question, + showAddQuestionPane, + setSelectedQuestionType, + setActiveQuestionId, + projectId, } = props; + const handleEditQuestionClick = useCallback((val: string) => { + showAddQuestionPane(); + setSelectedQuestionType(question.type); + setActiveQuestionId(val); + }, [ + showAddQuestionPane, + setSelectedQuestionType, + question.type, + setActiveQuestionId, + ]); + if (isNotDefined(question.groupId)) { return null; } @@ -36,41 +68,96 @@ function QuestionPreview(props: QuestionProps) { name={question.groupId} className={styles.preview} > - {(question.type === 'TEXT') && ( - - )} - {(question.type === 'INTEGER') && ( - - )} - {(question.type === 'RANK') && ( - - )} - {(question.type === 'DATE') && ( - - )} - {(question.type === 'TIME') && ( - - )} + } + variant="secondary" + > + + Edit question + + + )} + > + {(question.type === 'TEXT') && ( + + )} + {(question.type === 'INTEGER') && ( + + )} + {(question.type === 'RANK') && ( + + )} + {(question.type === 'DATE') && ( + + )} + {(question.type === 'TIME') && ( + + )} + {(question.type === 'NOTE') && ( + + )} + {(question.type === 'FILE') && ( + + )} + {(question.type === 'IMAGE') && ( + + )} + {(question.type === 'SELECT_ONE') && isDefined(projectId) && ( + + )} + {(question.type === 'SELECT_MULTIPLE') && isDefined(projectId) && ( + + )} + ); } diff --git a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx index 1a7995c..8783f48 100644 --- a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { TextInput, Button, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateRankQuestionMutation, CreateRankQuestionMutationVariables, + UpdateRankQuestionMutation, + UpdateRankQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import TextQuestionPreview from '#components/questionPreviews/TextQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_RANK_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateRankQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,6 +55,33 @@ const CREATE_RANK_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } + } + } + } + } +`; + +const UPDATE_RANK_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateRankQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } } } } @@ -67,6 +110,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +121,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function RankQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'RANK' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +213,66 @@ function RankQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'RANK' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_RANK_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -175,12 +300,32 @@ function RankQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx b/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx index cb57a84..5309ee9 100644 --- a/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx @@ -1,13 +1,14 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isNotDefined, + isDefined, } from '@togglecorp/fujs'; import { TextInput, Button, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { gql, useMutation, useQuery } from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +21,25 @@ import { import { CreateTextQuestionMutation, CreateTextQuestionMutationVariables, + UpdateTextQuestionMutation, + UpdateTextQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import TextQuestionPreview from '#components/questionPreviews/TextQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_TEXT_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateTextQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,6 +51,33 @@ const CREATE_TEXT_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } + } + } + } + } +`; + +const UPDATE_TEXT_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateTextQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } } } } @@ -67,6 +106,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +117,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function TextQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'TEXT' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +209,67 @@ function TextQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'TEXT' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_TEXT_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -175,12 +297,32 @@ function TextQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx index 910e7b9..69405c0 100644 --- a/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx @@ -1,13 +1,18 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { Button, TextInput, useAlert, } from '@the-deep/deep-ui'; -import { gql, useMutation } from '@apollo/client'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; import { ObjectSchema, createSubmitHandler, @@ -20,14 +25,25 @@ import { import { CreateTimeQuestionMutation, CreateTimeQuestionMutationVariables, + UpdateTimeQuestionMutation, + UpdateTimeQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, QuestionCreateInput, + QuestionUpdateInput, QuestionTypeEnum, } from '#generated/types'; import TimeQuestionPreview from '#components/questionPreviews/TimeQuestionPreview'; +import PillarSelectInput from '#components/PillarSelectInput'; +import { + QUESTION_FRAGMENT, + QUESTION_INFO, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_TIME_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateTimeQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -39,6 +55,33 @@ const CREATE_TIME_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } + } + } + } + } +`; + +const UPDATE_TIME_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateTimeQuestion( + $projectId: ID!, + $questionId: ID!, + $input: QuestionUpdateInput!, + ) { + private { + projectScope(pk: $projectId) { + updateQuestion ( + data: $input + id: $questionId, + ) { + ok + errors + result { + ...QuestionResponse + } } } } @@ -67,6 +110,10 @@ const schema: FormSchema = { required: true, requiredValidation: requiredStringCondition, }, + group: { + required: true, + requiredValidation: requiredStringCondition, + }, hint: {}, }), }; @@ -74,16 +121,67 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; } function TimeQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, } = props; const alert = useAlert(); + const initialFormValue: FormType = { + type: 'TIME' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const fieldError = getErrorObject(formError); + + const questionInfoVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(questionId)) { + return undefined; + } + return ({ + projectId, + questionId, + }); + }, [ + projectId, + questionId, + ]); + + useQuery( + QUESTION_INFO, + { + skip: isNotDefined(questionInfoVariables), + variables: questionInfoVariables, + onCompleted: (response) => { + const questionResponse = response.private.projectScope?.question; + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + group: questionResponse?.groupId, + hint: questionResponse?.hint, + }); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -115,39 +213,67 @@ function TimeQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'TIME' as QuestionTypeEnum, - questionnaire: questionnaireId, - name: randomString(), - }; - - const { - pristine, - validate, - value: formValue, - error: formError, - setFieldValue, - setError, - } = useForm(schema, { value: initialFormValue }); - const fieldError = getErrorObject(formError); + const [ + triggerQuestionUpdate, + { loading: updateQuestionPending }, + ] = useMutation( + UPDATE_TIME_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Question updated successfully.', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to update question.', + { variant: 'error' }, + ); + }, + }, + ); const handleQuestionSubmit = useCallback(() => { const handler = createSubmitHandler( validate, setError, (valueFromForm) => { - triggerQuestionCreate({ - variables: { - projectId, - input: valueFromForm as QuestionCreateInput, - }, - }); + if (isDefined(questionId)) { + triggerQuestionUpdate({ + variables: { + projectId, + questionId, + input: valueFromForm as QuestionUpdateInput, + }, + }); + } else { + triggerQuestionCreate({ + variables: { + projectId, + input: valueFromForm as QuestionCreateInput, + }, + }); + } }, ); handler(); }, [ triggerQuestionCreate, + triggerQuestionUpdate, + questionId, projectId, setError, validate, @@ -176,12 +302,32 @@ function TimeQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + + diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index d1218b9..2e57b33 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -57,9 +57,11 @@ import ImageQuestionForm from './ImageQuestionForm'; import SelectOneQuestionForm from './SelectOneQuestionForm'; import SelectMultipleQuestionForm from './SelectMultipleQuestionForm'; +import { + QUESTION_FRAGMENT, +} from './queries.ts'; import QuestionTypeItem, { QuestionType } from './QuestionTypeItem'; import QuestionPreview from './QuestionPreview'; -import SelectMultipleQuestionForm from './SelectMultipleQuestionForm'; import styles from './index.module.css'; @@ -99,10 +101,11 @@ const QUESTIONNAIRE = gql` `; const QUESTIONS_BY_GROUP = gql` + ${QUESTION_FRAGMENT} query QuestionsByGroup( $projectId: ID!, $questionnaireId: ID!, - $groupId: DjangoModelFilterInput, + $groupId: ID!, ) { private { projectScope(pk: $projectId) { @@ -112,7 +115,9 @@ const QUESTIONS_BY_GROUP = gql` questionnaire: { pk: $questionnaireId, }, - group: $groupId, + group: { + pk: $groupId, + }, includeChildGroup: true, } order: { @@ -123,14 +128,7 @@ const QUESTIONS_BY_GROUP = gql` limit offset items { - createdAt - hint - id - groupId - label - name - type - questionnaireId + ...QuestionResponse } } } @@ -217,6 +215,11 @@ export function Component() { setSelectedQuestionType, ] = useState(); + const [ + activeQuestionId, + setActiveQuestionId, + ] = useState(); + const [ selectedGroups, setSelectedGroups, @@ -292,7 +295,7 @@ export function Component() { return ({ projectId, questionnaireId, - activeGroupTab: finalSelectedTab, + groupId: finalSelectedTab, }); }, [ projectId, @@ -319,7 +322,14 @@ export function Component() { const questionRendererParams = useCallback((_: string, data: Question) => ({ question: data, - }), []); + showAddQuestionPane, + setSelectedQuestionType, + projectId, + setActiveQuestionId, + }), [ + showAddQuestionPane, + projectId, + ]); const groupTabRenderParams = useCallback((_: string, datum: QuestionGroup) => ({ children: datum.label, @@ -438,18 +448,21 @@ export function Component() { )} {(selectedQuestionType === 'INTEGER') && ( )} {(selectedQuestionType === 'RANK') && ( )} {(selectedQuestionType === 'SELECT_ONE') && ( @@ -468,30 +481,35 @@ export function Component() { )} {(selectedQuestionType === 'TIME') && ( )} {(selectedQuestionType === 'NOTE') && ( )} {(selectedQuestionType === 'FILE') && ( )} {(selectedQuestionType === 'IMAGE') && ( )} diff --git a/src/views/QuestionnaireEdit/queries.ts b/src/views/QuestionnaireEdit/queries.ts new file mode 100644 index 0000000..4a8f963 --- /dev/null +++ b/src/views/QuestionnaireEdit/queries.ts @@ -0,0 +1,54 @@ +import { gql } from '@apollo/client'; + +export const QUESTION_FRAGMENT = gql` + fragment QuestionResponse on QuestionType { + id + label + name + type + hint + groupId + questionnaireId + choiceCollection { + id + choices { + collectionId + id + label + name + } + } + } +`; + +export const QUESTION_INFO = gql` + query QuestionInfo ( + $projectId: ID!, + $questionId: ID!, + ) { + private { + projectScope(pk: $projectId) { + question(pk: $questionId) { + id + label + name + type + hint + groupId + questionnaireId + choiceCollection { + id + choices { + collectionId + id + label + name + } + } + } + } + } + } +`; + +export default QUESTION_INFO; From 4b98279262aafdc0bfe6b2b4a1797c287ae57c2f Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 23 Aug 2023 18:37:26 +0545 Subject: [PATCH 6/6] Fix review comments --- .../DateQuestionForm/index.tsx | 4 ++++ .../FileQuestionForm/index.tsx | 4 ++++ .../ImageQuestionForm/index.tsx | 4 ++++ .../IntegerQuestionForm/index.tsx | 4 ++++ .../NoteQuestionForm/index.tsx | 4 ++++ .../RankQuestionForm/index.tsx | 4 ++++ .../SelectMultipleQuestionForm/index.tsx | 10 ++++++++++ .../SelectOneQuestionForm/index.tsx | 10 ++++++++++ .../TextQuestionForm/index.tsx | 4 ++++ .../TimeQuestionForm/index.tsx | 4 ++++ src/views/QuestionnaireEdit/index.tsx | 19 +++++++++++++++++++ 11 files changed, 71 insertions(+) diff --git a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx index 2cca976..8050e96 100644 --- a/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/DateQuestionForm/index.tsx @@ -121,6 +121,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function DateQuestionForm(props: Props) { @@ -128,6 +129,7 @@ function DateQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -193,6 +195,7 @@ function DateQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -225,6 +228,7 @@ function DateQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx index b8e7884..26828d2 100644 --- a/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/FileQuestionForm/index.tsx @@ -121,6 +121,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function FileQuestionForm(props: Props) { @@ -128,6 +129,7 @@ function FileQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -193,6 +195,7 @@ function FileQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -225,6 +228,7 @@ function FileQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx index 1cf0ecc..9f44a19 100644 --- a/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/ImageQuestionForm/index.tsx @@ -121,6 +121,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function ImageQuestionForm(props: Props) { @@ -128,6 +129,7 @@ function ImageQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -193,6 +195,7 @@ function ImageQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -225,6 +228,7 @@ function ImageQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx b/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx index 6d25e31..8b0fc72 100644 --- a/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/IntegerQuestionForm/index.tsx @@ -117,6 +117,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function IntegerQuestionForm(props: Props) { @@ -124,6 +125,7 @@ function IntegerQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -189,6 +191,7 @@ function IntegerQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -221,6 +224,7 @@ function IntegerQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx index 56ae964..12f3fe5 100644 --- a/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/NoteQuestionForm/index.tsx @@ -121,6 +121,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function NoteQuestionForm(props: Props) { @@ -128,6 +129,7 @@ function NoteQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -193,6 +195,7 @@ function NoteQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -225,6 +228,7 @@ function NoteQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx index 8783f48..fa3e42e 100644 --- a/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/RankQuestionForm/index.tsx @@ -122,6 +122,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function RankQuestionForm(props: Props) { @@ -129,6 +130,7 @@ function RankQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -194,6 +196,7 @@ function RankQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -226,6 +229,7 @@ function RankQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/SelectMultipleQuestionForm/index.tsx b/src/views/QuestionnaireEdit/SelectMultipleQuestionForm/index.tsx index 66b68e4..ba0643d 100644 --- a/src/views/QuestionnaireEdit/SelectMultipleQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/SelectMultipleQuestionForm/index.tsx @@ -29,9 +29,13 @@ import SelectMultipleQuestionPreview from '#components/questionPreviews/SelectMu import PillarSelectInput from '#components/PillarSelectInput'; import ChoiceCollectionSelectInput from '#components/ChoiceCollectionSelectInput'; +import { + QUESTION_FRAGMENT, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_MULTIPLE_SELECTION_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateMultipleSelectionQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -43,6 +47,9 @@ const CREATE_MULTIPLE_SELECTION_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } @@ -86,12 +93,14 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + onSuccess: (questionId: string | undefined) => void; } function SelectMultipleQuestionForm(props: Props) { const { projectId, questionnaireId, + onSuccess, } = props; const alert = useAlert(); @@ -111,6 +120,7 @@ function SelectMultipleQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx index a062699..4eea940 100644 --- a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx @@ -30,9 +30,13 @@ import SelectOneQuestionPreview from '#components/questionPreviews/SelectOneQues import PillarSelectInput from '#components/PillarSelectInput'; import ChoiceCollectionSelectInput from '#components/ChoiceCollectionSelectInput'; +import { + QUESTION_FRAGMENT, +} from '../queries.ts'; import styles from './index.module.css'; const CREATE_SINGLE_SELECTION_QUESTION = gql` + ${QUESTION_FRAGMENT} mutation CreateSingleSelectionQuestion( $projectId: ID!, $input: QuestionCreateInput!, @@ -44,6 +48,9 @@ const CREATE_SINGLE_SELECTION_QUESTION = gql` ) { ok errors + result { + ...QuestionResponse + } } } } @@ -87,12 +94,14 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + onSuccess: (questionId: string | undefined) => void; } function SelectOneQuestionForm(props: Props) { const { projectId, questionnaireId, + onSuccess, } = props; const alert = useAlert(); @@ -111,6 +120,7 @@ function SelectOneQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx b/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx index 5309ee9..76d4cd3 100644 --- a/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/TextQuestionForm/index.tsx @@ -118,6 +118,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function TextQuestionForm(props: Props) { @@ -125,6 +126,7 @@ function TextQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -190,6 +192,7 @@ function TextQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -222,6 +225,7 @@ function TextQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx index 69405c0..80b624e 100644 --- a/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/TimeQuestionForm/index.tsx @@ -122,6 +122,7 @@ interface Props { projectId: string; questionnaireId: string; questionId?: string; + onSuccess: (questionId: string | undefined) => void; } function TimeQuestionForm(props: Props) { @@ -129,6 +130,7 @@ function TimeQuestionForm(props: Props) { projectId, questionnaireId, questionId, + onSuccess, } = props; const alert = useAlert(); @@ -194,6 +196,7 @@ function TimeQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question created successfully.', { variant: 'success' }, @@ -226,6 +229,7 @@ function TimeQuestionForm(props: Props) { return; } if (response.ok) { + onSuccess(response.result?.id); alert.show( 'Question updated successfully.', { variant: 'success' }, diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index 2e57b33..eb678f6 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -305,6 +305,7 @@ export function Component() { const { data: questionsResponse, + refetch: retriggerQuestions, } = useQuery( QUESTIONS_BY_GROUP, { @@ -313,6 +314,14 @@ export function Component() { }, ); + const handleQuestionCreateSuccess = useCallback(() => { + hideAddQuestionPane(); + retriggerQuestions(); + }, [ + hideAddQuestionPane, + retriggerQuestions, + ]); + const questionsData = questionsResponse?.private.projectScope?.questions?.items; const questionTypeRendererParams = useCallback((key: string, data: QuestionType) => ({ questionType: data, @@ -449,6 +458,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'INTEGER') && ( @@ -456,6 +466,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'RANK') && ( @@ -463,18 +474,21 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'SELECT_ONE') && ( )} {(selectedQuestionType === 'SELECT_MULTIPLE') && ( )} {(selectedQuestionType === 'DATE') && ( @@ -482,6 +496,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'TIME') && ( @@ -489,6 +504,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'NOTE') && ( @@ -496,6 +512,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'FILE') && ( @@ -503,6 +520,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )} {(selectedQuestionType === 'IMAGE') && ( @@ -510,6 +528,7 @@ export function Component() { projectId={projectId} questionnaireId={questionnaireId} questionId={activeQuestionId} + onSuccess={handleQuestionCreateSuccess} /> )}