diff --git a/src/App/routes.tsx b/src/App/routes.tsx index 97ed1bf..a9844ee 100644 --- a/src/App/routes.tsx +++ b/src/App/routes.tsx @@ -101,6 +101,18 @@ const resetPassword = myWrapRoute({ }, }); +const forgotPassword = myWrapRoute({ + path: 'forgot-password', + component: { + render: () => import('#views/Login/ForgotPassword'), + props: {}, + }, + context: { + title: 'Forgot Password', + visibility: 'anything', + }, +}); + // FIXME: eager load this page const resetPasswordRedirect = myWrapRoute({ path: '/permalink/password-reset/:uuid/:token', @@ -162,6 +174,7 @@ export const wrappedRoutes = { login, register, resetPassword, + forgotPassword, resetPasswordRedirect, projectEdit, questionnaireEdit, diff --git a/src/components/ChoiceCollectionSelectInput/index.tsx b/src/components/ChoiceCollectionSelectInput/index.tsx index 5b70c38..c3b5fe0 100644 --- a/src/components/ChoiceCollectionSelectInput/index.tsx +++ b/src/components/ChoiceCollectionSelectInput/index.tsx @@ -1,7 +1,12 @@ import { useMemo, useState } from 'react'; -import { isNotDefined } from '@togglecorp/fujs'; +import { + isNotDefined, +} from '@togglecorp/fujs'; import { gql, useQuery } from '@apollo/client'; -import { SearchSelectInput } from '@the-deep/deep-ui'; +import { + SearchSelectInput, + SearchSelectInputProps, +} from '@the-deep/deep-ui'; import { ChoiceCollectionsQuery, @@ -36,38 +41,40 @@ const CHOICE_COLLECTIONS = gql` } `; -type ChoiceCollection = NonNullable['choiceCollections']['items'][number]; +type ChoiceCollection = Omit['choiceCollections']['items'][number], '__typename'>; const choiceCollectionKeySelector = (d: ChoiceCollection) => d.id; const choiceCollectionLabelSelector = (d: ChoiceCollection) => d.label; -interface Props { +type Def = { containerClassName?: string }; +type ChoiceCollectionSelectInputProps< + K extends string, + GK extends string +> = SearchSelectInputProps< + string, + K, + GK, + ChoiceCollection, + Def, + 'onSearchValueChange' | 'searchOptions' | 'optionsPending' | 'keySelector' | 'labelSelector' | 'totalOptionsCount' | 'onShowDropdownChange' +> & { projectId: string; questionnaireId: string | null; - name: T; - label: string; - onChange: (value: string | undefined, name: T) => void; - value: string | null | undefined; - error: string | undefined; -} +}; -function ChoiceCollectionSelectInput(props: Props) { +const PAGE_SIZE = 20; + +function ChoiceCollectionSelectInput< + K extends string, + GK extends string +>(props: ChoiceCollectionSelectInputProps) { const { projectId, questionnaireId, - name, - value, - label, - onChange, - error, + ...otherProps } = props; - const [ - choiceCollectionOptions, - setChoiceCollectionOptions, - ] = useState(); - - const [search, setSearch] = useState(); + const [searchText, setSearchText] = useState(); const [opened, setOpened] = useState(false); const optionsVariables = useMemo(() => { @@ -78,12 +85,14 @@ function ChoiceCollectionSelectInput(props: Props) { return ({ projectId, questionnaireId, - search, + search: searchText, + limit: PAGE_SIZE, + offset: 0, }); }, [ projectId, questionnaireId, - search, + searchText, ]); const { @@ -96,23 +105,19 @@ function ChoiceCollectionSelectInput(props: Props) { skip: isNotDefined(optionsVariables) || !opened, variables: optionsVariables, }); - const searchOption = choiceCollectionsResponse?.private.projectScope?.choiceCollections.items; + + const options = choiceCollectionsResponse?.private.projectScope?.choiceCollections.items; return ( ); } diff --git a/src/components/Navbar/index.module.css b/src/components/Navbar/index.module.css index 537b5cb..5d2a328 100644 --- a/src/components/Navbar/index.module.css +++ b/src/components/Navbar/index.module.css @@ -43,6 +43,10 @@ color: var(--dui-color-primary); font-size: var(--dui-font-size-small); font-weight: var(--dui-font-weight-bold); + + &.active { + border-color: var(--dui-color-primary); + } } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 75e9411..74a54d1 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,6 +1,13 @@ import { useContext, useMemo, useCallback } from 'react'; -import { NavLink, Link } from 'react-router-dom'; +import { + NavLink, + Link, + useLocation, +} from 'react-router-dom'; import { IoLogOutOutline } from 'react-icons/io5'; +import { + _cs, +} from '@togglecorp/fujs'; import { gql, useMutation } from '@apollo/client'; import { DropdownMenu, @@ -36,6 +43,8 @@ function Navbar(props: Props) { header, } = props; + const location = useLocation(); + const { userDetails } = useContext(UserContext); const alert = useAlert(); @@ -104,7 +113,10 @@ function Navbar(props: Props) {
My Projects @@ -135,7 +147,6 @@ function Navbar(props: Props) { Profile } diff --git a/src/components/UserSelectInput/index.tsx b/src/components/UserSelectInput/index.tsx index 4b1b2a8..7b74f07 100644 --- a/src/components/UserSelectInput/index.tsx +++ b/src/components/UserSelectInput/index.tsx @@ -48,10 +48,14 @@ export type User = Omit u.id; const labelSelector = (u: User) => u.displayName; + const PAGE_SIZE = 20; type Def = { containerClassName?: string }; -type UserSelectInputProps = SearchSelectInputProps< +type UserSelectInputProps< + K extends string, + GK extends string +> = SearchSelectInputProps< string, K, GK, diff --git a/src/components/questionPreviews/DateQuestionPreview/index.module.css b/src/components/questionPreviews/DateQuestionPreview/index.module.css index ec321db..2211aad 100644 --- a/src/components/questionPreviews/DateQuestionPreview/index.module.css +++ b/src/components/questionPreviews/DateQuestionPreview/index.module.css @@ -1,6 +1,10 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } } diff --git a/src/components/questionPreviews/DateQuestionPreview/index.tsx b/src/components/questionPreviews/DateQuestionPreview/index.tsx index 83901fb..955640d 100644 --- a/src/components/questionPreviews/DateQuestionPreview/index.tsx +++ b/src/components/questionPreviews/DateQuestionPreview/index.tsx @@ -7,6 +7,7 @@ import { import { TextOutput, DateInput, + Element, } from '@the-deep/deep-ui'; import styles from './index.module.css'; @@ -32,13 +33,18 @@ function DateQuestionPreview(props: Props) { spacing="none" block /> - } - readOnly - /> + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + +
); } diff --git a/src/components/questionPreviews/FileQuestionPreview/index.module.css b/src/components/questionPreviews/FileQuestionPreview/index.module.css index 23548fe..3f0555a 100644 --- a/src/components/questionPreviews/FileQuestionPreview/index.module.css +++ b/src/components/questionPreviews/FileQuestionPreview/index.module.css @@ -1,7 +1,6 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); .upload-preview-wrapper { diff --git a/src/components/questionPreviews/ImageQuestionPreview/index.module.css b/src/components/questionPreviews/ImageQuestionPreview/index.module.css index 23548fe..3f0555a 100644 --- a/src/components/questionPreviews/ImageQuestionPreview/index.module.css +++ b/src/components/questionPreviews/ImageQuestionPreview/index.module.css @@ -1,7 +1,6 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); .upload-preview-wrapper { diff --git a/src/components/questionPreviews/IntegerQuestionPreview/index.module.css b/src/components/questionPreviews/IntegerQuestionPreview/index.module.css index ec321db..2211aad 100644 --- a/src/components/questionPreviews/IntegerQuestionPreview/index.module.css +++ b/src/components/questionPreviews/IntegerQuestionPreview/index.module.css @@ -1,6 +1,10 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } } diff --git a/src/components/questionPreviews/IntegerQuestionPreview/index.tsx b/src/components/questionPreviews/IntegerQuestionPreview/index.tsx index ea0588d..d76d8e6 100644 --- a/src/components/questionPreviews/IntegerQuestionPreview/index.tsx +++ b/src/components/questionPreviews/IntegerQuestionPreview/index.tsx @@ -7,6 +7,7 @@ import { import { TextOutput, NumberInput, + Element, } from '@the-deep/deep-ui'; import styles from './index.module.css'; @@ -32,13 +33,18 @@ function IntegerQuestionPreview(props: Props) { spacing="none" block /> - } - readOnly - /> + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + +
); } diff --git a/src/components/questionPreviews/NoteQuestionPreview/index.module.css b/src/components/questionPreviews/NoteQuestionPreview/index.module.css index ec321db..2211aad 100644 --- a/src/components/questionPreviews/NoteQuestionPreview/index.module.css +++ b/src/components/questionPreviews/NoteQuestionPreview/index.module.css @@ -1,6 +1,10 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } } diff --git a/src/components/questionPreviews/NoteQuestionPreview/index.tsx b/src/components/questionPreviews/NoteQuestionPreview/index.tsx index a8650a5..2269d61 100644 --- a/src/components/questionPreviews/NoteQuestionPreview/index.tsx +++ b/src/components/questionPreviews/NoteQuestionPreview/index.tsx @@ -1,12 +1,12 @@ import { - MdOutlineAbc, + MdOutlineEditNote, } from 'react-icons/md'; import { _cs, } from '@togglecorp/fujs'; import { + Element, TextOutput, - TextInput, } from '@the-deep/deep-ui'; import styles from './index.module.css'; @@ -24,18 +24,17 @@ function NoteQuestionPreview(props: Props) { return (
- - } - readOnly - /> + } + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + +
); } diff --git a/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.module.css b/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.module.css new file mode 100644 index 0000000..e25ef0c --- /dev/null +++ b/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.module.css @@ -0,0 +1,13 @@ +.item-wrapper { + display: flex; + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: var(--dui-border-radius-card); + + .icon { + background-color: var(--color-gray2); + padding: var(--dui-spacing-small); + } + .item { + padding: var(--dui-spacing-small); + } +} diff --git a/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.tsx b/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.tsx new file mode 100644 index 0000000..b82c322 --- /dev/null +++ b/src/components/questionPreviews/RankQuestionPreview/RankQuestionItem/index.tsx @@ -0,0 +1,31 @@ +import { + GrDrag, +} from 'react-icons/gr'; +import { + Element, +} from '@the-deep/deep-ui'; + +import styles from './index.module.css'; + +interface Props { + title: string; +} + +function RankQuestionItem(props: Props) { + const { + title, + } = props; + + return ( + } + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.item} + > + {title} + + ); +} + +export default RankQuestionItem; diff --git a/src/components/questionPreviews/RankQuestionPreview/index.module.css b/src/components/questionPreviews/RankQuestionPreview/index.module.css index ec321db..7bc0d1c 100644 --- a/src/components/questionPreviews/RankQuestionPreview/index.module.css +++ b/src/components/questionPreviews/RankQuestionPreview/index.module.css @@ -1,6 +1,22 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .choices-preview { + align-items: flex-start; + gap: var(--dui-spacing-large); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } + + .choices { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--dui-spacing-small); + } + } } diff --git a/src/components/questionPreviews/RankQuestionPreview/index.tsx b/src/components/questionPreviews/RankQuestionPreview/index.tsx index c8741ca..19e972e 100644 --- a/src/components/questionPreviews/RankQuestionPreview/index.tsx +++ b/src/components/questionPreviews/RankQuestionPreview/index.tsx @@ -1,16 +1,60 @@ +import { useMemo, useCallback } from 'react'; +import { + IoSwapVertical, +} from 'react-icons/io5'; import { _cs, + isNotDefined, } from '@togglecorp/fujs'; import { + gql, + useQuery, +} from '@apollo/client'; +import { + Element, + ListView, TextOutput, } from '@the-deep/deep-ui'; +import { + RankChoicesQuery, + RankChoicesQueryVariables, +} from '#generated/types'; + +import RankQuestionItem from './RankQuestionItem'; import styles from './index.module.css'; +const RANK_CHOICES = gql` + query RankChoices ( + $projectId: ID!, + $choiceCollectionId: ID!, + ) { + private { + projectScope(pk: $projectId) { + choiceCollection(pk: $choiceCollectionId) { + id + name + label + choices { + id + label + name + } + } + } + } + } +`; + +type ChoiceType = NonNullable['projectScope']>['choiceCollection']>['choices']>[number]; +const rankChoiceKeySelector = (c: ChoiceType) => c.id; + interface Props { className?: string; label?: string; hint?: string | null; + choiceCollectionId: string | undefined | null; + projectId: string; } function RankQuestionPreview(props: Props) { @@ -18,8 +62,39 @@ function RankQuestionPreview(props: Props) { className, label, hint, + choiceCollectionId, + projectId, } = props; + const rankChoicesVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(choiceCollectionId)) { + return undefined; + } + return ({ + projectId, + choiceCollectionId, + }); + }, [ + projectId, + choiceCollectionId, + ]); + + const { + data: optionsListResponse, + } = useQuery( + RANK_CHOICES, + { + skip: isNotDefined(rankChoicesVariables), + variables: rankChoicesVariables, + }, + ); + + const choices = optionsListResponse?.private?.projectScope?.choiceCollection?.choices ?? []; + + const rankChoiceRendererParams = useCallback((_: string, datum: ChoiceType) => ({ + title: datum.label, + }), []); + return (
+ } + iconsContainerClassName={styles.icon} + > + +
); } diff --git a/src/components/questionPreviews/SelectMultipleQuestionPreview/index.module.css b/src/components/questionPreviews/SelectMultipleQuestionPreview/index.module.css index 2f92235..21765d2 100644 --- a/src/components/questionPreviews/SelectMultipleQuestionPreview/index.module.css +++ b/src/components/questionPreviews/SelectMultipleQuestionPreview/index.module.css @@ -1,13 +1,23 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); - .checkbox-list { - display: flex; - flex-direction: column; - gap: var(--dui-spacing-small); - width: 10rem; + .choices-preview { + align-items: flex-start; + gap: var(--dui-spacing-large); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } + + .choices { + display: flex; + flex-direction: column; + flex-grow: 1; + border: none; + gap: var(--dui-spacing-small); + } } } diff --git a/src/components/questionPreviews/SelectMultipleQuestionPreview/index.tsx b/src/components/questionPreviews/SelectMultipleQuestionPreview/index.tsx index ff61f15..c0d4bea 100644 --- a/src/components/questionPreviews/SelectMultipleQuestionPreview/index.tsx +++ b/src/components/questionPreviews/SelectMultipleQuestionPreview/index.tsx @@ -1,4 +1,7 @@ import { useCallback, useMemo } from 'react'; +import { + MdOutlineChecklist, +} from 'react-icons/md'; import { gql, useQuery } from '@apollo/client'; import { _cs, @@ -7,6 +10,7 @@ import { } from '@togglecorp/fujs'; import { Checkbox, + Element, ListView, TextOutput, } from '@the-deep/deep-ui'; @@ -97,16 +101,22 @@ function SelectMultipleQuestionPreview(props: Props) { spacing="none" block /> - + } + iconsContainerClassName={styles.icon} + > + + ); } diff --git a/src/components/questionPreviews/SelectOneQuestionPreview/index.module.css b/src/components/questionPreviews/SelectOneQuestionPreview/index.module.css index 8a243df..7bc0d1c 100644 --- a/src/components/questionPreviews/SelectOneQuestionPreview/index.module.css +++ b/src/components/questionPreviews/SelectOneQuestionPreview/index.module.css @@ -1,14 +1,22 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); - .question-list { - display: flex; + .choices-preview { align-items: flex-start; - flex-direction: column; - gap: var(--dui-spacing-small); - width: 10rem; + gap: var(--dui-spacing-large); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } + + .choices { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--dui-spacing-small); + } } } diff --git a/src/components/questionPreviews/SelectOneQuestionPreview/index.tsx b/src/components/questionPreviews/SelectOneQuestionPreview/index.tsx index 9580762..428bc42 100644 --- a/src/components/questionPreviews/SelectOneQuestionPreview/index.tsx +++ b/src/components/questionPreviews/SelectOneQuestionPreview/index.tsx @@ -1,4 +1,7 @@ import { useMemo } from 'react'; +import { + IoRadioButtonOn, +} from 'react-icons/io5'; import { _cs, isNotDefined, @@ -6,6 +9,7 @@ import { } from '@togglecorp/fujs'; import { gql, useQuery } from '@apollo/client'; import { + Element, RadioInput, TextOutput, } from '@the-deep/deep-ui'; @@ -95,18 +99,24 @@ function SelectOneQuestionPreview(props: Props) { spacing="none" block /> - + } + iconsContainerClassName={styles.icon} + > + + ); } diff --git a/src/components/questionPreviews/TextQuestionPreview/index.module.css b/src/components/questionPreviews/TextQuestionPreview/index.module.css index ec321db..2211aad 100644 --- a/src/components/questionPreviews/TextQuestionPreview/index.module.css +++ b/src/components/questionPreviews/TextQuestionPreview/index.module.css @@ -1,6 +1,10 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } } diff --git a/src/components/questionPreviews/TextQuestionPreview/index.tsx b/src/components/questionPreviews/TextQuestionPreview/index.tsx index cd73706..ab95b99 100644 --- a/src/components/questionPreviews/TextQuestionPreview/index.tsx +++ b/src/components/questionPreviews/TextQuestionPreview/index.tsx @@ -5,6 +5,7 @@ import { _cs, } from '@togglecorp/fujs'; import { + Element, TextOutput, TextInput, } from '@the-deep/deep-ui'; @@ -32,13 +33,18 @@ function TextQuestionPreview(props: Props) { spacing="none" block /> - } - readOnly - /> + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + + ); } diff --git a/src/components/questionPreviews/TimeQuestionPreview/index.module.css b/src/components/questionPreviews/TimeQuestionPreview/index.module.css index ec321db..2211aad 100644 --- a/src/components/questionPreviews/TimeQuestionPreview/index.module.css +++ b/src/components/questionPreviews/TimeQuestionPreview/index.module.css @@ -1,6 +1,10 @@ .preview { display: flex; flex-direction: column; - padding: var(--dui-spacing-extra-large) var(--dui-spacing-large); gap: var(--dui-spacing-medium); + + .icon { + color: var(--dui-color-primary); + font-size: var(--dui-font-size-extra-large); + } } diff --git a/src/components/questionPreviews/TimeQuestionPreview/index.tsx b/src/components/questionPreviews/TimeQuestionPreview/index.tsx index 60150fe..d141bec 100644 --- a/src/components/questionPreviews/TimeQuestionPreview/index.tsx +++ b/src/components/questionPreviews/TimeQuestionPreview/index.tsx @@ -5,6 +5,7 @@ import { _cs, } from '@togglecorp/fujs'; import { + Element, TextOutput, TimeInput, } from '@the-deep/deep-ui'; @@ -32,13 +33,18 @@ function DateQuestionPreview(props: Props) { spacing="none" block /> - } - readOnly - /> + iconsContainerClassName={styles.icon} + childrenContainerClassName={styles.uploadPreview} + > + + ); } diff --git a/src/views/Home/ProjectItem/index.module.css b/src/views/Home/ProjectItem/index.module.css index 4915d23..056e114 100644 --- a/src/views/Home/ProjectItem/index.module.css +++ b/src/views/Home/ProjectItem/index.module.css @@ -3,7 +3,6 @@ border: unset; border-radius: 0; background-color: transparent; - padding: var(--dui-spacing-medium); width: 100%; text-align: left; @@ -11,7 +10,18 @@ background-color: var(--dui-color-secondary); } - .description { - display: flex; + .clickable { + flex-grow: 1; + cursor: pointer; + padding: var(--dui-spacing-medium); + + .description { + display: flex; + } + } + .menu { + align-self: flex-start; + flex-shrink: 0; + margin: var(--dui-spacing-medium); } } diff --git a/src/views/Home/ProjectItem/index.tsx b/src/views/Home/ProjectItem/index.tsx index e5265e7..78dacf8 100644 --- a/src/views/Home/ProjectItem/index.tsx +++ b/src/views/Home/ProjectItem/index.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { IoEllipsisVerticalOutline, } from 'react-icons/io5'; @@ -8,7 +9,6 @@ import { _cs } from '@togglecorp/fujs'; import { TextOutput, DateOutput, - Button, QuickActionDropdownMenu, DropdownMenuItem, } from '@the-deep/deep-ui'; @@ -39,38 +39,50 @@ function ProjectItem(props: Props) { const link = generatePath(wrappedRoutes.projectEdit.absolutePath, { projectId }); + const handleProjectItemClick = useCallback(() => { + onProjectItemClick(projectId); + }, [ + onProjectItemClick, + projectId, + ]); + return ( - +
+ + Created on +   + +
+ )} + block + spacing="compact" + /> + + } + > + + Edit Project + + + ); } diff --git a/src/views/Login/ForgotPassword/index.module.css b/src/views/Login/ForgotPassword/index.module.css new file mode 100644 index 0000000..c0c0987 --- /dev/null +++ b/src/views/Login/ForgotPassword/index.module.css @@ -0,0 +1,59 @@ +.login { + display: flex; + position: relative; + align-items: center; + justify-content: space-around; + background-color: var(--background-color); + width: 100vw; + height: 100vh; + overflow: hidden; + + &:before { + display: block; + position: absolute; + top: 0; + clip-path: polygon(0% 0%, 40% 0, 75% 50%, 40% 100%, 0% 100%); + border: 1px solid #F0F0F0; + background-color: #FCFAFF; + width: 100vw; + height: 100vh; + content: ''; + } + + .logo-container { + display: flex; + align-items: center; + flex-direction: column; + z-index: 1; + max-width: 30rem; + text-align: center; + + .logo { + width: 30rem; + } + } + + .login-form { + display: flex; + flex-direction: column; + z-index: 1; + border: 0.5px solid #D2D2D2; + border-radius: 30px; + background: #fff; + padding: var(--dui-spacing-super-large) var(--dui-spacing-mega-large); + gap: var(--dui-spacing-large); + + .button { + align-self: center; + } + + .footnote { + display: flex; + align-items: baseline; + .footnote-button { + padding: var(--dui-spacing-extra-small); + color: var(--dui-color-primary); + } + } + } +} diff --git a/src/views/Login/ForgotPassword/index.tsx b/src/views/Login/ForgotPassword/index.tsx new file mode 100644 index 0000000..14022e3 --- /dev/null +++ b/src/views/Login/ForgotPassword/index.tsx @@ -0,0 +1,180 @@ +import { useRef, useCallback } from 'react'; +import { useMutation, gql } from '@apollo/client'; +import { + useAlert, + TextInput, + Header, + Button, +} from '@the-deep/deep-ui'; +import { + ObjectSchema, + emailCondition, + getErrorObject, + requiredStringCondition, + createSubmitHandler, + useForm, + PartialForm, +} from '@togglecorp/toggle-form'; +import Captcha from '@hcaptcha/react-hcaptcha'; + +import HCaptcha from '#components/HCaptcha'; +import { hCaptchaKey } from '#configs/hCaptcha'; +import { + PasswordResetTriggerInput, + ForgotPasswordMutation, + ForgotPasswordMutationVariables, +} from '#generated/types'; + +import styles from './index.module.css'; + +const FORGOT_PASSWORD = gql` + mutation ForgotPassword( + $input: PasswordResetTriggerInput!, + ) { + public { + passwordResetTrigger(data: $input) { + ok + errors + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const schema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + validations: [ + emailCondition, + ], + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +const initialValue: FormType = {}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const elementRef = useRef(null); + const alert = useAlert(); + + const [ + forgotPasswordTrigger, + { loading: forgotPasswordPending }, + ] = useMutation( + FORGOT_PASSWORD, + { + onCompleted: (resetResponse) => { + const response = resetResponse?.public?.passwordResetTrigger; + if (!response) { + return; + } + if (response.ok) { + alert.show( + 'Password changed successfully!', + { variant: 'success' }, + ); + } else { + alert.show( + 'Failed to change password!', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to change password!', + { variant: 'error' }, + ); + }, + }, + ); + + const { + // FIXME: use pristine on submit value + pristine, + validate, + value: formValue, + error, + setFieldValue, + setError, + } = useForm(schema, { value: initialValue }); + + const handleSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (val) => { + elementRef.current?.resetCaptcha(); + // eslint-disable-next-line no-console + console.log('submit value', val); + forgotPasswordTrigger({ + variables: { + input: val as PasswordResetTriggerInput, + }, + }); + }, + ); + + handler(); + }, [ + forgotPasswordTrigger, + validate, + setError, + ]); + + const safeError = getErrorObject(error); + + return ( +
+
+ Questionnaire Builder Logo +
+
+
+ + + +
+
+ ); +} + +Component.displayName = 'ForgotPassword'; diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index 6fab38e..750b02e 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -29,14 +29,14 @@ import { import styles from './index.module.css'; const LOGIN = gql` -mutation Login($input: LoginInput!) { - public { - login(data: $input) { - ok - errors + mutation Login($input: LoginInput!) { + public { + login(data: $input) { + ok + errors + } } } -} `; type FormType = PartialForm; @@ -141,11 +141,6 @@ export function Component() { className={styles.logo} alt="Questionnaire Builder Logo" /> -
- Lorem ipsum dolor sit amet consectetur. - jahsdhjakasdbkjabd jkhdakjhasdkjbnajksd - b -
- + diff --git a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.module.css b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.module.css index 88a597a..97261c0 100644 --- a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.module.css +++ b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.module.css @@ -6,6 +6,7 @@ .preview { border: var(--dui-width-separator-thin) solid var(--dui-color-separator); border-radius: var(--dui-border-radius-card); + padding: var(--dui-spacing-extra-large); } .edit-section { diff --git a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx index 3ea61d0..911e176 100644 --- a/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx +++ b/src/views/QuestionnaireEdit/SelectOneQuestionForm/index.tsx @@ -1,10 +1,12 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { - randomString, + isDefined, + isNotDefined, } from '@togglecorp/fujs'; import { gql, useMutation, + useQuery, } from '@apollo/client'; import { Button, @@ -13,6 +15,7 @@ import { } from '@the-deep/deep-ui'; import { ObjectSchema, + removeNull, createSubmitHandler, requiredStringCondition, getErrorObject, @@ -21,10 +24,15 @@ import { } from '@togglecorp/toggle-form'; import { - QuestionCreateInput, - QuestionTypeEnum, CreateSingleSelectionQuestionMutation, CreateSingleSelectionQuestionMutationVariables, + UpdateSingleSelectionQuestionMutation, + UpdateSingleSelectionQuestionMutationVariables, + QuestionInfoQuery, + QuestionInfoQueryVariables, + QuestionCreateInput, + QuestionUpdateInput, + QuestionTypeEnum, } from '#generated/types'; import SelectOneQuestionPreview from '#components/questionPreviews/SelectOneQuestionPreview'; import PillarSelectInput from '#components/PillarSelectInput'; @@ -32,6 +40,8 @@ import ChoiceCollectionSelectInput from '#components/ChoiceCollectionSelectInput import { QUESTION_FRAGMENT, + QUESTION_INFO, + ChoiceCollectionType, } from '../queries.ts'; import styles from './index.module.css'; @@ -57,6 +67,30 @@ const CREATE_SINGLE_SELECTION_QUESTION = gql` } `; +const UPDATE_SINGLE_SELECTION_QUESTION = gql` + ${QUESTION_FRAGMENT} + mutation UpdateSingleSelectionQuestion( + $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; @@ -94,6 +128,7 @@ const schema: FormSchema = { interface Props { projectId: string; questionnaireId: string; + questionId?: string; onSuccess: (questionId: string | undefined) => void; } @@ -101,10 +136,72 @@ function SelectOneQuestionForm(props: Props) { const { projectId, questionnaireId, + questionId, onSuccess, } = props; const alert = useAlert(); + + const initialFormValue: FormType = { + type: 'SELECT_ONE' as QuestionTypeEnum, + questionnaire: questionnaireId, + }; + + const { + pristine, + validate, + value: formValue, + error: formError, + setFieldValue, + setValue, + setError, + } = useForm(schema, { value: initialFormValue }); + + const [ + choiceCollectionOption, + setChoiceCollectionOption, + ] = useState(); + + 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 = removeNull(response.private.projectScope?.question); + setValue({ + name: questionResponse?.name, + type: questionResponse?.type, + questionnaire: questionResponse?.questionnaireId, + label: questionResponse?.label, + leafGroup: questionResponse?.leafGroupId, + hint: questionResponse?.hint, + choiceCollection: questionResponse?.choiceCollection?.id, + }); + const choiceCollection = questionResponse?.choiceCollection; + const choiceCollectionOptions = isDefined(choiceCollection) + ? [choiceCollection] + : []; + setChoiceCollectionOption(choiceCollectionOptions); + }, + }, + ); + const [ triggerQuestionCreate, { loading: createQuestionPending }, @@ -140,39 +237,71 @@ function SelectOneQuestionForm(props: Props) { }, }, ); - const initialFormValue: FormType = { - type: 'SELECT_ONE' 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< + UpdateSingleSelectionQuestionMutation, + UpdateSingleSelectionQuestionMutationVariables + >( + UPDATE_SINGLE_SELECTION_QUESTION, + { + onCompleted: (questionResponse) => { + const response = questionResponse?.private?.projectScope?.updateQuestion; + if (!response) { + return; + } + if (response.ok) { + onSuccess(response.result?.id); + 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, @@ -202,10 +331,19 @@ function SelectOneQuestionForm(props: Props) { error={fieldError?.hint} onChange={setFieldValue} /> + Apply diff --git a/src/views/QuestionnaireEdit/TextQuestionForm/index.module.css b/src/views/QuestionnaireEdit/TextQuestionForm/index.module.css index 88a597a..97261c0 100644 --- a/src/views/QuestionnaireEdit/TextQuestionForm/index.module.css +++ b/src/views/QuestionnaireEdit/TextQuestionForm/index.module.css @@ -6,6 +6,7 @@ .preview { border: var(--dui-width-separator-thin) solid var(--dui-color-separator); border-radius: var(--dui-border-radius-card); + padding: var(--dui-spacing-extra-large); } .edit-section { diff --git a/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css b/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css index 77597e1..249c579 100644 --- a/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css +++ b/src/views/QuestionnaireEdit/TimeQuestionForm/index.module.css @@ -6,6 +6,7 @@ .preview { border: var(--dui-width-separator-thin) solid var(--dui-color-separator); border-radius: var(--dui-border-radius-card); + padding: var(--dui-spacing-extra-large); } .edit-section { display: flex; diff --git a/src/views/QuestionnaireEdit/index.module.css b/src/views/QuestionnaireEdit/index.module.css index 11c0784..24090a9 100644 --- a/src/views/QuestionnaireEdit/index.module.css +++ b/src/views/QuestionnaireEdit/index.module.css @@ -65,6 +65,18 @@ padding: var(--dui-spacing-large); } + .tabs { + display: flex; + border-bottom: var(--dui-width-separator-thin) solid var(--dui-color-separator); + + .tab { + &.active { + background-color: var(--dui-color-secondary); + border-bottom: var(--dui-width-separator-medium) solid var(--dui-color-accent); + } + } + } + .question-list { display: flex; flex-direction: column; diff --git a/src/views/QuestionnaireEdit/index.tsx b/src/views/QuestionnaireEdit/index.tsx index bab5ebf..d05c26c 100644 --- a/src/views/QuestionnaireEdit/index.tsx +++ b/src/views/QuestionnaireEdit/index.tsx @@ -8,6 +8,7 @@ import { IoCloseOutline, IoDocumentTextOutline, IoRadioButtonOn, + IoSwapVertical, } from 'react-icons/io5'; import { MdOutline123, @@ -40,6 +41,7 @@ import { } from '@the-deep/deep-ui'; import SubNavbar from '#components/SubNavbar'; +import SortableList from '#components/SortableList'; import TocList from '#components/TocList'; import { flatten } from '#utils/common'; import { @@ -173,7 +175,7 @@ const questionTypes: QuestionType[] = [ { key: 'RANK', name: 'Rank', - icon: , + icon: , }, { key: 'DATE', @@ -422,14 +424,22 @@ export function Component() { finalSelectedTab, ]); + const [ + orderedQuestions, + setOrderedQuestions, + ] = useState(); + const { - data: questionsResponse, refetch: retriggerQuestions, } = useQuery( QUESTIONS_BY_GROUP, { skip: isNotDefined(questionsVariables), variables: questionsVariables, + onCompleted: (response) => { + const questions = response?.private?.projectScope?.questions?.items; + setOrderedQuestions(questions); + }, }, ); @@ -441,7 +451,6 @@ export function Component() { retriggerQuestions, ]); - const questionsData = questionsResponse?.private.projectScope?.questions?.items; const questionTypeRendererParams = useCallback((key: string, data: QuestionType) => ({ questionType: data, name: key, @@ -459,9 +468,18 @@ export function Component() { projectId, ]); + const handleQuestionAdd = useCallback(() => { + showAddQuestionPane(); + setActiveQuestionId(undefined); + }, [ + showAddQuestionPane, + ]); + const groupTabRenderParams = useCallback((_: string, datum: QuestionGroup) => ({ children: datum.name, name: datum.id, + className: styles.tab, + activeClassName: styles.active, }), []); if (isNotDefined(projectId) || isNotDefined(questionnaireId)) { @@ -505,7 +523,7 @@ export function Component() { actions={(
diff --git a/src/views/ResetPassword/index.tsx b/src/views/ResetPassword/index.tsx index 134c677..573c01a 100644 --- a/src/views/ResetPassword/index.tsx +++ b/src/views/ResetPassword/index.tsx @@ -24,6 +24,7 @@ import { PartialForm, createSubmitHandler, getErrorObject, + undefinedValue, } from '@togglecorp/toggle-form'; import Captcha from '@hcaptcha/react-hcaptcha'; @@ -109,6 +110,7 @@ const schema: FormSchema = { samePasswordCondition, ], requiredValidation: requiredStringCondition, + forceValue: undefinedValue, }, }), }; @@ -220,11 +222,6 @@ export function Component() { className={styles.logo} alt="Questionnaire Builder Logo" /> -
- Lorem ipsum dolor sit amet consectetur. - jahsdhjakasdbkjabd jkhdakjhasdkjbnajksd - b -