From c848f6c185760d20083d9c3a1dba849fc37094c2 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 19 Aug 2022 14:42:50 -0300 Subject: [PATCH 01/36] temp updates --- .../client/src/components/Dropdown.js | 4 +- .../ProposalCreate/StepOne/FormConfig.js | 19 ++ .../ProposalCreate/StepOne/index.js | 259 ++++++++++-------- .../client/src/components/StepByStep/index.js | 53 ++-- .../client/src/components/common/Form.js | 15 +- .../client/src/pages/ProposalCreate.js | 1 + 6 files changed, 213 insertions(+), 138 deletions(-) create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js diff --git a/frontend/packages/client/src/components/Dropdown.js b/frontend/packages/client/src/components/Dropdown.js index b033e3342..8c9df47fa 100644 --- a/frontend/packages/client/src/components/Dropdown.js +++ b/frontend/packages/client/src/components/Dropdown.js @@ -26,7 +26,9 @@ const DropDown = forwardRef( const [isOpen, setIsOpen] = useState(false); const [innerValue, setInnerValue] = useState(defaultValue ?? { label }); - const openCloseDropdown = () => { + const openCloseDropdown = (e) => { + e.preventDefault(); + e.stopPropagation(); setIsOpen((status) => !status); }; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js new file mode 100644 index 000000000..3adc42c5c --- /dev/null +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -0,0 +1,19 @@ +import * as yup from 'yup'; + +const initialValues = { + name: '', +}; +const Schema = yup.object().shape({ + name: yup + .string() + .trim() + .required('Please enter a proposal title') + .max(150, 'The maximum length for title is 128 characters'), +}); + +const stepOne = { + Schema, + initialValues, +}; + +export { stepOne }; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index dfb5855f4..cad5aa762 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -6,12 +6,16 @@ import React, { useState, } from 'react'; import { Editor } from 'react-draft-wysiwyg'; +import { useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { useModalContext } from 'contexts/NotificationModal'; import { Dropdown, Error, UploadImageModal } from 'components'; import { Image } from 'components/Svg'; +import Form from 'components/common/Form'; +import Input from 'components/common/Input'; import { useCommunityDetails } from 'hooks'; import { kebabToString } from 'utils'; +import { yupResolver } from '@hookform/resolvers/yup'; import { AtomicBlockUtils, ContentState, @@ -21,6 +25,8 @@ import { SelectionState, } from 'draft-js'; import { Map } from 'immutable'; +import pick from 'lodash/pick'; +import { stepOne } from './FormConfig'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; @@ -65,6 +71,8 @@ const StepOne = ({ setStepValid, onDataChange, setPreCheckStepAdvance, + formRef, + moveToNextStep, }) => { const dropDownRef = useRef(); @@ -127,7 +135,7 @@ const StepOne = ({ const isValid = Object.keys(requiredFields).every( (field) => stepData && requiredFields[field](stepData[field]) ); - setStepValid(isValid); + setStepValid(true); }, [stepData, setStepValid, onDataChange, tabOption]); useEffect(() => { @@ -168,6 +176,7 @@ const StepOne = ({ const { strategy } = stepData ?? {}; + console.log('formId > ', formRef); useEffect(() => { setPreCheckStepAdvance(() => { if (!strategy) { @@ -384,6 +393,26 @@ const StepOne = ({ const showTitleInputError = !checkValidTitleLength(stepData?.title ?? ''); + const fieldsObj = Object.assign( + {}, + stepOne.initialValues, + pick(stepData || {}, ['title']) + ); + const { register, handleSubmit, formState, control, setValue, reset } = + useForm({ + defaultValues: fieldsObj, + resolver: yupResolver(stepOne.Schema), + }); + + const { isDirty, isSubmitting, errors, submit } = formState; + + const field = useWatch({ control, name: 'title' }); + console.log('watchinig ', field); + + const onSubmit = (data) => { + console.log('data submitted >>> ', data); + // moveToNextStep(); + }; return ( <> {showUploadImagesModal && ( @@ -392,126 +421,122 @@ const StepOne = ({ onDone={addImagesToEditor} /> )} -
-
-

- Title * -

-

- Give your proposal a title based on the decision or initiative being - voted on. Best to keep it simple and specific. -

- - onDataChange({ - title: event.target.value, - }) - } - /> - {showTitleInputError && ( -
-

- The maximum length for Title is 128 characters -

-
- )} -
-
-

- Description * -

-

- This is where you build the key information for the proposal: the - details of what’s being voted on; background information for - context; the expected costs and benefits of this collective - decision. -

- ]} - customStyleMap={styleMap} - blockRenderMap={extendedBlockRenderMap} - /> -
-
-

Voting Strategy

-

- Select a strategy for how voting power is calculated. Voting - strategies are set by community admins. -

- ({ - label: vs.name, - value: vs.key, - })) ?? [] - } - disabled={votingStrategies.length === 0} - onSelectValue={onSelectStrategy} - ref={dropDownRef} - /> -
-
-

- Choices * -

-

- Provide the specific options you’d like to cast votes for. Use - Text-based presentation for choices that described in words. Use - Visual for side-by-side visual options represented by images. -

-
-
    -
  • - -
  • -
  • - -
  • -
+ +
+ +
+
+

+ Title * +

+

+ Give your proposal a title based on the decision or initiative + being voted on. Best to keep it simple and specific. +

- {tabOption === 'text-based' && ( - +

+ Description * +

+

+ This is where you build the key information for the proposal: the + details of what’s being voted on; background information for + context; the expected costs and benefits of this collective + decision. +

+ ]} + customStyleMap={styleMap} + blockRenderMap={extendedBlockRenderMap} /> - )} - {tabOption === 'visual' && ( - +
+

Voting Strategy

+

+ Select a strategy for how voting power is calculated. Voting + strategies are set by community admins. +

+ ({ + label: vs.name, + value: vs.key, + })) ?? [] + } + disabled={votingStrategies.length === 0} + onSelectValue={onSelectStrategy} + ref={dropDownRef} /> - )} +
+
+

+ Choices * +

+

+ Provide the specific options you’d like to cast votes for. Use + Text-based presentation for choices that described in words. Use + Visual for side-by-side visual options represented by images. +

+
+
    +
  • + +
  • +
  • + +
  • +
+
+ {tabOption === 'text-based' && ( + + )} + {tabOption === 'visual' && ( + + )} +
-
+ + ); }; diff --git a/frontend/packages/client/src/components/StepByStep/index.js b/frontend/packages/client/src/components/StepByStep/index.js index 67432e226..140c4318b 100644 --- a/frontend/packages/client/src/components/StepByStep/index.js +++ b/frontend/packages/client/src/components/StepByStep/index.js @@ -20,6 +20,7 @@ function StepByStep({ const [isStepValid, setStepValid] = useState(false); const [stepsData, setStepsData] = useState({}); const refs = React.useRef(); + const formRef = React.useRef(); const onStepAdvance = (direction = 'next') => { if (direction === 'next') { @@ -122,6 +123,8 @@ function StepByStep({ const child = showPreStep ? preStep : steps[currentStep].component; + const { useHookForms = false } = steps[currentStep]; + const getBackLabel = () => (
( -
-
- Next + const getNextButton = ({ formRef } = {}) => { + console.log('formId', formRef); + return ( +
+ { + e.preventDefault(); + + formRef.current.dispatchEvent( + new Event('submit', { cancelable: true, bubbles: true }) + ); + } + : moveToNextStep + } + > + Next +
-
- ); + ); + }; const getSubmitButton = () => (
@@ -165,6 +179,7 @@ function StepByStep({
); + console.log('currentStep', steps[currentStep]); return ( <> {blockNavigationOut && ( @@ -203,7 +218,7 @@ function StepByStep({
{steps.map((step, i) => getStepIcon(i, step.label))}
{currentStep < steps.length - 1 && !passNextToComp && - getNextButton()} + getNextButton(useHookForms ? { formRef } : undefined)} {currentStep === steps.length - 1 && !passSubmitToComp && getSubmitButton()} @@ -257,15 +272,21 @@ function StepByStep({ ? { onSubmit: _onSubmit } : undefined), ...(showPreStep ? { dismissPreStep } : undefined), + formId: `form-Id-${currentStep}`, + ...(useHookForms ? { formRef } : undefined), })} -
+ {/*
{currentStep < steps.length - 1 && !passNextToComp && - getNextButton()} + getNextButton({ + formId: useHookForms ? `form-Id-${currentStep}` : undefined, + })} {currentStep === steps.length - 1 && !passSubmitToComp && - getSubmitButton()} -
+ getSubmitButton({ + formId: useHookForms ? `form-Id-${currentStep}` : undefined, + })} +
*/}
diff --git a/frontend/packages/client/src/components/common/Form.js b/frontend/packages/client/src/components/common/Form.js index 825635e0e..e33089744 100644 --- a/frontend/packages/client/src/components/common/Form.js +++ b/frontend/packages/client/src/components/common/Form.js @@ -1,9 +1,16 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; + +const Form = forwardRef((props, ref) => { + const { removeInnerForm, children, onSubmit, formId } = props; -export default function Form({ removeInnerForm, children, onSubmit }) { return removeInnerForm ? ( <>{children} ) : ( -
{children}
+
+ <>ddffdfdf {formId} + {children} +
); -} +}); + +export default Form; diff --git a/frontend/packages/client/src/pages/ProposalCreate.js b/frontend/packages/client/src/pages/ProposalCreate.js index 4fac40365..0301e4d49 100644 --- a/frontend/packages/client/src/pages/ProposalCreate.js +++ b/frontend/packages/client/src/pages/ProposalCreate.js @@ -123,6 +123,7 @@ export default function ProposalCreatePage() { description: 'Some description of what you can write here that is useful.', component: , + useHookForms: true, }, { label: 'Set Date & Time', From 004a626f4207a64d012a1e39471faa0f90076f8e Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 19 Aug 2022 16:53:36 -0300 Subject: [PATCH 02/36] updates --- .../ProposalCreate/StepOne/FormConfig.js | 2 +- .../ProposalCreate/StepOne/index.js | 58 +++++++------ .../components/StepByStep/NexStepButton.js | 27 ++++++ .../src/components/StepByStep/SubmitButton.js | 0 .../client/src/components/StepByStep/index.js | 86 ++++++++++--------- .../client/src/components/common/Form.js | 11 +-- 6 files changed, 112 insertions(+), 72 deletions(-) create mode 100644 frontend/packages/client/src/components/StepByStep/NexStepButton.js create mode 100644 frontend/packages/client/src/components/StepByStep/SubmitButton.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js index 3adc42c5c..4840b9a91 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -1,7 +1,7 @@ import * as yup from 'yup'; const initialValues = { - name: '', + title: '', }; const Schema = yup.object().shape({ name: yup diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index cad5aa762..b7a934fa1 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -71,7 +71,7 @@ const StepOne = ({ setStepValid, onDataChange, setPreCheckStepAdvance, - formRef, + formId, moveToNextStep, }) => { const dropDownRef = useRef(); @@ -176,7 +176,6 @@ const StepOne = ({ const { strategy } = stepData ?? {}; - console.log('formId > ', formRef); useEffect(() => { setPreCheckStepAdvance(() => { if (!strategy) { @@ -393,26 +392,41 @@ const StepOne = ({ const showTitleInputError = !checkValidTitleLength(stepData?.title ?? ''); + console.log('step data comes with', stepData); const fieldsObj = Object.assign( {}, stepOne.initialValues, pick(stepData || {}, ['title']) ); - const { register, handleSubmit, formState, control, setValue, reset } = - useForm({ - defaultValues: fieldsObj, - resolver: yupResolver(stepOne.Schema), - }); - const { isDirty, isSubmitting, errors, submit } = formState; + // const { register, handleSubmit, formState, control, setValue, reset } = + // useForm({ + + // resolver: yupResolver(stepOne.Schema), + // }); + + // const { isDirty, isSubmitting, errors, submit } = formState; - const field = useWatch({ control, name: 'title' }); - console.log('watchinig ', field); + // const field = useWatch({ control, name: 'example' }); + // console.log('watchinig ', field); + const { register, handleSubmit, watch, formState } = useForm({ + defaultValues: fieldsObj, + }); const onSubmit = (data) => { - console.log('data submitted >>> ', data); - // moveToNextStep(); + console.log('data on submit >>', data); + onDataChange(data); + moveToNextStep(); }; + + const { isDirty, isSubmitting, errors } = formState; + + console.log(watch('example')); // watch input value by passing the name of it + // const onSubmit = (data) => { + // console.log('data submitted >>> ', data); + // // moveToNextStep(); + // }; + return ( <> {showUploadImagesModal && ( @@ -422,13 +436,7 @@ const StepOne = ({ /> )} -
- +

@@ -438,6 +446,12 @@ const StepOne = ({ Give your proposal a title based on the decision or initiative being voted on. Best to keep it simple and specific.

+

@@ -532,11 +546,7 @@ const StepOne = ({ )}

- -
+ ); }; diff --git a/frontend/packages/client/src/components/StepByStep/NexStepButton.js b/frontend/packages/client/src/components/StepByStep/NexStepButton.js new file mode 100644 index 000000000..ab51f8ee5 --- /dev/null +++ b/frontend/packages/client/src/components/StepByStep/NexStepButton.js @@ -0,0 +1,27 @@ +import React from 'react'; + +const NextButton = ({ + formId: formIdParam, + moveToNextStep, + disabled = false, +} = {}) => { + const classNames = `button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered ${ + disabled && 'is-disabled' + }`; + + return ( +
+ {formIdParam ? ( + + ) : ( +
+ Next +
+ )} +
+ ); +}; + +export default NextButton; diff --git a/frontend/packages/client/src/components/StepByStep/SubmitButton.js b/frontend/packages/client/src/components/StepByStep/SubmitButton.js new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/packages/client/src/components/StepByStep/index.js b/frontend/packages/client/src/components/StepByStep/index.js index 140c4318b..9440d9b1b 100644 --- a/frontend/packages/client/src/components/StepByStep/index.js +++ b/frontend/packages/client/src/components/StepByStep/index.js @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; import { Prompt } from 'react-router-dom'; import Loader from '../Loader'; import { ArrowLeft, CheckMark } from '../Svg'; +import NextButton from './NexStepButton'; function StepByStep({ finalLabel, @@ -20,7 +21,6 @@ function StepByStep({ const [isStepValid, setStepValid] = useState(false); const [stepsData, setStepsData] = useState({}); const refs = React.useRef(); - const formRef = React.useRef(); const onStepAdvance = (direction = 'next') => { if (direction === 'next') { @@ -125,6 +125,8 @@ function StepByStep({ const { useHookForms = false } = steps[currentStep]; + const formId = useHookForms ? `form-Id-${currentStep}` : undefined; + const getBackLabel = () => (
{ - console.log('formId', formRef); + const getNextButton = ({ formId: formIdParam } = {}) => { + const classNames = `button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered ${ + !isStepValid && 'is-disabled' + }`; + return (
- { - e.preventDefault(); - - formRef.current.dispatchEvent( - new Event('submit', { cancelable: true, bubbles: true }) - ); - } - : moveToNextStep - } - > - Next - + {formIdParam ? ( + + ) : ( +
+ Next +
+ )}
); }; - const getSubmitButton = () => ( + const getSubmitButton = ({ formId: formIdParam } = {}) => (
-
- {finalLabel} -
+ {formIdParam ? ( + + ) : ( +
+ {finalLabel} +
+ )}
); @@ -218,10 +228,11 @@ function StepByStep({
{steps.map((step, i) => getStepIcon(i, step.label))}
{currentStep < steps.length - 1 && !passNextToComp && - getNextButton(useHookForms ? { formRef } : undefined)} + getNextButton({ formId })} + {currentStep === steps.length - 1 && !passSubmitToComp && - getSubmitButton()} + getSubmitButton({ formId })}
{/* left panel mobile */}
+
{currentStep < steps.length - 1 && !passNextToComp && - getNextButton({ - formId: useHookForms ? `form-Id-${currentStep}` : undefined, - })} + getNextButton({ formId })} {currentStep === steps.length - 1 && !passSubmitToComp && - getSubmitButton({ - formId: useHookForms ? `form-Id-${currentStep}` : undefined, - })} -
*/} + getSubmitButton({ formId })} +
diff --git a/frontend/packages/client/src/components/common/Form.js b/frontend/packages/client/src/components/common/Form.js index e33089744..5a74b2b1e 100644 --- a/frontend/packages/client/src/components/common/Form.js +++ b/frontend/packages/client/src/components/common/Form.js @@ -1,16 +1,13 @@ -import React, { forwardRef } from 'react'; - -const Form = forwardRef((props, ref) => { - const { removeInnerForm, children, onSubmit, formId } = props; +import React from 'react'; +const Form = ({ removeInnerForm = false, children, onSubmit, formId } = {}) => { return removeInnerForm ? ( <>{children} ) : ( -
- <>ddffdfdf {formId} + {children}
); -}); +}; export default Form; From 63df4f23579b5e5c214707da738f4891b1998004 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 19 Aug 2022 17:49:19 -0300 Subject: [PATCH 03/36] update --- .../ProposalCreate/StepOne/FormConfig.js | 13 ++- .../ProposalCreate/StepOne/index.js | 81 +++------------- .../components/StepByStep/NexStepButton.js | 10 +- .../src/components/StepByStep/SubmitButton.js | 28 ++++++ .../client/src/components/StepByStep/index.js | 93 +++++++------------ .../client/src/pages/CommunityCreate.js | 1 + .../client/src/pages/ProposalCreate.js | 2 + 7 files changed, 97 insertions(+), 131 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js index 4840b9a91..f98d949fc 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -1,19 +1,24 @@ import * as yup from 'yup'; -const initialValues = { - title: '', -}; +const formFields = ['title', 'strategy']; + const Schema = yup.object().shape({ - name: yup + title: yup .string() .trim() .required('Please enter a proposal title') .max(150, 'The maximum length for title is 128 characters'), + strategy: yup.string().required('Please select a strategy'), }); +const initialValues = Object.assign( + {}, + ...formFields.map((key) => ({ [key]: '' })) +); const stepOne = { Schema, initialValues, + formFields, }; export { stepOne }; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index b7a934fa1..3ecc13380 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -9,8 +9,9 @@ import { Editor } from 'react-draft-wysiwyg'; import { useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { useModalContext } from 'contexts/NotificationModal'; -import { Dropdown, Error, UploadImageModal } from 'components'; +import { Error, UploadImageModal } from 'components'; import { Image } from 'components/Svg'; +import Dropdown from 'components/common/Dropdown'; import Form from 'components/common/Form'; import Input from 'components/common/Input'; import { useCommunityDetails } from 'hooks'; @@ -30,8 +31,6 @@ import { stepOne } from './FormConfig'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; -const checkValidTitleLength = (text) => text?.length <= 128; - // using a React component to render custom blocks const ImageCaptionCustomBlock = (props) => { return
{props.children}
; @@ -105,7 +104,6 @@ const StepOne = ({ useEffect(() => { const requiredFields = { - title: (text) => text?.trim().length > 0 && checkValidTitleLength(text), description: (body) => body?.getCurrentContent().hasText(), choices: (opts) => { const getLabel = (o) => o?.value?.trim(); @@ -174,36 +172,6 @@ const StepOne = ({ }, }; - const { strategy } = stepData ?? {}; - - useEffect(() => { - setPreCheckStepAdvance(() => { - if (!strategy) { - openModal( - React.createElement(Error, { - error: ( -
- -
- ), - errorTitle: 'Please select a strategy.', - }), - { classNameModalContent: 'rounded-sm' } - ); - return false; - } - return true; - }); - }, [strategy, setPreCheckStepAdvance, openModal, closeModal]); - const choices = useMemo(() => stepData?.choices || [], [stepData?.choices]); const onCreateChoice = useCallback(() => { @@ -253,15 +221,6 @@ const StepOne = ({ [onDataChange] ); - const onSelectStrategy = (strategy) => { - const strategySelected = votingStrategies?.find( - (vs) => vs.key === strategy - ); - onDataChange({ - strategy: { label: strategySelected.name, value: strategySelected.key }, - }); - }; - const addImage = () => { setShowUploadImagesModal(true); }; @@ -388,38 +347,28 @@ const StepOne = ({ setShowUploadImagesModal(false); }; - const defaultValueStrategy = stepData?.strategy; - - const showTitleInputError = !checkValidTitleLength(stepData?.title ?? ''); - console.log('step data comes with', stepData); const fieldsObj = Object.assign( {}, stepOne.initialValues, - pick(stepData || {}, ['title']) + pick(stepData || {}, stepOne.formFields) ); - // const { register, handleSubmit, formState, control, setValue, reset } = - // useForm({ - - // resolver: yupResolver(stepOne.Schema), - // }); - - // const { isDirty, isSubmitting, errors, submit } = formState; - - // const field = useWatch({ control, name: 'example' }); - // console.log('watchinig ', field); - - const { register, handleSubmit, watch, formState } = useForm({ + const { register, handleSubmit, watch, formState, control } = useForm({ defaultValues: fieldsObj, + resolver: yupResolver(stepOne.Schema), }); + const onSubmit = (data) => { console.log('data on submit >>', data); onDataChange(data); moveToNextStep(); }; - const { isDirty, isSubmitting, errors } = formState; + const { isDirty, isSubmitting, isValid, errors } = formState; + + console.log('ERRORS => ', errors); + console.log('isValid => ', isValid); console.log(watch('example')); // watch input value by passing the name of it // const onSubmit = (data) => { @@ -482,17 +431,17 @@ const StepOne = ({ strategies are set by community admins.

({ label: vs.name, value: vs.key, })) ?? [] } - disabled={votingStrategies.length === 0} - onSelectValue={onSelectStrategy} - ref={dropDownRef} + disabled={isSubmitting || votingStrategies.length === 0} + control={control} />
diff --git a/frontend/packages/client/src/components/StepByStep/NexStepButton.js b/frontend/packages/client/src/components/StepByStep/NexStepButton.js index ab51f8ee5..694efd959 100644 --- a/frontend/packages/client/src/components/StepByStep/NexStepButton.js +++ b/frontend/packages/client/src/components/StepByStep/NexStepButton.js @@ -1,14 +1,16 @@ import React from 'react'; +import classnames from 'classnames'; const NextButton = ({ formId: formIdParam, moveToNextStep, disabled = false, } = {}) => { - const classNames = `button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered ${ - disabled && 'is-disabled' - }`; - + const classNames = classnames( + 'button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered', + { 'is-disabled': disabled }, + { 'is-fullwidth': !!formIdParam } + ); return (
{formIdParam ? ( diff --git a/frontend/packages/client/src/components/StepByStep/SubmitButton.js b/frontend/packages/client/src/components/StepByStep/SubmitButton.js index e69de29bb..2827eff19 100644 --- a/frontend/packages/client/src/components/StepByStep/SubmitButton.js +++ b/frontend/packages/client/src/components/StepByStep/SubmitButton.js @@ -0,0 +1,28 @@ +import React from 'react'; +import classnames from 'classnames'; + +export default function SubmitButton({ + formId: formIdParam, + label = '', + disabled = false, + onSubmit = () => {}, +} = {}) { + const classNames = classnames( + 'button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered', + { 'is-disabled': disabled }, + { 'is-fullwidth': !!formIdParam } + ); + return ( +
+ {formIdParam ? ( + + ) : ( +
+ {label} +
+ )} +
+ ); +} diff --git a/frontend/packages/client/src/components/StepByStep/index.js b/frontend/packages/client/src/components/StepByStep/index.js index 9440d9b1b..688f6337c 100644 --- a/frontend/packages/client/src/components/StepByStep/index.js +++ b/frontend/packages/client/src/components/StepByStep/index.js @@ -1,8 +1,10 @@ import React, { useCallback, useState } from 'react'; import { Prompt } from 'react-router-dom'; +import { isValid } from 'date-fns'; import Loader from '../Loader'; import { ArrowLeft, CheckMark } from '../Svg'; import NextButton from './NexStepButton'; +import SubmitButton from './SubmitButton'; function StepByStep({ finalLabel, @@ -12,6 +14,7 @@ function StepByStep({ isSubmitting, submittingMessage, passNextToComp = false, + showActionButtonLeftPannel = false, passSubmitToComp = false, blockNavigationOut = false, blockNavigationText, @@ -144,52 +147,11 @@ function StepByStep({ [onSubmit, stepsData] ); - const getNextButton = ({ formId: formIdParam } = {}) => { - const classNames = `button is-block has-background-yellow rounded-sm py-2 px-4 has-text-centered ${ - !isStepValid && 'is-disabled' - }`; - - return ( -
- {formIdParam ? ( - - ) : ( -
- Next -
- )} -
- ); - }; - - const getSubmitButton = ({ formId: formIdParam } = {}) => ( -
- {formIdParam ? ( - - ) : ( -
- {finalLabel} -
- )} -
- ); + const showNextButton = !passNextToComp || showActionButtonLeftPannel; + const showSubmitButton = !passSubmitToComp || showActionButtonLeftPannel; console.log('currentStep', steps[currentStep]); + return ( <> {blockNavigationOut && ( @@ -226,13 +188,21 @@ function StepByStep({ {currentStep > 0 && getBackLabel()}
{steps.map((step, i) => getStepIcon(i, step.label))}
- {currentStep < steps.length - 1 && - !passNextToComp && - getNextButton({ formId })} - - {currentStep === steps.length - 1 && - !passSubmitToComp && - getSubmitButton({ formId })} + {currentStep < steps.length - 1 && showNextButton && ( + + )} + {currentStep === steps.length - 1 && showSubmitButton && ( + + )}
{/* left panel mobile */}
- {currentStep < steps.length - 1 && - !passNextToComp && - getNextButton({ formId })} - {currentStep === steps.length - 1 && - !passSubmitToComp && - getSubmitButton({ formId })} + {currentStep < steps.length - 1 && showNextButton && ( + + )} + {currentStep === steps.length - 1 && showSubmitButton && ( + + )}
diff --git a/frontend/packages/client/src/pages/CommunityCreate.js b/frontend/packages/client/src/pages/CommunityCreate.js index 419933fcc..b977f81b7 100644 --- a/frontend/packages/client/src/pages/CommunityCreate.js +++ b/frontend/packages/client/src/pages/CommunityCreate.js @@ -162,6 +162,7 @@ export default function CommunityCreate() { submittingMessage: 'Creating community...', passNextToComp: true, passSubmitToComp: true, + showActionButtonLeftPannel: true, preStep: , blockNavigationOut: true && !data, blockNavigationText: diff --git a/frontend/packages/client/src/pages/ProposalCreate.js b/frontend/packages/client/src/pages/ProposalCreate.js index 0301e4d49..134c4ed20 100644 --- a/frontend/packages/client/src/pages/ProposalCreate.js +++ b/frontend/packages/client/src/pages/ProposalCreate.js @@ -117,6 +117,8 @@ export default function ProposalCreatePage() { blockNavigationOut: true && !data, blockNavigationText: 'Proposal creation is not complete yet, are you sure you want to leave?', + passNextToComp: true, + showActionButtonLeftPannel: true, steps: [ { label: 'Draft Proposal', From 48aeaedfb539824ef92d7012d75789fc4749e497 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 19 Aug 2022 17:53:50 -0300 Subject: [PATCH 04/36] remove comment --- .../client/src/components/ProposalCreate/StepOne/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 3ecc13380..c9636d309 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -370,12 +370,6 @@ const StepOne = ({ console.log('ERRORS => ', errors); console.log('isValid => ', isValid); - console.log(watch('example')); // watch input value by passing the name of it - // const onSubmit = (data) => { - // console.log('data submitted >>> ', data); - // // moveToNextStep(); - // }; - return ( <> {showUploadImagesModal && ( From 21d16d7c80a0e8b14d3f94e59788a997e098effd Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 19 Aug 2022 18:08:11 -0300 Subject: [PATCH 05/36] remove output --- .../client/src/components/ProposalCreate/StepOne/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index c9636d309..9ae35b71b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -347,7 +347,6 @@ const StepOne = ({ setShowUploadImagesModal(false); }; - console.log('step data comes with', stepData); const fieldsObj = Object.assign( {}, stepOne.initialValues, From d79e914d9ee9cea7e6502f1b941377fee028e25d Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Mon, 22 Aug 2022 14:29:24 -0300 Subject: [PATCH 06/36] updates --- .../ProposalCreate/StepOne/Editor.js | 250 +++++++++++++++++ .../ProposalCreate/StepOne/FormConfig.js | 3 +- .../ProposalCreate/StepOne/index.js | 252 ++---------------- .../client/src/pages/ProposalCreate.js | 6 +- frontend/packages/client/src/utils.js | 4 + 5 files changed, 277 insertions(+), 238 deletions(-) create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js new file mode 100644 index 000000000..0232ba045 --- /dev/null +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js @@ -0,0 +1,250 @@ +import React, { useEffect, useState } from 'react'; +import { Editor } from 'react-draft-wysiwyg'; +import { UploadImageModal } from 'components'; +import { Image } from 'components/Svg'; +import { customDraftToHTML, customHTMLtoDraft } from 'utils'; +import { + AtomicBlockUtils, + ContentState, + DefaultDraftBlockRenderMap, + EditorState, + Modifier, + SelectionState, +} from 'draft-js'; +import { Map } from 'immutable'; + +const options = ['blockType', 'inline', 'list', 'link', 'emoji']; +const inline = { + options: ['bold', 'italic', 'underline'], +}; +const list = { + options: ['unordered'], +}; +const link = { + options: ['link'], + defaultTargetOption: '_blank', +}; + +const styleMap = { + IMAGE_CAPTION: { + fontFamily: 'Arimo', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '12px', + }, +}; + +// using a React component to render custom blocks +const ImageCaptionCustomBlock = (props) => { + return
{props.children}
; +}; +const blockRenderMap = Map({ + 'image-caption-block': { + // element is used during paste or html conversion to auto match your component; + // it is also retained as part of this.props.children and not stripped out. Example: + // element: "section", + wrapper: , + }, +}); + +// keep support for other draft default block types and add our image-caption type +const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap); + +function AddImageOption({ addImage }) { + return ( + <> + + + ); +} +export default function CustomEditor({ onChange, value } = {}) { + const [localEditorState, setLocalEditorState] = useState( + EditorState.createEmpty() + ); + const [updated, setUpdated] = useState(false); + + useEffect(() => { + if (!updated) { + const defaultValue = value ? value : ''; + console.log('loads content', defaultValue); + const blocksFromHtml = customHTMLtoDraft(defaultValue); + // const contentState = ContentState.createFromBlockArray( + // blocksFromHtml.contentBlocks, + // blocksFromHtml.entityMap + // ); + const newEditorState = EditorState.createWithContent(blocksFromHtml); + setLocalEditorState(newEditorState); + } + }, [value]); + + const onEditorStateChange = (editorState) => { + setUpdated(true); + setLocalEditorState(editorState); + return onChange(customDraftToHTML(editorState.getCurrentContent())); + }; + + const [showUploadImagesModal, setShowUploadImagesModal] = useState(false); + + const addImage = () => { + setShowUploadImagesModal(true); + }; + + // const onEditorChange = (changes) => { + // setLocalEditorState(changes); + // onChange(changes); + // }; + + // function to update editor state + // used to insert more than one image at the time + function updateEditorState( + editorState, + { src, height, width, alt }, + caption + ) { + const entityKey = editorState + .getCurrentContent() + .createEntity('IMAGE', 'MUTABLE', { + src, + height, + width, + alt, + }) + .getLastCreatedEntityKey(); + + const selection = editorState.getSelection(); + + const currentFocusKey = selection.getFocusKey(); + + const newESWidthImageAndExtraBlock = AtomicBlockUtils.insertAtomicBlock( + editorState, + entityKey, + ' ' + ); + // user did not add caption text + if (caption.length === 0) { + return newESWidthImageAndExtraBlock; + } + // using cs: content state + const contentState = newESWidthImageAndExtraBlock.getCurrentContent(); + + const atomicBlockInserted = contentState.getBlockAfter(currentFocusKey); + + // AtomicBlockUtils.insertAtomicBlock inserts an empty block right after the cursor position + const emptyBlockInserted = contentState.getBlockAfter( + atomicBlockInserted.getKey() + ); + + const lastBlockAddedKey = emptyBlockInserted.getKey(); + + // get existing blocks and + // filter and remove the last block added + // bc it's not necessary and caption block goes right after it + const blockMapArray = contentState + .getBlocksAsArray() + .filter((block) => block.getKey() !== lastBlockAddedKey); + + // create new temporal content state to extract block with text + const tempCSWithCaption = ContentState.createFromText(caption); + // get the block with the text from temp content + const [tempBlockArray] = tempCSWithCaption.getBlocksAsArray(); + + // update block type so it's a custom type: image-caption + const csWithUpdatedBlock = Modifier.setBlockType( + ContentState.createFromBlockArray([tempBlockArray]), + SelectionState.createEmpty(tempBlockArray.key), + 'image-caption-block' + ); + // get the block with custom type and with text + const [updatedBlock] = csWithUpdatedBlock.getBlocksAsArray(); + + const newBlockMapArray = blockMapArray.reduce( + (accumulator, currentValue) => { + if (currentValue.getKey() === atomicBlockInserted.getKey()) { + return [ + ...accumulator, + currentValue, + updatedBlock, + emptyBlockInserted, + ]; + } + return [...accumulator, currentValue]; + }, + [] + ); + // add block updated and concat empty block at the end + const newContentState = ContentState.createFromBlockArray( + newBlockMapArray, + contentState.getEntityMap() + ); + + // this keeps the history of the action + const editorStateWithImageAndCaption = EditorState.push( + newESWidthImageAndExtraBlock, + newContentState, + 'insert-fragment' + ); + + // move cursor to the end + const newState = EditorState.moveSelectionToEnd( + editorStateWithImageAndCaption + ); + return newState; + } + + const addImagesToEditor = (images, captionValues) => { + // captionValue + let tempEditorState = localEditorState; + + for (let index = 0; index < images.length; index++) { + const image = images[index]; + const caption = captionValues[index]; + tempEditorState = updateEditorState( + tempEditorState, + { + src: image.imageUrl, + height: 'auto', + width: '100%', + alt: caption, + }, + caption + ); + } + setLocalEditorState(tempEditorState); + setShowUploadImagesModal(false); + }; + + const onDismissModal = () => { + setShowUploadImagesModal(false); + }; + return ( + <> + {showUploadImagesModal && ( + + )} + ]} + customStyleMap={styleMap} + blockRenderMap={extendedBlockRenderMap} + /> + + ); +} diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js index f98d949fc..de07679ec 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -1,6 +1,6 @@ import * as yup from 'yup'; -const formFields = ['title', 'strategy']; +const formFields = ['title', 'strategy', 'body']; const Schema = yup.object().shape({ title: yup @@ -9,6 +9,7 @@ const Schema = yup.object().shape({ .required('Please enter a proposal title') .max(150, 'The maximum length for title is 128 characters'), strategy: yup.string().required('Please select a strategy'), + body: yup.string().required('Please enter a proposal description'), }); const initialValues = Object.assign( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 9ae35b71b..75e09896b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -1,70 +1,19 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { Editor } from 'react-draft-wysiwyg'; -import { useForm, useWatch } from 'react-hook-form'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { Controller, useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { useModalContext } from 'contexts/NotificationModal'; -import { Error, UploadImageModal } from 'components'; -import { Image } from 'components/Svg'; import Dropdown from 'components/common/Dropdown'; import Form from 'components/common/Form'; import Input from 'components/common/Input'; import { useCommunityDetails } from 'hooks'; import { kebabToString } from 'utils'; import { yupResolver } from '@hookform/resolvers/yup'; -import { - AtomicBlockUtils, - ContentState, - DefaultDraftBlockRenderMap, - EditorState, - Modifier, - SelectionState, -} from 'draft-js'; -import { Map } from 'immutable'; import pick from 'lodash/pick'; +import CustomEditor from './Editor'; import { stepOne } from './FormConfig'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; -// using a React component to render custom blocks -const ImageCaptionCustomBlock = (props) => { - return
{props.children}
; -}; -const blockRenderMap = Map({ - 'image-caption-block': { - // element is used during paste or html conversion to auto match your component; - // it is also retained as part of this.props.children and not stripped out. Example: - // element: "section", - wrapper: , - }, -}); - -// keep support for other draft default block types and add our image-caption type -const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap); - -function AddImageOption({ addImage }) { - return ( - <> - - - ); -} - const StepOne = ({ stepData, setStepValid, @@ -73,8 +22,6 @@ const StepOne = ({ formId, moveToNextStep, }) => { - const dropDownRef = useRef(); - const { communityId } = useParams(); const { data: community } = useCommunityDetails(communityId); @@ -96,15 +43,10 @@ const StepOne = ({ () => stepData?.proposalType || 'text-based', [stepData?.proposalType] ); - const [localEditorState, setLocalEditorState] = useState( - stepData?.description || EditorState.createEmpty() - ); - - const [showUploadImagesModal, setShowUploadImagesModal] = useState(false); useEffect(() => { const requiredFields = { - description: (body) => body?.getCurrentContent().hasText(), + // description: (body) => body?.getCurrentContent().hasText(), choices: (opts) => { const getLabel = (o) => o?.value?.trim(); const getImageUrl = (o) => o?.choiceImgUrl?.trim(); @@ -136,10 +78,10 @@ const StepOne = ({ setStepValid(true); }, [stepData, setStepValid, onDataChange, tabOption]); - useEffect(() => { - onDataChange({ description: localEditorState }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [localEditorState]); + // useEffect(() => { + // onDataChange({ description: localEditorState }); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [localEditorState]); const setTab = (option) => () => { onDataChange({ @@ -147,30 +89,9 @@ const StepOne = ({ }); }; - const onEditorChange = (changes) => { - setLocalEditorState(changes); - }; - - const options = ['blockType', 'inline', 'list', 'link', 'emoji']; - const inline = { - options: ['bold', 'italic', 'underline'], - }; - const list = { - options: ['unordered'], - }; - const link = { - options: ['link'], - defaultTargetOption: '_blank', - }; - - const styleMap = { - IMAGE_CAPTION: { - fontFamily: 'Arimo', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '12px', - }, - }; + // const onEditorChange = (changes) => { + // setLocalEditorState(changes); + // }; const choices = useMemo(() => stepData?.choices || [], [stepData?.choices]); @@ -221,132 +142,6 @@ const StepOne = ({ [onDataChange] ); - const addImage = () => { - setShowUploadImagesModal(true); - }; - const onDismissModal = () => { - setShowUploadImagesModal(false); - }; - - // function to update editor state - // used to insert more than one image at the time - function updateEditorState( - editorState, - { src, height, width, alt }, - caption - ) { - const entityKey = editorState - .getCurrentContent() - .createEntity('IMAGE', 'MUTABLE', { - src, - height, - width, - alt, - }) - .getLastCreatedEntityKey(); - - const selection = editorState.getSelection(); - - const currentFocusKey = selection.getFocusKey(); - - const newESWidthImageAndExtraBlock = AtomicBlockUtils.insertAtomicBlock( - editorState, - entityKey, - ' ' - ); - // user did not add caption text - if (caption.length === 0) { - return newESWidthImageAndExtraBlock; - } - // using cs: content state - const contentState = newESWidthImageAndExtraBlock.getCurrentContent(); - - const atomicBlockInserted = contentState.getBlockAfter(currentFocusKey); - - // AtomicBlockUtils.insertAtomicBlock inserts an empty block right after the cursor position - const emptyBlockInserted = contentState.getBlockAfter( - atomicBlockInserted.getKey() - ); - - const lastBlockAddedKey = emptyBlockInserted.getKey(); - - // get existing blocks and - // filter and remove the last block added - // bc it's not necessary and caption block goes right after it - const blockMapArray = contentState - .getBlocksAsArray() - .filter((block) => block.getKey() !== lastBlockAddedKey); - - // create new temporal content state to extract block with text - const tempCSWithCaption = ContentState.createFromText(caption); - // get the block with the text from temp content - const [tempBlockArray] = tempCSWithCaption.getBlocksAsArray(); - - // update block type so it's a custom type: image-caption - const csWithUpdatedBlock = Modifier.setBlockType( - ContentState.createFromBlockArray([tempBlockArray]), - SelectionState.createEmpty(tempBlockArray.key), - 'image-caption-block' - ); - // get the block with custom type and with text - const [updatedBlock] = csWithUpdatedBlock.getBlocksAsArray(); - - const newBlockMapArray = blockMapArray.reduce( - (accumulator, currentValue) => { - if (currentValue.getKey() === atomicBlockInserted.getKey()) { - return [ - ...accumulator, - currentValue, - updatedBlock, - emptyBlockInserted, - ]; - } - return [...accumulator, currentValue]; - }, - [] - ); - // add block updated and concat empty block at the end - const newContentState = ContentState.createFromBlockArray( - newBlockMapArray, - contentState.getEntityMap() - ); - - // this keeps the history of the action - const editorStateWithImageAndCaption = EditorState.push( - newESWidthImageAndExtraBlock, - newContentState, - 'insert-fragment' - ); - - // move cursor to the end - const newState = EditorState.moveSelectionToEnd( - editorStateWithImageAndCaption - ); - return newState; - } - - const addImagesToEditor = (images, captionValues) => { - // captionValue - let tempEditorState = localEditorState; - - for (let index = 0; index < images.length; index++) { - const image = images[index]; - const caption = captionValues[index]; - tempEditorState = updateEditorState( - tempEditorState, - { - src: image.imageUrl, - height: 'auto', - width: '100%', - alt: caption, - }, - caption - ); - } - setLocalEditorState(tempEditorState); - setShowUploadImagesModal(false); - }; - const fieldsObj = Object.assign( {}, stepOne.initialValues, @@ -363,21 +158,16 @@ const StepOne = ({ onDataChange(data); moveToNextStep(); }; + const body = useWatch({ control, name: 'body' }); const { isDirty, isSubmitting, isValid, errors } = formState; console.log('ERRORS => ', errors); console.log('isValid => ', isValid); + console.log('body => ', body); return ( <> - {showUploadImagesModal && ( - - )} -
@@ -405,16 +195,12 @@ const StepOne = ({ context; the expected costs and benefits of this collective decision.

- ]} - customStyleMap={styleMap} - blockRenderMap={extendedBlockRenderMap} + { + return ; + }} />
diff --git a/frontend/packages/client/src/pages/ProposalCreate.js b/frontend/packages/client/src/pages/ProposalCreate.js index 134c4ed20..9c8face2f 100644 --- a/frontend/packages/client/src/pages/ProposalCreate.js +++ b/frontend/packages/client/src/pages/ProposalCreate.js @@ -11,7 +11,7 @@ import { PropCreateStepTwo, } from 'components/ProposalCreate'; import { useProposal } from 'hooks'; -import { customDraftToHTML, parseDateToServer } from 'utils'; +import { parseDateToServer } from 'utils'; export default function ProposalCreatePage() { const { createProposal, data, loading, error } = useProposal(); @@ -72,9 +72,7 @@ export default function ProposalCreatePage() { } const name = stepsData[0].title; - const currentContent = stepsData[0]?.description?.getCurrentContent(); - - const body = customDraftToHTML(currentContent); + const body = stepsData[0]?.body; const startTime = parseDateToServer( stepsData[1].startDate, diff --git a/frontend/packages/client/src/utils.js b/frontend/packages/client/src/utils.js index 920572597..6fb4f236c 100644 --- a/frontend/packages/client/src/utils.js +++ b/frontend/packages/client/src/utils.js @@ -1,5 +1,6 @@ import { formatDistance } from 'date-fns'; import { stateToHTML } from 'draft-js-export-html'; +import { stateFromHTML } from 'draft-js-import-html'; import { customAlphabet } from 'nanoid'; const nanoid = customAlphabet('1234567890abcdef', 10); @@ -180,6 +181,9 @@ export const customDraftToHTML = (content) => { return stateToHTML(content, options); }; +export const customHTMLtoDraft = (html) => { + return stateFromHTML(html); +}; export const isValidAddress = (addr) => /0[x,X][a-zA-Z0-9]{16}$/gim.test(addr); export const wait = async (milliSeconds = 5000) => From a26c929bbbb4c1b0d8b14c2b447be129e5b5a329 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Mon, 22 Aug 2022 14:29:56 -0300 Subject: [PATCH 07/36] install package --- frontend/packages/client/package.json | 1 + frontend/yarn.lock | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/frontend/packages/client/package.json b/frontend/packages/client/package.json index 9afa5c3f7..9e48641ec 100644 --- a/frontend/packages/client/package.json +++ b/frontend/packages/client/package.json @@ -17,6 +17,7 @@ "date-fns": "^2.28.0", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", + "draft-js-import-html": "^1.4.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", "immutable": "^4.0.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 82928025b..fb4a5e5bd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4871,6 +4871,21 @@ draft-js-export-html@^1.4.1: dependencies: draft-js-utils "^1.4.0" +draft-js-import-element@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz#8760acbfeb60ed824a1c8319ec049f702681df66" + integrity sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg== + dependencies: + draft-js-utils "^1.4.0" + synthetic-dom "^1.4.0" + +draft-js-import-html@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz#c222a3a40ab27dee5874fcf78526b64734fe6ea4" + integrity sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg== + dependencies: + draft-js-import-element "^1.4.0" + draft-js-utils@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.0.tgz" @@ -11462,6 +11477,11 @@ symbol-tree@^3.2.4: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +synthetic-dom@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/synthetic-dom/-/synthetic-dom-1.4.0.tgz#d988d7a4652458e2fc8706a875417af913e4dd34" + integrity sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg== + table@^6.0.9: version "6.7.3" resolved "https://registry.npmjs.org/table/-/table-6.7.3.tgz" From 28359607072abf3f260133cdde8e5e8e73866a63 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 11:05:00 -0300 Subject: [PATCH 08/36] updates --- .../ProposalCreate/StepOne/index.js | 21 +- .../client/src/components/common/Checkbox.js | 1 - .../client/src/components/common/Dropdown.js | 218 +++++++++--------- .../Editor/DraftjsEditor.js} | 52 ++--- .../src/components/common/Editor/Editor.js | 25 ++ .../components/common/Editor/ImageOption.js | 20 ++ .../src/components/common/Editor/index.js | 1 + frontend/packages/client/src/utils.js | 9 +- 8 files changed, 188 insertions(+), 159 deletions(-) rename frontend/packages/client/src/components/{ProposalCreate/StepOne/Editor.js => common/Editor/DraftjsEditor.js} (84%) create mode 100644 frontend/packages/client/src/components/common/Editor/Editor.js create mode 100644 frontend/packages/client/src/components/common/Editor/ImageOption.js create mode 100644 frontend/packages/client/src/components/common/Editor/index.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 75e09896b..1dc1af755 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -1,15 +1,15 @@ import React, { useCallback, useEffect, useMemo } from 'react'; -import { Controller, useForm, useWatch } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { useModalContext } from 'contexts/NotificationModal'; import Dropdown from 'components/common/Dropdown'; +import { Editor } from 'components/common/Editor'; import Form from 'components/common/Form'; import Input from 'components/common/Input'; import { useCommunityDetails } from 'hooks'; import { kebabToString } from 'utils'; import { yupResolver } from '@hookform/resolvers/yup'; import pick from 'lodash/pick'; -import CustomEditor from './Editor'; import { stepOne } from './FormConfig'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; @@ -78,21 +78,12 @@ const StepOne = ({ setStepValid(true); }, [stepData, setStepValid, onDataChange, tabOption]); - // useEffect(() => { - // onDataChange({ description: localEditorState }); - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [localEditorState]); - const setTab = (option) => () => { onDataChange({ proposalType: option, }); }; - // const onEditorChange = (changes) => { - // setLocalEditorState(changes); - // }; - const choices = useMemo(() => stepData?.choices || [], [stepData?.choices]); const onCreateChoice = useCallback(() => { @@ -195,13 +186,7 @@ const StepOne = ({ context; the expected costs and benefits of this collective decision.

- { - return ; - }} - /> +

Voting Strategy

diff --git a/frontend/packages/client/src/components/common/Checkbox.js b/frontend/packages/client/src/components/common/Checkbox.js index 247a6a7fb..c769f4aa2 100644 --- a/frontend/packages/client/src/components/common/Checkbox.js +++ b/frontend/packages/client/src/components/common/Checkbox.js @@ -8,7 +8,6 @@ export default function Checkbox({ label, disabled, error, - type = 'text', } = {}) { return (
diff --git a/frontend/packages/client/src/components/common/Dropdown.js b/frontend/packages/client/src/components/common/Dropdown.js index fa0d12f35..b7d3c3fdc 100644 --- a/frontend/packages/client/src/components/common/Dropdown.js +++ b/frontend/packages/client/src/components/common/Dropdown.js @@ -1,123 +1,132 @@ -import React, { useEffect, useState } from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; import { Controller } from 'react-hook-form'; import FadeIn from 'components/FadeIn'; import { CaretDown } from 'components/Svg'; import classnames from 'classnames'; -const Dropdown = ({ - defaultValue, - options = [], - onSelectValue = () => {}, - disabled = false, - label = 'Select option', - dropdownFull = true, - isRight = false, - padding = '', - margin = '', -} = {}) => { - const [isOpen, setIsOpen] = useState(false); - const [innerValue, setInnerValue] = useState( - defaultValue ? { value: defaultValue } : undefined - ); +const Dropdown = forwardRef( + ( + { + defaultValue, + options = [], + onSelectValue = () => {}, + disabled = false, + label = 'Select option', + dropdownFull = true, + isRight = false, + padding = '', + margin = '', + name, + } = {}, + ref + ) => { + const [isOpen, setIsOpen] = useState(false); + const [innerValue, setInnerValue] = useState( + defaultValue ? { value: defaultValue } : undefined + ); - useEffect(() => { - if (!innerValue?.label && innerValue?.value && options.length) { - const defaultSelection = options.find( - (op) => op.value === innerValue?.value - ); - if (defaultSelection) { - setInnerValue(defaultSelection); + useEffect(() => { + if (!innerValue?.label && innerValue?.value && options.length) { + const defaultSelection = options.find( + (op) => op.value === innerValue?.value + ); + if (defaultSelection) { + setInnerValue(defaultSelection); + } } - } - }, [options, innerValue]); + }, [options, innerValue]); - const openCloseDropdown = (e) => { - e.preventDefault(); - e.stopPropagation(); - setIsOpen((status) => !status); - }; + const openCloseDropdown = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsOpen((status) => !status); + }; + + const setValue = + ({ label, value }) => + (e) => { + e.target.value = value; + setInnerValue({ label, value }); + onSelectValue(e); + setIsOpen(false); + }; - const setValue = - ({ label, value }) => - (e) => { - e.target.value = value; - setInnerValue({ label, value }); - onSelectValue(e); + // use for click out on dropdown + const closeOnBlur = () => { setIsOpen(false); }; - // use for click out on dropdown - const closeOnBlur = () => { - setIsOpen(false); - }; - - const classNames = classnames( - `dropdown is-flex is-flex-grow-1`, - { 'is-right': isRight }, - { 'is-active': isOpen }, - { [padding]: !!padding }, - { [margin]: !!margin } - ); - return ( -
+ const classNames = classnames( + `dropdown is-flex is-flex-grow-1`, + { 'is-right': isRight }, + { 'is-active': isOpen }, + { [padding]: !!padding }, + { [margin]: !!margin } + ); + return (
- +
+ - -
- ); -}; + ); + } +); + export default function DropdownWrapper({ control, name, @@ -134,13 +143,13 @@ export default function DropdownWrapper({ control={control} name={name} render={({ - field: { onChange, onBlur, value, name, ref }, - fieldState: { invalid, isTouched, isDirty, error }, - formState, + field: { onChange, value, name, ref }, + fieldState: { error }, }) => { return (
{error && ( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js b/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js similarity index 84% rename from frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js rename to frontend/packages/client/src/components/common/Editor/DraftjsEditor.js index 0232ba045..184307985 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/Editor.js +++ b/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Editor } from 'react-draft-wysiwyg'; import { UploadImageModal } from 'components'; -import { Image } from 'components/Svg'; import { customDraftToHTML, customHTMLtoDraft } from 'utils'; import { AtomicBlockUtils, @@ -12,6 +11,7 @@ import { SelectionState, } from 'draft-js'; import { Map } from 'immutable'; +import AddImageOption from './ImageOption'; const options = ['blockType', 'inline', 'list', 'link', 'emoji']; const inline = { @@ -50,47 +50,33 @@ const blockRenderMap = Map({ // keep support for other draft default block types and add our image-caption type const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap); -function AddImageOption({ addImage }) { - return ( - <> - - - ); -} -export default function CustomEditor({ onChange, value } = {}) { +export default function CustomEditor({ onChange, value, ref } = {}) { const [localEditorState, setLocalEditorState] = useState( EditorState.createEmpty() ); const [updated, setUpdated] = useState(false); + // this effect runs on initial component load to create editor from html useEffect(() => { if (!updated) { const defaultValue = value ? value : ''; - console.log('loads content', defaultValue); - const blocksFromHtml = customHTMLtoDraft(defaultValue); - // const contentState = ContentState.createFromBlockArray( - // blocksFromHtml.contentBlocks, - // blocksFromHtml.entityMap - // ); - const newEditorState = EditorState.createWithContent(blocksFromHtml); + const htmlToState = customHTMLtoDraft(defaultValue); + const newEditorState = EditorState.createWithContent(htmlToState); setLocalEditorState(newEditorState); } - }, [value]); + }, [value, updated]); const onEditorStateChange = (editorState) => { - setUpdated(true); + !updated && setUpdated(true); setLocalEditorState(editorState); - return onChange(customDraftToHTML(editorState.getCurrentContent())); + console.log( + 'customDraftToHTML', + customDraftToHTML(editorState.getCurrentContent()) + ); + const pureHtml = customDraftToHTML(editorState.getCurrentContent()); + // when editor es empty but has been changed it will return


+ const textOnly = pureHtml.replace(/<[^>]+>/g, ''); + return onChange('' === textOnly ? '' : pureHtml); }; const [showUploadImagesModal, setShowUploadImagesModal] = useState(false); @@ -99,11 +85,6 @@ export default function CustomEditor({ onChange, value } = {}) { setShowUploadImagesModal(true); }; - // const onEditorChange = (changes) => { - // setLocalEditorState(changes); - // onChange(changes); - // }; - // function to update editor state // used to insert more than one image at the time function updateEditorState( @@ -219,7 +200,7 @@ export default function CustomEditor({ onChange, value } = {}) { caption ); } - setLocalEditorState(tempEditorState); + onEditorStateChange(tempEditorState); setShowUploadImagesModal(false); }; @@ -244,6 +225,7 @@ export default function CustomEditor({ onChange, value } = {}) { toolbarCustomButtons={[]} customStyleMap={styleMap} blockRenderMap={extendedBlockRenderMap} + ref={ref} /> ); diff --git a/frontend/packages/client/src/components/common/Editor/Editor.js b/frontend/packages/client/src/components/common/Editor/Editor.js new file mode 100644 index 000000000..66ea5b395 --- /dev/null +++ b/frontend/packages/client/src/components/common/Editor/Editor.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Controller } from 'react-hook-form'; +import FadeIn from 'components/FadeIn'; +import DraftjsEditor from './DraftjsEditor'; + +export default function Editor({ control, error, name } = {}) { + return ( + <> + { + return ; + }} + /> + {error && ( + +
+

{error?.message}

+
+
+ )} + + ); +} diff --git a/frontend/packages/client/src/components/common/Editor/ImageOption.js b/frontend/packages/client/src/components/common/Editor/ImageOption.js new file mode 100644 index 000000000..1f235bdf2 --- /dev/null +++ b/frontend/packages/client/src/components/common/Editor/ImageOption.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Image } from 'components/Svg'; + +export default function ImageOption({ addImage = () => {} } = {}) { + return ( + <> + + + ); +} diff --git a/frontend/packages/client/src/components/common/Editor/index.js b/frontend/packages/client/src/components/common/Editor/index.js new file mode 100644 index 000000000..366882009 --- /dev/null +++ b/frontend/packages/client/src/components/common/Editor/index.js @@ -0,0 +1 @@ +export { default as Editor } from './Editor'; diff --git a/frontend/packages/client/src/utils.js b/frontend/packages/client/src/utils.js index 6fb4f236c..44ce87332 100644 --- a/frontend/packages/client/src/utils.js +++ b/frontend/packages/client/src/utils.js @@ -182,7 +182,14 @@ export const customDraftToHTML = (content) => { }; export const customHTMLtoDraft = (html) => { - return stateFromHTML(html); + const options = { + customBlockFn: (element) => { + if (element.tagName === 'P' && element.className === 'image-caption') { + return { type: 'image-caption-block' }; + } + }, + }; + return stateFromHTML(html, options); }; export const isValidAddress = (addr) => /0[x,X][a-zA-Z0-9]{16}$/gim.test(addr); From a65c810054197616baabaa309a4c9ebd34ce8096 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 13:53:24 -0300 Subject: [PATCH 09/36] update --- frontend/packages/client/src/App.sass | 11 ++- .../ProposalCreate/StepOne/FormConfig.js | 13 ++- .../ProposalCreate/StepOne/ImageChoices.js | 2 - .../ProposalCreate/StepOne/index.js | 99 ++++++++----------- 4 files changed, 59 insertions(+), 66 deletions(-) diff --git a/frontend/packages/client/src/App.sass b/frontend/packages/client/src/App.sass index 20e7e50ed..7dfc095bb 100644 --- a/frontend/packages/client/src/App.sass +++ b/frontend/packages/client/src/App.sass @@ -685,9 +685,12 @@ span[data-tooltip] background-size: contain border-color: transparent background-repeat: no-repeat - + @keyframes star - from - transform: scale(0) + from + transform: scale(0) to - transform: scale(1) \ No newline at end of file + transform: scale(1) + +.rdw-editor-toolbar + border-radius: 8px !important \ No newline at end of file diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js index de07679ec..55bbdc618 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -1,6 +1,6 @@ -import * as yup from 'yup'; +import yup from 'helpers/validation'; -const formFields = ['title', 'strategy', 'body']; +const formFields = ['title', 'strategy', 'body', 'choices']; const Schema = yup.object().shape({ title: yup @@ -10,6 +10,15 @@ const Schema = yup.object().shape({ .max(150, 'The maximum length for title is 128 characters'), strategy: yup.string().required('Please select a strategy'), body: yup.string().required('Please enter a proposal description'), + choices: yup + .array( + yup.object({ + value: yup.string().required('Please enter option value'), + choiceImgUrl: yup.string(), + }) + ) + .min(2, 'Please add an option, minimun amout of options is two') + .unique('value', 'Invalid duplicated option'), }); const initialValues = Object.assign( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js index b2a1dac84..35fd2f622 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js @@ -7,12 +7,10 @@ const ImageChoices = ({ choices = [], onChoiceChange, initChoices } = {}) => { if (getProposalType(choices) !== 'image') { initChoices([ { - id: 1, value: '', choiceImgUrl: '', }, { - id: 2, value: '', choiceImgUrl: '', }, diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 1dc1af755..4114777ad 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; +import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { useModalContext } from 'contexts/NotificationModal'; import Dropdown from 'components/common/Dropdown'; @@ -37,8 +37,6 @@ const StepOne = ({ [strategies] ); - const { openModal, closeModal } = useModalContext(); - const tabOption = useMemo( () => stepData?.proposalType || 'text-based', [stepData?.proposalType] @@ -78,84 +76,69 @@ const StepOne = ({ setStepValid(true); }, [stepData, setStepValid, onDataChange, tabOption]); - const setTab = (option) => () => { + const setTab = (option) => (e) => { + e.preventDefault(); + e.stopPropagation(); onDataChange({ proposalType: option, }); }; - const choices = useMemo(() => stepData?.choices || [], [stepData?.choices]); - - const onCreateChoice = useCallback(() => { - onDataChange({ - choices: choices.concat([ - { - id: choices.length + 1, - value: '', - }, - ]), - }); - }, [onDataChange, choices]); - - const onDestroyChoice = useCallback( - (choiceIdx) => { - const newChoices = choices.slice(0); - newChoices.splice(choiceIdx, 1); - onDataChange({ choices: newChoices }); - }, - [choices, onDataChange] - ); - - const onChoiceChange = useCallback( - (choiceUpdate, choiceIdx) => { - const newChoices = choices.map((choice, idx) => { - if (idx === choiceIdx) { - return { - ...choice, - ...choiceUpdate, - }; - } - - return choice; - }); - - onDataChange({ choices: newChoices }); - }, - [choices, onDataChange] - ); - - const initChoices = useCallback( - (choices) => { - onDataChange({ - choices, - }); - }, - [onDataChange] - ); - const fieldsObj = Object.assign( {}, stepOne.initialValues, + { choices: [] }, pick(stepData || {}, stepOne.formFields) ); - const { register, handleSubmit, watch, formState, control } = useForm({ + const { register, handleSubmit, formState, control } = useForm({ defaultValues: fieldsObj, resolver: yupResolver(stepOne.Schema), }); + const { + fields: choicesField, + append, + remove, + update, + replace, + } = useFieldArray({ + control, + name: 'choices', + focusAppend: true, + }); + const onSubmit = (data) => { console.log('data on submit >>', data); onDataChange(data); moveToNextStep(); }; - const body = useWatch({ control, name: 'body' }); + + const onCreateChoice = () => + append({ + value: '', + }); + + const onDestroyChoice = (choiceIdx) => { + remove(choiceIdx); + }; + + const onChoiceChange = (choiceUpdate, choiceIdx) => { + update(choiceIdx, choiceUpdate); + }; + + const initChoices = (choices) => { + replace(choices); + }; + + const choicesss = useWatch({ control, name: 'choices' }); const { isDirty, isSubmitting, isValid, errors } = formState; console.log('ERRORS => ', errors); console.log('isValid => ', isValid); - console.log('body => ', body); + console.log('choices => ', choicesss); + console.log('choicesN => ', choicesField); return ( <> @@ -243,7 +226,7 @@ const StepOne = ({
{tabOption === 'text-based' && ( From 455c7d1f24562eecd29dd64dcd5db3640ef58b9b Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 13:53:32 -0300 Subject: [PATCH 10/36] create yup helper --- .../packages/client/src/helpers/validation.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frontend/packages/client/src/helpers/validation.js diff --git a/frontend/packages/client/src/helpers/validation.js b/frontend/packages/client/src/helpers/validation.js new file mode 100644 index 000000000..b66a3c640 --- /dev/null +++ b/frontend/packages/client/src/helpers/validation.js @@ -0,0 +1,25 @@ +import * as yup from 'yup'; + +yup.addMethod(yup.array, 'unique', function (field, message) { + return this.test('unique', message, function (array = []) { + const uniqueData = Array.from( + new Set(array.map((row) => row[field]?.toLowerCase())) + ); + const isUnique = array.length === uniqueData.length; + if (isUnique) { + return true; + } + const index = array.findIndex( + (row, i) => row[field]?.toLowerCase() !== uniqueData[i] + ); + if (array[index][field] === '') { + return true; + } + return this.createError({ + path: `${this.path}.${index}.${field}`, + message, + }); + }); +}); + +export default yup; From ef0c070147aa207113a95886b5d21ec9aadc3593 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 13:54:58 -0300 Subject: [PATCH 11/36] remove output --- frontend/packages/client/src/components/StepByStep/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/packages/client/src/components/StepByStep/index.js b/frontend/packages/client/src/components/StepByStep/index.js index 688f6337c..b0d57d65b 100644 --- a/frontend/packages/client/src/components/StepByStep/index.js +++ b/frontend/packages/client/src/components/StepByStep/index.js @@ -150,8 +150,6 @@ function StepByStep({ const showNextButton = !passNextToComp || showActionButtonLeftPannel; const showSubmitButton = !passSubmitToComp || showActionButtonLeftPannel; - console.log('currentStep', steps[currentStep]); - return ( <> {blockNavigationOut && ( From 56cad6e43c1aad618265c82e5cc66ccbd380e750 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 14:32:45 -0300 Subject: [PATCH 12/36] create option component --- .../src/components/ProposalCreate/StepOne/ChoiceOptionTab.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js new file mode 100644 index 000000000..0d4754751 --- /dev/null +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default function ChoiceOption({} = {}) {} From 9378fb42e26f12f6f0629edf551bc067a724eaf2 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 16:59:16 -0300 Subject: [PATCH 13/36] updates --- .../StepOne/ChoiceOptionCreator.js | 79 +++++++++++ .../ProposalCreate/StepOne/ChoiceOptionTab.js | 3 - .../ProposalCreate/StepOne/FormConfig.js | 24 +++- .../ProposalCreate/StepOne/ImageChoices.js | 2 +- .../StepOne/TextBasedChoices.js | 86 +++++++---- .../ProposalCreate/StepOne/index.js | 133 +++--------------- 6 files changed, 173 insertions(+), 154 deletions(-) create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js delete mode 100644 frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js new file mode 100644 index 000000000..5439924f4 --- /dev/null +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -0,0 +1,79 @@ +import React from 'react'; +import ImageChoices from './ImageChoices'; +import TextBasedChoices from './TextBasedChoices'; + +export default function ChoiceOptionCreator({ + tabOption, + choices, + append, + remove, + replace, + update, + setValue = () => {}, + error = [], +} = {}) { + // tabOption value is sabed on form + const setTab = (option) => (e) => { + e.preventDefault(); + e.stopPropagation(); + setValue('tabOption', option); + }; + + const onCreateChoice = (e) => { + e.preventDefault(); + e.stopPropagation(); + append({ + value: '', + }); + }; + + const initChoices = (choices) => replace(choices); + + return ( + <> +
+
    +
  • + +
  • +
  • + +
  • +
+
+ + {tabOption === 'text-based' && ( + + )} + {tabOption === 'visual' && ( + + )} + + ); +} diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js deleted file mode 100644 index 0d4754751..000000000 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionTab.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export default function ChoiceOption({} = {}) {} diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js index 55bbdc618..b10a12111 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js @@ -1,6 +1,6 @@ import yup from 'helpers/validation'; -const formFields = ['title', 'strategy', 'body', 'choices']; +const formFields = ['title', 'strategy', 'body', 'choices', 'tabOption']; const Schema = yup.object().shape({ title: yup @@ -10,14 +10,26 @@ const Schema = yup.object().shape({ .max(150, 'The maximum length for title is 128 characters'), strategy: yup.string().required('Please select a strategy'), body: yup.string().required('Please enter a proposal description'), + tabOption: yup.string().oneOf(['text-based', 'visual']), choices: yup .array( - yup.object({ - value: yup.string().required('Please enter option value'), - choiceImgUrl: yup.string(), - }) + yup + .object({ + value: yup.string().required('Please enter option value'), + choiceImgUrl: yup.string().nullable(), + }) + .when('tabOption', { + is: (option) => option === 'visual', + then: yup.object({ + value: yup.string().required('Please enter option value'), + choiceImgUrl: yup + .string() + .url() + .required('Image option is not valid'), + }), + }) ) - .min(2, 'Please add an option, minimun amout of options is two') + .min(2, 'Please add a choice, minimun amout is two') .unique('value', 'Invalid duplicated option'), }); diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js index 35fd2f622..fb928724f 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js @@ -20,7 +20,7 @@ const ImageChoices = ({ choices = [], onChoiceChange, initChoices } = {}) => { }, []); const onImageUpdate = (index) => (image) => { - onChoiceChange({ value: image.text, choiceImgUrl: image.imageUrl }, index); + onChoiceChange(index, { value: image.text, choiceImgUrl: image.imageUrl }); }; const [choiceA, choiceB] = choices; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index 36558afef..3d85b6a1b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import AddButton from 'components/AddButton'; +import FadeIn from 'components/FadeIn'; import { Bin } from 'components/Svg'; import { getProposalType } from 'utils'; @@ -9,6 +10,7 @@ const TextBasedChoices = ({ onDestroyChoice, onCreateChoice, initChoices, + error, } = {}) => { useEffect(() => { if (getProposalType(choices) !== 'text-based') { @@ -16,38 +18,64 @@ const TextBasedChoices = ({ } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return ( <> - {choices?.map((choice, i) => ( -
- - onChoiceChange({ value: event.target.value }, i) - } - autoFocus - /> -
onDestroyChoice(i)} - > - + {choices?.map((choice, index) => { + const errorInField = Array.isArray(error) + ? error?.[index]?.value + : null; + return ( + <> +
+ + onChoiceChange(index, { value: event.target.value }) + } + autoFocus + /> +
onDestroyChoice(index)} + > + +
+
+ {errorInField && ( + +
+

+ {errorInField.message} +

+
+
+ )} + + ); + })} + {error?.message && ( + +
+

{error.message}

-
- ))} + + )}
stepData?.proposalType || 'text-based', - [stepData?.proposalType] - ); - - useEffect(() => { - const requiredFields = { - // description: (body) => body?.getCurrentContent().hasText(), - choices: (opts) => { - const getLabel = (o) => o?.value?.trim(); - const getImageUrl = (o) => o?.choiceImgUrl?.trim(); - const moreThanOne = Array.isArray(opts) && opts.length > 1; - - const optLabels = (opts || []).map((opt) => getLabel(opt)); - - const haveLabels = - moreThanOne && optLabels.every((opt) => opt.length > 0); - - const eachUnique = - moreThanOne && - optLabels.every((opt, idx) => optLabels.indexOf(opt) === idx); - - if (tabOption === 'text-based') return haveLabels && eachUnique; - - const imagesUrl = (opts || []).map((opt) => getImageUrl(opt)); - - const validImageOpts = imagesUrl.every( - (imgUrl) => imgUrl && imgUrl.length > 0 - ); - - return haveLabels && eachUnique && validImageOpts; - }, - }; - const isValid = Object.keys(requiredFields).every( - (field) => stepData && requiredFields[field](stepData[field]) - ); - setStepValid(true); - }, [stepData, setStepValid, onDataChange, tabOption]); - - const setTab = (option) => (e) => { - e.preventDefault(); - e.stopPropagation(); - onDataChange({ - proposalType: option, - }); - }; - const fieldsObj = Object.assign( {}, stepOne.initialValues, - { choices: [] }, + { choices: [], tabOption: 'text-based' }, pick(stepData || {}, stepOne.formFields) ); - const { register, handleSubmit, formState, control } = useForm({ + const { register, handleSubmit, formState, control, setValue } = useForm({ defaultValues: fieldsObj, resolver: yupResolver(stepOne.Schema), }); @@ -114,31 +65,13 @@ const StepOne = ({ moveToNextStep(); }; - const onCreateChoice = () => - append({ - value: '', - }); - - const onDestroyChoice = (choiceIdx) => { - remove(choiceIdx); - }; - - const onChoiceChange = (choiceUpdate, choiceIdx) => { - update(choiceIdx, choiceUpdate); - }; - - const initChoices = (choices) => { - replace(choices); - }; - - const choicesss = useWatch({ control, name: 'choices' }); + const tabOption = useWatch({ control, name: 'tabOption' }); const { isDirty, isSubmitting, isValid, errors } = formState; - console.log('ERRORS => ', errors); - console.log('isValid => ', isValid); - console.log('choices => ', choicesss); - console.log('choicesN => ', choicesField); + useEffect(() => { + setStepValid((isDirty || isValid) && !isSubmitting); + }, [isDirty, isValid, isSubmitting, setStepValid]); return ( <> @@ -200,46 +133,16 @@ const StepOne = ({ Text-based presentation for choices that described in words. Use Visual for side-by-side visual options represented by images.

-
-
    -
  • - -
  • -
  • - -
  • -
-
- {tabOption === 'text-based' && ( - - )} - {tabOption === 'visual' && ( - - )} +
From 54f3ef99f434ef7df920655996d9b76d8e313324 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 23 Aug 2022 19:03:30 -0300 Subject: [PATCH 14/36] updates --- .../{StepOne => }/FormConfig.js | 34 +++-- .../StepOne/TextBasedChoices.js | 4 +- .../ProposalCreate/StepOne/index.js | 4 +- .../src/components/ProposalCreate/StepTwo.js | 130 +++++++++--------- .../src/components/common/CustomDatePicker.js | 31 +++++ .../components/common/Editor/DraftjsEditor.js | 12 +- .../client/src/pages/ProposalCreate.js | 6 +- 7 files changed, 132 insertions(+), 89 deletions(-) rename frontend/packages/client/src/components/ProposalCreate/{StepOne => }/FormConfig.js (53%) create mode 100644 frontend/packages/client/src/components/common/CustomDatePicker.js diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js similarity index 53% rename from frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js rename to frontend/packages/client/src/components/ProposalCreate/FormConfig.js index b10a12111..e1c7c5dab 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -1,8 +1,10 @@ +import { Schema } from 'components/Community/ProposalThresholdEditor'; import yup from 'helpers/validation'; -const formFields = ['title', 'strategy', 'body', 'choices', 'tabOption']; +const formFieldsStepOne = ['title', 'strategy', 'body', 'choices', 'tabOption']; +const formFieldsStepTwo = []; -const Schema = yup.object().shape({ +const StepOneSchema = yup.object().shape({ title: yup .string() .trim() @@ -33,14 +35,26 @@ const Schema = yup.object().shape({ .unique('value', 'Invalid duplicated option'), }); -const initialValues = Object.assign( - {}, - ...formFields.map((key) => ({ [key]: '' })) -); +const StepTwoSchema = yup.object().shape({ + startDate: yup.date().required('Please provide a start date'), + startTime: yup.date().required('Please provide a start time'), + endDate: yup.date().required('Please provide a end date'), + endTime: yup.date().required('Please provide a end time'), +}); + +const initialValues = (fields = []) => + Object.assign({}, ...fields.map((key) => ({ [key]: '' }))); + const stepOne = { - Schema, - initialValues, - formFields, + Schema: StepOneSchema, + initialValues: initialValues(formFieldsStepOne), + formFields: formFieldsStepOne, +}; + +const stepTwo = { + Schema: StepTwoSchema, + initialValues: initialValues(formFieldsStepTwo), + formFields: formFieldsStepTwo, }; -export { stepOne }; +export { stepOne, stepTwo }; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index 3d85b6a1b..df96f8844 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -26,7 +26,7 @@ const TextBasedChoices = ({ ? error?.[index]?.value : null; return ( - <> +
)} - + ); })} {error?.message && ( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index a452badd5..96b5391dd 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -9,14 +9,13 @@ import { useCommunityDetails } from 'hooks'; import { kebabToString } from 'utils'; import { yupResolver } from '@hookform/resolvers/yup'; import pick from 'lodash/pick'; +import { stepOne } from '../FormConfig'; import ChoiceOptionCreator from './ChoiceOptionCreator'; -import { stepOne } from './FormConfig'; const StepOne = ({ stepData, setStepValid, onDataChange, - setPreCheckStepAdvance, formId, moveToNextStep, }) => { @@ -60,7 +59,6 @@ const StepOne = ({ }); const onSubmit = (data) => { - console.log('data on submit >>', data); onDataChange(data); moveToNextStep(); }; diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo.js index 4679558af..4ebb644e5 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo.js @@ -1,31 +1,29 @@ import React, { useEffect, useState } from 'react'; import DatePicker from 'react-datepicker'; +import { useForm, useWatch } from 'react-hook-form'; import { Calendar, CaretDown } from 'components/Svg'; +import CustomDatePicker from 'components/common/CustomDatePicker'; +import Form from 'components/common/Form'; import { useMediaQuery } from 'hooks'; +import { yupResolver } from '@hookform/resolvers/yup'; +import pick from 'lodash/pick'; +import { stepTwo } from './FormConfig'; const detectTimeZone = () => new window.Intl.DateTimeFormat().resolvedOptions().timeZone; -const StepTwo = ({ stepData, setStepValid, onDataChange }) => { +const StepTwo = ({ + stepData, + setStepValid, + onDataChange, + formId, + moveToNextStep, +}) => { const [isStartTimeOpen, setStartTimeOpen] = useState(false); const [isEndTimeOpen, setEndTimeOpen] = useState(false); const notMobile = useMediaQuery(); - useEffect(() => { - const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]'; - const requiredFields = { - startDate: isDate, - endDate: isDate, - startTime: isDate, - endTime: isDate, - }; - const isValid = Object.keys(requiredFields).every( - (field) => stepData && requiredFields[field](stepData[field]) - ); - setStepValid(isValid); - }, [stepData, setStepValid, onDataChange]); - const closeStartOnBlur = () => { setStartTimeOpen(false); }; @@ -90,21 +88,44 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { const onSetStartTimeOpen = () => setStartTimeOpen(true); - const setStartTime = (itemValue) => () => { - onDataChange({ - startTime: itemValue, - }); - setStartTimeOpen(false); + const fieldsObj = Object.assign( + {}, + stepTwo.initialValues, + pick(stepData || {}, stepTwo.formFields) + ); + + const { handleSubmit, formState, control, setValue } = useForm({ + defaultValues: fieldsObj, + resolver: yupResolver(stepTwo.Schema), + }); + + const setTime = (field, itemValue) => (e) => { + e.preventDefault(); + e.stopPropagation(); + setValue(field, itemValue); + field === 'startTime' ? setStartTimeOpen(false) : setEndTimeOpen(false); }; - const setEndTime = (itemValue) => () => { - onDataChange({ - endTime: itemValue, - }); - setEndTimeOpen(false); + + const { errors, isValid, isDirty, isSubmitting } = formState; + + console.log('errors ', errors); + + const onSubmit = (data) => { + onDataChange(data); + moveToNextStep(); }; + const startDate = useWatch({ control, name: 'startDate' }); + const startTime = useWatch({ control, name: 'startTime' }); + const endDate = useWatch({ control, name: 'endDate' }); + const endTime = useWatch({ control, name: 'endTime' }); + + useEffect(() => { + setStepValid((isValid || isDirty) && !isSubmitting); + }, [isValid, isDirty, isSubmitting, setStepValid]); + return ( -
+

Start date and time * @@ -114,20 +135,10 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { className="columns is-mobile p-0 pr-2 p-0-mobile mb-4-mobile m-0 column is-half" style={{ position: 'relative' }} > - !notMobile && e.target.blur()} - onChange={(date) => { - onDataChange({ - startDate: date, - // resets time in case user has selected a future date and comes back to present with a non valid hour - startTime: isToday(date) ? null : stepData?.startTime, - }); - }} - className="border-light rounded-sm column is-full is-full-mobile p-3" +
{
{ onClick={onSetStartTimeOpen} >
- {stepData?.startTime - ? formatTime(stepData.startTime) - : 'Select Time'} + {startTime ? formatTime(startTime) : 'Select Time'}
@@ -176,9 +185,9 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { {timeIntervals.map((itemValue, index) => ( @@ -262,7 +262,7 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { className={`button is-white dropdown-item has-text-grey${ itemValue === stepData?.endTime ? ' is-active' : '' }`} - onMouseDown={setEndTime(itemValue)} + onMouseDown={setTime('endTime', itemValue)} key={`drop-down-${index}`} > {formatTime(itemValue)} @@ -280,7 +280,7 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { zone as: {timeZone}
)} -
+ ); }; diff --git a/frontend/packages/client/src/components/common/CustomDatePicker.js b/frontend/packages/client/src/components/common/CustomDatePicker.js new file mode 100644 index 000000000..e00a53e69 --- /dev/null +++ b/frontend/packages/client/src/components/common/CustomDatePicker.js @@ -0,0 +1,31 @@ +import React from 'react'; +import DatePicker from 'react-datepicker'; +import { Controller } from 'react-hook-form'; +import FadeIn from 'components/FadeIn'; + +export default function CustomDatePicker({ + control, + fieldName, + notMobile, + minDate, + disabled = false, +} = {}) { + return ( + ( + !notMobile && e.target.blur()} + onChange={(date) => field.onChange(date)} + className="border-light rounded-sm column is-full is-full-mobile p-3" + disabled={disabled} + /> + )} + /> + ); +} diff --git a/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js b/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js index 184307985..7ff002524 100644 --- a/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js +++ b/frontend/packages/client/src/components/common/Editor/DraftjsEditor.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; import { Editor } from 'react-draft-wysiwyg'; import { UploadImageModal } from 'components'; import { customDraftToHTML, customHTMLtoDraft } from 'utils'; @@ -50,7 +50,7 @@ const blockRenderMap = Map({ // keep support for other draft default block types and add our image-caption type const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap); -export default function CustomEditor({ onChange, value, ref } = {}) { +const CustomEditor = forwardRef(({ onChange, value }, ref) => { const [localEditorState, setLocalEditorState] = useState( EditorState.createEmpty() ); @@ -69,10 +69,6 @@ export default function CustomEditor({ onChange, value, ref } = {}) { const onEditorStateChange = (editorState) => { !updated && setUpdated(true); setLocalEditorState(editorState); - console.log( - 'customDraftToHTML', - customDraftToHTML(editorState.getCurrentContent()) - ); const pureHtml = customDraftToHTML(editorState.getCurrentContent()); // when editor es empty but has been changed it will return


const textOnly = pureHtml.replace(/<[^>]+>/g, ''); @@ -229,4 +225,6 @@ export default function CustomEditor({ onChange, value, ref } = {}) { /> ); -} +}); + +export default CustomEditor; diff --git a/frontend/packages/client/src/pages/ProposalCreate.js b/frontend/packages/client/src/pages/ProposalCreate.js index 9c8face2f..f53d16099 100644 --- a/frontend/packages/client/src/pages/ProposalCreate.js +++ b/frontend/packages/client/src/pages/ProposalCreate.js @@ -116,6 +116,7 @@ export default function ProposalCreatePage() { blockNavigationText: 'Proposal creation is not complete yet, are you sure you want to leave?', passNextToComp: true, + passSubmitToComp: true, showActionButtonLeftPannel: true, steps: [ { @@ -129,13 +130,14 @@ export default function ProposalCreatePage() { label: 'Set Date & Time', description: 'Some description of what you can write here that is useful.', - component: , + component: , + useHookForms: true, }, { label: 'Preview Proposal', description: 'Some description of what you can write here that is useful.', - component: , + component: , }, ], }; From ce6da1068fe4a856499e1a9bb17064c5bc4f8c09 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Fri, 26 Aug 2022 10:04:01 -0300 Subject: [PATCH 15/36] updates --- .../client/src/components/ProposalCreate/FormConfig.js | 4 ++-- .../ProposalCreate/StepOne/ChoiceOptionCreator.js | 7 +++++-- .../ProposalCreate/StepOne/ImageChoiceUploader.js | 1 + .../components/ProposalCreate/StepOne/ImageChoices.js | 10 +++++++++- .../ProposalCreate/StepOne/TextBasedChoices.js | 8 +++----- .../src/components/ProposalCreate/StepOne/index.js | 5 ++++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index e1c7c5dab..59988b05e 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -1,4 +1,3 @@ -import { Schema } from 'components/Community/ProposalThresholdEditor'; import yup from 'helpers/validation'; const formFieldsStepOne = ['title', 'strategy', 'body', 'choices', 'tabOption']; @@ -14,7 +13,8 @@ const StepOneSchema = yup.object().shape({ body: yup.string().required('Please enter a proposal description'), tabOption: yup.string().oneOf(['text-based', 'visual']), choices: yup - .array( + .array() + .of( yup .object({ value: yup.string().required('Please enter option value'), diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js index 5439924f4..4bedfee94 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; @@ -11,6 +11,8 @@ export default function ChoiceOptionCreator({ update, setValue = () => {}, error = [], + register, + fieldName, } = {}) { // tabOption value is sabed on form const setTab = (option) => (e) => { @@ -59,11 +61,12 @@ export default function ChoiceOptionCreator({ {tabOption === 'text-based' && ( )} {tabOption === 'visual' && ( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index 95c13f15b..4e769fb8a 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -58,6 +58,7 @@ export default function ImageChoiceUploader({ onImageUpdate, image: imageParam, letterLabel, + error: errorParam, } = {}) { const [errorMessage, setErrorMessage] = useState(null); // existing image and component receives props diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js index fb928724f..a52fa8643 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js @@ -2,7 +2,13 @@ import React, { useEffect } from 'react'; import { getProposalType } from 'utils'; import ImageChoiceUploader from './ImageChoiceUploader'; -const ImageChoices = ({ choices = [], onChoiceChange, initChoices } = {}) => { +const ImageChoices = ({ + choices = [], + onChoiceChange, + initChoices, + error, +} = {}) => { + const [errorOptOne, errorOptTwo] = error; useEffect(() => { if (getProposalType(choices) !== 'image') { initChoices([ @@ -42,6 +48,7 @@ const ImageChoices = ({ choices = [], onChoiceChange, initChoices } = {}) => { }} letterLabel="A" onImageUpdate={onImageUpdate(0)} + error={errorOptOne} />

@@ -52,6 +59,7 @@ const ImageChoices = ({ choices = [], onChoiceChange, initChoices } = {}) => { }} letterLabel="B" onImageUpdate={onImageUpdate(1)} + error={errorOptTwo} />
diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index df96f8844..29af616ec 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -6,7 +6,8 @@ import { getProposalType } from 'utils'; const TextBasedChoices = ({ choices = [], - onChoiceChange, + fieldName = 'choices', + register, onDestroyChoice, onCreateChoice, initChoices, @@ -39,10 +40,7 @@ const TextBasedChoices = ({ className={`border-light rounded-sm p-3 column is-full pr-6 ${ !errorInField ? 'mb-4' : '' }`} - value={choice.value} - onChange={(event) => - onChoiceChange(index, { value: event.target.value }) - } + {...register(`${fieldName}.${index}.value`)} autoFocus />
@@ -137,9 +138,11 @@ const StepOne = ({ setValue={setValue} append={append} remove={remove} - update={update} replace={replace} error={errors['choices']} + fieldName="choices" + register={register} + update={update} />
From bc537ed67c33fc9319ceaeb1c41b96752b9cc72f Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Mon, 29 Aug 2022 18:37:47 -0300 Subject: [PATCH 16/36] updates --- .../components/ProposalCreate/FormConfig.js | 8 + .../ProposalCreate/StepOne/index.js | 32 ++-- .../src/components/ProposalCreate/StepTwo.js | 152 ++++++++++-------- .../src/components/common/CustomDatePicker.js | 5 +- 4 files changed, 110 insertions(+), 87 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index 59988b05e..66c4c3a1d 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -33,6 +33,14 @@ const StepOneSchema = yup.object().shape({ ) .min(2, 'Please add a choice, minimun amout is two') .unique('value', 'Invalid duplicated option'), + maxWeight: yup + .string() + .trim() + .matches(/(^[0-9]+$)/, 'Proposal max weight must be a number'), + minBalance: yup + .string() + .trim() + .matches(/(^[0-9]+$)/, 'Proposal min balance must be a number'), }); const StepTwoSchema = yup.object().shape({ diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 42f983f8b..9987969b0 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -64,6 +64,7 @@ const StepOne = ({ }; const tabOption = useWatch({ control, name: 'tabOption' }); + const defaultValueStrategy = useWatch({ control, name: 'strategy' }); const { isDirty, isSubmitting, isValid, errors } = formState; @@ -71,9 +72,8 @@ const StepOne = ({ setStepValid((isDirty || isValid) && !isSubmitting); }, [isDirty, isValid, isSubmitting, setStepValid]); - const defaultValueStrategy = stepData?.strategy; - console.log('all errors ', errors); + return ( <> @@ -126,27 +126,19 @@ const StepOne = ({ /> {defaultValueStrategy && ( <> - - onDataChange({ - minBalance: event.target.value, - }) - } + classNames="rounded-sm border-light p-3 column is-full mt-4 mb-4" + register={register} + error={errors['minBalance']} + name="minBalance" /> - - onDataChange({ - maxWeight: event.target.value, - }) - } + classNames="rounded-sm border-light p-3 column is-full" + register={register} + error={errors['maxWeight']} + name="maxWeight" /> )} diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo.js index 407264f0d..dbdd8a162 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo.js @@ -1,8 +1,13 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; +import React, { useEffect, useState } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; import { Calendar, CaretDown } from 'components/Svg'; +import CustomDatePicker from 'components/common/CustomDatePicker'; +import Form from 'components/common/Form'; import { useMediaQuery } from 'hooks'; import { HAS_DELAY_ON_START_TIME } from 'const'; +import { yupResolver } from '@hookform/resolvers/yup'; +import pick from 'lodash/pick'; +import { stepTwo } from './FormConfig'; const detectTimeZone = () => new window.Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -80,28 +85,18 @@ const TimeIntervals = ({ date, time, setTime, type } = {}) => { ); }; -const StepTwo = ({ stepData, setStepValid, onDataChange }) => { +const StepTwo = ({ + stepData, + setStepValid, + onDataChange, + formId, + moveToNextStep, +}) => { const [isStartTimeOpen, setStartTimeOpen] = useState(false); const [isEndTimeOpen, setEndTimeOpen] = useState(false); const notMobile = useMediaQuery(); - useEffect(() => { - const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]'; - const requiredFields = { - startDate: isDate, - endDate: isDate, - startTime: isDate, - endTime: isDate, - }; - - const isValid = Object.keys(requiredFields).every( - (field) => stepData && requiredFields[field](stepData[field]) - ); - - setStepValid(isValid); - }, [stepData, setStepValid, onDataChange]); - const closeStartOnBlur = () => { setStartTimeOpen(false); }; @@ -112,38 +107,74 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { const timeZone = detectTimeZone(); - const onSetStartTimeOpen = () => setStartTimeOpen(true); + const onSetStartTimeOpen = (e) => { + e.preventDefault(); + e.stopPropagation(); + setStartTimeOpen(true); + }; + const onSetEndTimeOpen = (e) => { + e.preventDefault(); + e.stopPropagation(); + setEndTimeOpen(true); + }; - const setStartTime = useCallback( - (itemValue) => () => { - onDataChange({ - startTime: itemValue, - }); - setStartTimeOpen(false); - }, - [onDataChange] + const fieldsObj = Object.assign( + {}, + stepTwo.initialValues, + pick(stepData || {}, stepTwo.formFields) ); - const setEndTime = useCallback( - (itemValue) => () => { - onDataChange({ - endTime: itemValue, - }); - setEndTimeOpen(false); - }, - [onDataChange] - ); + const { handleSubmit, formState, control, setValue } = useForm({ + defaultValues: fieldsObj, + resolver: yupResolver(stepTwo.Schema), + }); + + const setTime = (field) => (itemValue) => (e) => { + e.preventDefault(); + e.stopPropagation(); + setValue(field, itemValue); + field === 'startTime' ? setStartTimeOpen(false) : setEndTimeOpen(false); + }; + + const { errors, isValid, isDirty, isSubmitting } = formState; + + console.log('errors ', errors); + + const onSubmit = (data) => { + onDataChange(data); + moveToNextStep(); + }; + + const startDate = useWatch({ control, name: 'startDate' }); + const startTime = useWatch({ control, name: 'startTime' }); + const endDate = useWatch({ control, name: 'endDate' }); + const endTime = useWatch({ control, name: 'endTime' }); + + useEffect(() => { + setStepValid((isValid || isDirty) && !isSubmitting); + }, [isValid, isDirty, isSubmitting, setStepValid]); + + useEffect(() => { + if ( + isToday(startDate) && + startTime && + new Date().setHours(startTime.getHours(), startTime.getMinutes(), 0, 0) < + new Date().setHours(1, 0, 0, 0) + ) { + setValue('startTime', ''); + } + }, [startDate, startTime, setValue]); const minDateForStartDate = new Date( HAS_DELAY_ON_START_TIME ? Date.now() + 60 * 60 * 1000 : Date.now() ); - const maxDateForStartDate = stepData?.endDate - ? subtractDays(new Date(stepData?.endDate), 1) + const maxDateForStartDate = endDate + ? subtractDays(new Date(endDate), 1) : undefined; return ( -
+

Start date and time * @@ -153,21 +184,13 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { className="columns is-mobile p-0 pr-2 p-0-mobile mb-4-mobile m-0 column is-half" style={{ position: 'relative' }} > - !notMobile && e.target.blur()} - onChange={(date) => { - onDataChange({ - startDate: date, - // resets time in case user has selected a future date and comes back to present with a non valid hour - startTime: isToday(date) ? null : stepData?.startTime, - }); - }} - className="border-light rounded-sm column is-full is-full-mobile p-3" />
{
{ onClick={onSetStartTimeOpen} >
- {stepData?.startTime - ? formatTime(stepData.startTime) - : 'Select Time'} + {startTime ? formatTime(startTime) : 'Select Time'}
@@ -214,10 +235,10 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { style={{ maxHeight: 300, overflow: 'auto' }} >
@@ -235,6 +256,7 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { style={{ position: 'relative' }} > { className="button rounded-sm is-outlined border-light column m-0 py-0 px-3 is-full-mobile" aria-haspopup="true" aria-controls="dropdown-menu" - onClick={() => setEndTimeOpen(true)} + onClick={onSetEndTimeOpen} >
- {stepData?.endTime - ? formatTime(stepData.endTime) - : 'Select Time'} + {endTime ? formatTime(endTime) : 'Select Time'}
@@ -286,9 +306,9 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { style={{ maxHeight: 300, overflow: 'auto' }} >

@@ -302,7 +322,7 @@ const StepTwo = ({ stepData, setStepValid, onDataChange }) => { zone as: {timeZone}
)} -
+ ); }; diff --git a/frontend/packages/client/src/components/common/CustomDatePicker.js b/frontend/packages/client/src/components/common/CustomDatePicker.js index e00a53e69..a53308f9a 100644 --- a/frontend/packages/client/src/components/common/CustomDatePicker.js +++ b/frontend/packages/client/src/components/common/CustomDatePicker.js @@ -8,7 +8,9 @@ export default function CustomDatePicker({ fieldName, notMobile, minDate, + maxDate, disabled = false, + placeholderText, } = {}) { return ( ( !notMobile && e.target.blur()} onChange={(date) => field.onChange(date)} className="border-light rounded-sm column is-full is-full-mobile p-3" From 222add0f29803cb64009400900824982d67d55b4 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Mon, 29 Aug 2022 18:42:29 -0300 Subject: [PATCH 17/36] update --- .../client/src/components/ProposalCreate/StepThree.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepThree.js b/frontend/packages/client/src/components/ProposalCreate/StepThree.js index 3dfbcb299..1683ff141 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepThree.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepThree.js @@ -1,6 +1,5 @@ import React, { useCallback, useEffect } from 'react'; import { parseDateToServer } from 'utils'; -import { customDraftToHTML } from 'utils'; import { ProposalStatus, VoteOptions } from '../Proposal'; const StepThree = ({ stepsData, setStepValid }) => { @@ -27,9 +26,7 @@ const StepThree = ({ stepsData, setStepValid }) => { })), }; - const currentContent = stepsData[0]?.description?.getCurrentContent(); - - const htmlBody = customDraftToHTML(currentContent); + const htmlBody = stepsData[0]?.body; return (
From 6d3d35a3ac85d49d31b722e58574845b360997cc Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 30 Aug 2022 10:06:37 -0300 Subject: [PATCH 18/36] update validation --- .../client/src/components/ProposalCreate/FormConfig.js | 4 ++-- .../components/ProposalCreate/StepOne/ChoiceOptionCreator.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index 66c4c3a1d..1eb30317a 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -36,11 +36,11 @@ const StepOneSchema = yup.object().shape({ maxWeight: yup .string() .trim() - .matches(/(^[0-9]+$)/, 'Proposal max weight must be a number'), + .matches(/\s+$|^$|(^[0-9]+$)/, 'Proposal max weight must be a number'), minBalance: yup .string() .trim() - .matches(/(^[0-9]+$)/, 'Proposal min balance must be a number'), + .matches(/\s+$|^$|(^[0-9]+$)/, 'Proposal min balance must be a number'), }); const StepTwoSchema = yup.object().shape({ diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js index 4bedfee94..96187298f 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; From 5bbb771c9e0ca14cfcecee10278918ba1a605f52 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 30 Aug 2022 13:38:02 -0300 Subject: [PATCH 19/36] updates --- .../components/ProposalCreate/FormConfig.js | 30 +-- .../StepOne/ChoiceOptionCreator.js | 11 +- .../StepOne/ImageChoiceUploader.js | 33 ++- .../ProposalCreate/StepOne/ImageChoices.js | 28 +-- .../StepOne/TextBasedChoices.js | 11 +- .../ProposalCreate/StepOne/index.js | 191 +++++++++--------- 6 files changed, 136 insertions(+), 168 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index 1eb30317a..84c84b3bc 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -15,22 +15,24 @@ const StepOneSchema = yup.object().shape({ choices: yup .array() .of( - yup - .object({ + yup.object({ + value: yup.string().required('Please enter option value'), + choiceImgUrl: yup.string().nullable(), + }) + ) + .when('tabOption', { + is: 'visual', + then: yup.array().of( + yup.object({ value: yup.string().required('Please enter option value'), - choiceImgUrl: yup.string().nullable(), - }) - .when('tabOption', { - is: (option) => option === 'visual', - then: yup.object({ - value: yup.string().required('Please enter option value'), - choiceImgUrl: yup - .string() - .url() - .required('Image option is not valid'), - }), + choiceImgUrl: yup + .string() + .trim() + .url('Image option is not valid') + .required('Please upload an image'), }) - ) + ), + }) .min(2, 'Please add a choice, minimun amout is two') .unique('value', 'Invalid duplicated option'), maxWeight: yup diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js index 96187298f..3390f8124 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -7,7 +7,6 @@ export default function ChoiceOptionCreator({ choices, append, remove, - replace, update, setValue = () => {}, error = [], @@ -29,8 +28,6 @@ export default function ChoiceOptionCreator({ }); }; - const initChoices = (choices) => replace(choices); - return ( <>
@@ -63,19 +60,13 @@ export default function ChoiceOptionCreator({ choices={choices} onDestroyChoice={remove} onCreateChoice={onCreateChoice} - initChoices={initChoices} error={error} register={register} fieldName={fieldName} /> )} {tabOption === 'visual' && ( - + )} ); diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index 4e769fb8a..c6f87fc8b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useDropzone } from 'react-dropzone'; -import { Loader } from 'components'; +import { FadeIn, Loader } from 'components'; import { Bin, Upload } from 'components/Svg'; import { useFileUploader } from 'hooks'; import { MAX_FILE_SIZE } from 'const'; @@ -46,14 +46,6 @@ const UploadArea = ({ getRootProps, getInputProps, errorMessage }) => { ); }; -// initial state when no image has been uploaded -const initialState = { - imageUrl: null, - uploadStatus: null, - file: null, - text: '', -}; - export default function ImageChoiceUploader({ onImageUpdate, image: imageParam, @@ -62,17 +54,15 @@ export default function ImageChoiceUploader({ } = {}) { const [errorMessage, setErrorMessage] = useState(null); // existing image and component receives props + console.log('imageParam', imageParam); const { imageUrl, text } = imageParam; - const existingImage = { - imageUrl, - uploadStatus: IMAGE_STATUS.uploaded, + + const [image, setImage] = useState({ + imageUrl: imageUrl === '' ? null : imageUrl, + uploadStatus: imageUrl === '' ? null : IMAGE_STATUS.uploaded, file: null, text, - }; - - const [image, setImage] = useState( - imageParam.imageUrl === '' ? initialState : existingImage - ); + }); const { uploadFile, loading, error } = useFileUploader({ useModalNotifications: false, @@ -254,6 +244,15 @@ export default function ImageChoiceUploader({ } style={{ width: '100%' }} /> + {errorParam?.value?.message && ( + +
+

+ {errorParam.value.message} +

+
+
+ )}
); } diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js index a52fa8643..f76122942 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js @@ -1,34 +1,14 @@ -import React, { useEffect } from 'react'; -import { getProposalType } from 'utils'; +import React from 'react'; import ImageChoiceUploader from './ImageChoiceUploader'; -const ImageChoices = ({ - choices = [], - onChoiceChange, - initChoices, - error, -} = {}) => { - const [errorOptOne, errorOptTwo] = error; - useEffect(() => { - if (getProposalType(choices) !== 'image') { - initChoices([ - { - value: '', - choiceImgUrl: '', - }, - { - value: '', - choiceImgUrl: '', - }, - ]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +const ImageChoices = ({ choices = [], onChoiceChange, error } = {}) => { + const [errorOptOne, errorOptTwo] = Array.isArray(error) ? error : []; const onImageUpdate = (index) => (image) => { onChoiceChange(index, { value: image.text, choiceImgUrl: image.imageUrl }); }; + console.log('cjoices', choices); const [choiceA, choiceB] = choices; return ( <> diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index 29af616ec..9bdf4d73f 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -1,8 +1,7 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import AddButton from 'components/AddButton'; import FadeIn from 'components/FadeIn'; import { Bin } from 'components/Svg'; -import { getProposalType } from 'utils'; const TextBasedChoices = ({ choices = [], @@ -10,16 +9,8 @@ const TextBasedChoices = ({ register, onDestroyChoice, onCreateChoice, - initChoices, error, } = {}) => { - useEffect(() => { - if (getProposalType(choices) !== 'text-based') { - initChoices([]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( <> {choices?.map((choice, index) => { diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 9987969b0..78d788fd8 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -42,6 +42,7 @@ const StepOne = ({ ); const { register, handleSubmit, formState, control, setValue } = useForm({ + reValidateMode: 'onChange', defaultValues: fieldsObj, resolver: yupResolver(stepOne.Schema), }); @@ -59,7 +60,13 @@ const StepOne = ({ }); const onSubmit = (data) => { - onDataChange(data); + let choices; + if (data.tabOption === 'visual') { + choices = data.choices.slice(0, 2); + } else { + choices = data.choices.map((e) => ({ value: e.value })); + } + onDataChange({ ...data, choices }); moveToNextStep(); }; @@ -75,99 +82,97 @@ const StepOne = ({ console.log('all errors ', errors); return ( - <> -
-
-
-

- Title * -

-

- Give your proposal a title based on the decision or initiative - being voted on. Best to keep it simple and specific. -

- -
-
-

- Description * -

-

- This is where you build the key information for the proposal: the - details of what’s being voted on; background information for - context; the expected costs and benefits of this collective - decision. -

- -
-
-

Voting Strategy

-

- Select a strategy for how voting power is calculated. Voting - strategies are set by community admins. -

- ({ - label: vs.name, - value: vs.key, - })) ?? [] - } - disabled={isSubmitting || votingStrategies.length === 0} - control={control} - /> - {defaultValueStrategy && ( - <> - - - - )} -
-
-

- Choices * -

-

- Provide the specific options you’d like to cast votes for. Use - Text-based presentation for choices that described in words. Use - Visual for side-by-side visual options represented by images. -

- -
+ +
+
+

+ Title * +

+

+ Give your proposal a title based on the decision or initiative being + voted on. Best to keep it simple and specific. +

+
- - +
+

+ Description * +

+

+ This is where you build the key information for the proposal: the + details of what’s being voted on; background information for + context; the expected costs and benefits of this collective + decision. +

+ +
+
+

Voting Strategy

+

+ Select a strategy for how voting power is calculated. Voting + strategies are set by community admins. +

+ ({ + label: vs.name, + value: vs.key, + })) ?? [] + } + disabled={isSubmitting || votingStrategies.length === 0} + control={control} + /> + {defaultValueStrategy && ( + <> + + + + )} +
+
+

+ Choices * +

+

+ Provide the specific options you’d like to cast votes for. Use + Text-based presentation for choices that described in words. Use + Visual for side-by-side visual options represented by images. +

+ +
+
+ ); }; From fe47c82b223e38ee0f2f9e88a0cd3747ff050143 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 30 Aug 2022 13:45:02 -0300 Subject: [PATCH 20/36] add initial state back --- .../ProposalCreate/StepOne/ImageChoiceUploader.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index c6f87fc8b..7a1352d1b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -13,6 +13,13 @@ const IMAGE_STATUS = { toBeDeleted: 'to-be-deleted', }; +const initialState = { + imageUrl: null, + uploadStatus: null, + file: null, + text: '', +}; + const UploadArea = ({ getRootProps, getInputProps, errorMessage }) => { return ( <> From badec5ba413b8b991ff90c44ff6846aa6c780810 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 30 Aug 2022 17:48:12 -0300 Subject: [PATCH 21/36] updates for choices --- .../components/ProposalCreate/FormConfig.js | 4 +-- .../StepOne/ChoiceOptionCreator.js | 26 +++++----------- .../ProposalCreate/StepOne/ImageChoices.js | 26 +++++++++++++--- .../StepOne/TextBasedChoices.js | 30 +++++++++++++----- .../ProposalCreate/StepOne/index.js | 31 ++++++------------- .../client/src/components/common/Input.js | 5 ++- 6 files changed, 67 insertions(+), 55 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index 84c84b3bc..c8ba05284 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -1,7 +1,7 @@ import yup from 'helpers/validation'; const formFieldsStepOne = ['title', 'strategy', 'body', 'choices', 'tabOption']; -const formFieldsStepTwo = []; +const formFieldsStepTwo = ['startDate', 'endDate', 'startTime', 'endTime']; const StepOneSchema = yup.object().shape({ title: yup @@ -53,7 +53,7 @@ const StepTwoSchema = yup.object().shape({ }); const initialValues = (fields = []) => - Object.assign({}, ...fields.map((key) => ({ [key]: '' }))); + Object.assign({}, ...fields.map((key) => ({ [key]: undefined }))); const stepOne = { Schema: StepOneSchema, diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js index 3390f8124..4fa8d29d9 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -1,33 +1,24 @@ import React from 'react'; +import { useWatch } from 'react-hook-form'; import ImageChoices from './ImageChoices'; import TextBasedChoices from './TextBasedChoices'; export default function ChoiceOptionCreator({ - tabOption, - choices, - append, - remove, - update, setValue = () => {}, error = [], register, fieldName, + control, } = {}) { - // tabOption value is sabed on form + const tabOption = useWatch({ control, name: 'tabOption' }); + + // tabOption value is saved on form const setTab = (option) => (e) => { e.preventDefault(); e.stopPropagation(); setValue('tabOption', option); }; - const onCreateChoice = (e) => { - e.preventDefault(); - e.stopPropagation(); - append({ - value: '', - }); - }; - return ( <>
@@ -54,19 +45,16 @@ export default function ChoiceOptionCreator({
- {tabOption === 'text-based' && ( )} {tabOption === 'visual' && ( - + )} ); diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js index f76122942..e98423571 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoices.js @@ -1,14 +1,32 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useFieldArray } from 'react-hook-form'; import ImageChoiceUploader from './ImageChoiceUploader'; -const ImageChoices = ({ choices = [], onChoiceChange, error } = {}) => { +const ImageChoices = ({ error, control } = {}) => { const [errorOptOne, errorOptTwo] = Array.isArray(error) ? error : []; + const { + fields: choices, + update, + append, + } = useFieldArray({ + control, + name: 'choices', + focusAppend: true, + }); + + useEffect(() => { + if (choices.length < 2) { + const size = 2 - choices.length; + const toAdd = new Array(size).fill({ value: '', choiceImgUrl: '' }); + append(toAdd); + } + }, [choices, append]); + const onImageUpdate = (index) => (image) => { - onChoiceChange(index, { value: image.text, choiceImgUrl: image.imageUrl }); + update(index, { value: image.text, choiceImgUrl: image.imageUrl }); }; - console.log('cjoices', choices); const [choiceA, choiceB] = choices; return ( <> diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index 9bdf4d73f..fba38a64b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -1,16 +1,33 @@ import React from 'react'; +import { useFieldArray } from 'react-hook-form'; import AddButton from 'components/AddButton'; import FadeIn from 'components/FadeIn'; import { Bin } from 'components/Svg'; const TextBasedChoices = ({ - choices = [], fieldName = 'choices', register, - onDestroyChoice, - onCreateChoice, error, + control, } = {}) => { + const { + fields: choices, + append, + remove, + } = useFieldArray({ + control, + name: 'choices', + focusAppend: true, + }); + + const onCreateChoice = (index) => (e) => { + e.preventDefault(); + e.stopPropagation(); + append({ + value: '', + }); + }; + return ( <> {choices?.map((choice, index) => { @@ -18,9 +35,9 @@ const TextBasedChoices = ({ ? error?.[index]?.value : null; return ( - +
@@ -32,7 +49,6 @@ const TextBasedChoices = ({ !errorInField ? 'mb-4' : '' }`} {...register(`${fieldName}.${index}.value`)} - autoFocus />
onDestroyChoice(index)} + onClick={() => remove(index)} >
diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 78d788fd8..dcf342fe1 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -1,5 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import { useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import Dropdown from 'components/common/Dropdown'; import { Editor } from 'components/common/Editor'; @@ -37,7 +37,10 @@ const StepOne = ({ const fieldsObj = Object.assign( {}, stepOne.initialValues, - { choices: [], tabOption: 'text-based' }, + { + choices: [], + tabOption: 'text-based', + }, pick(stepData || {}, stepOne.formFields) ); @@ -47,18 +50,6 @@ const StepOne = ({ resolver: yupResolver(stepOne.Schema), }); - const { - fields: choicesField, - append, - remove, - update, - replace, - } = useFieldArray({ - control, - name: 'choices', - focusAppend: true, - }); - const onSubmit = (data) => { let choices; if (data.tabOption === 'visual') { @@ -70,7 +61,6 @@ const StepOne = ({ moveToNextStep(); }; - const tabOption = useWatch({ control, name: 'tabOption' }); const defaultValueStrategy = useWatch({ control, name: 'strategy' }); const { isDirty, isSubmitting, isValid, errors } = formState; @@ -134,7 +124,8 @@ const StepOne = ({ <>
diff --git a/frontend/packages/client/src/components/common/Input.js b/frontend/packages/client/src/components/common/Input.js index 8c349dbec..9533dfb73 100644 --- a/frontend/packages/client/src/components/common/Input.js +++ b/frontend/packages/client/src/components/common/Input.js @@ -10,9 +10,12 @@ export default function Input({ disabled, error, type = 'text', + conatinerClassNames = '', } = {}) { return ( -
+
Date: Tue, 30 Aug 2022 19:31:29 -0300 Subject: [PATCH 22/36] updates --- .../components/ProposalCreate/FormConfig.js | 4 +- .../StepOne/TextBasedChoices.js | 2 +- .../ProposalCreate/{ => StepTwo}/StepTwo.js | 185 ++++++++---------- .../ProposalCreate/StepTwo/TimeIntervals.js | 56 ++++++ .../ProposalCreate/StepTwo/index.js | 1 + .../src/components/ProposalCreate/index.js | 2 +- frontend/packages/client/src/utils.js | 10 + 7 files changed, 155 insertions(+), 105 deletions(-) rename frontend/packages/client/src/components/ProposalCreate/{ => StepTwo}/StepTwo.js (64%) create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepTwo/TimeIntervals.js create mode 100644 frontend/packages/client/src/components/ProposalCreate/StepTwo/index.js diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index c8ba05284..ec5c2454b 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -48,8 +48,8 @@ const StepOneSchema = yup.object().shape({ const StepTwoSchema = yup.object().shape({ startDate: yup.date().required('Please provide a start date'), startTime: yup.date().required('Please provide a start time'), - endDate: yup.date().required('Please provide a end date'), - endTime: yup.date().required('Please provide a end time'), + endDate: yup.date().required('Please provide an end date'), + endTime: yup.date().required('Please provide an end time'), }); const initialValues = (fields = []) => diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index fba38a64b..fba024258 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -20,7 +20,7 @@ const TextBasedChoices = ({ focusAppend: true, }); - const onCreateChoice = (index) => (e) => { + const onCreateChoice = (e) => { e.preventDefault(); e.stopPropagation(); append({ diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js similarity index 64% rename from frontend/packages/client/src/components/ProposalCreate/StepTwo.js rename to frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js index dbdd8a162..be4558193 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js @@ -1,33 +1,20 @@ import React, { useEffect, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; +import { FadeIn } from 'components'; import { Calendar, CaretDown } from 'components/Svg'; import CustomDatePicker from 'components/common/CustomDatePicker'; import Form from 'components/common/Form'; import { useMediaQuery } from 'hooks'; import { HAS_DELAY_ON_START_TIME } from 'const'; +import { formatTime } from 'utils'; import { yupResolver } from '@hookform/resolvers/yup'; import pick from 'lodash/pick'; -import { stepTwo } from './FormConfig'; +import { stepTwo } from '../FormConfig'; +import TimeIntervals from './TimeIntervals'; const detectTimeZone = () => new window.Intl.DateTimeFormat().resolvedOptions().timeZone; -const getTimeIntervals = (cutOffDate = 0) => { - const timeIntervals = []; - for (let i = 0; i < 24; i++) { - for (let j = 0; j < 4; j++) { - let time = new Date(); - time.setHours(i); - time.setMinutes(j * 15); - time.setSeconds(0); - if (time.getTime() >= cutOffDate) { - timeIntervals.push(time); - } - } - } - return timeIntervals; -}; - const addDays = (date, days) => { date.setDate(date.getDate() + days); return date; @@ -38,53 +25,10 @@ const subtractDays = (date, days) => { return date; }; -const formatTime = (date) => { - let hours = date.getHours(); - let minutes = date.getMinutes(); - const ampm = hours >= 12 ? 'pm' : 'am'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' - minutes = minutes < 10 ? '0' + minutes : minutes; - return hours + ':' + minutes + ' ' + ampm; -}; - const isToday = (date) => { return date?.setHours(0, 0, 0, 0) === new Date().setHours(0, 0, 0, 0); }; -const TimeIntervals = ({ date, time, setTime, type } = {}) => { - const startDateIsToday = date ? isToday(date) : false; - - const startTimeInterval = startDateIsToday - ? HAS_DELAY_ON_START_TIME - ? new Date(Date.now() + 60 * 60 * 1000) - : Date.now() - : 0; - - const timeIntervals = getTimeIntervals(startTimeInterval); - - // this enables setting start time inmediatly - if (startDateIsToday && !HAS_DELAY_ON_START_TIME) { - // push date now to the top of timeIntervals - timeIntervals[0] !== new Date() && timeIntervals.unshift(new Date()); - } - return ( - <> - {timeIntervals.map((itemValue, index) => ( - - ))} - - ); -}; - const StepTwo = ({ stepData, setStepValid, @@ -212,18 +156,31 @@ const StepTwo = ({ aria-haspopup="true" aria-controls="dropdown-menu" > -
- +
+ {errors?.startTime?.message && ( +
+ +
+

+ {errors?.startTime?.message} +

+
+
- + )}
*
-
- +
- + +
+ +
+ {errors?.endTime?.message && ( +
+ +
+

+ {errors?.endTime?.message} +

+
+
+
+ )}
-
- +
+ {errors?.endTime?.message && ( +
+ +
+

+ {errors?.endTime?.message} +

+
+
- + )}
{ + const timeIntervals = []; + for (let i = 0; i < 24; i++) { + for (let j = 0; j < 4; j++) { + let time = new Date(); + time.setHours(i); + time.setMinutes(j * 15); + time.setSeconds(0); + if (time.getTime() >= cutOffDate) { + timeIntervals.push(time); + } + } + } + return timeIntervals; +}; + +const isToday = (date) => { + return date?.setHours(0, 0, 0, 0) === new Date().setHours(0, 0, 0, 0); +}; + +export default function TimeIntervals({ date, time, setTime, type } = {}) { + const startDateIsToday = date ? isToday(date) : false; + + const startTimeInterval = startDateIsToday + ? HAS_DELAY_ON_START_TIME + ? new Date(Date.now() + 60 * 60 * 1000) + : Date.now() + : 0; + + const timeIntervals = getTimeIntervals(startTimeInterval); + + // this enables setting start time inmediatly + if (startDateIsToday && !HAS_DELAY_ON_START_TIME) { + // push date now to the top of timeIntervals + timeIntervals[0] !== new Date() && timeIntervals.unshift(new Date()); + } + return ( + <> + {timeIntervals.map((itemValue, index) => ( + + ))} + + ); +} diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo/index.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo/index.js new file mode 100644 index 000000000..5bf146194 --- /dev/null +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo/index.js @@ -0,0 +1 @@ +export { default as StepTwo } from './StepTwo'; diff --git a/frontend/packages/client/src/components/ProposalCreate/index.js b/frontend/packages/client/src/components/ProposalCreate/index.js index 97a706f51..e1f8aa522 100644 --- a/frontend/packages/client/src/components/ProposalCreate/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/index.js @@ -1,3 +1,3 @@ export { default as PropCreateStepOne } from './StepOne/index'; -export { default as PropCreateStepTwo } from './StepTwo'; +export { StepTwo as PropCreateStepTwo } from './StepTwo'; export { default as PropCreateStepThree } from './StepThree'; diff --git a/frontend/packages/client/src/utils.js b/frontend/packages/client/src/utils.js index 7fbe5e782..8ec619fab 100644 --- a/frontend/packages/client/src/utils.js +++ b/frontend/packages/client/src/utils.js @@ -238,3 +238,13 @@ export const isStartTimeValid = (startTime, startDate) => { return HAS_DELAY_ON_START_TIME ? dif > 1 : true; }; + +export const formatTime = (date) => { + let hours = date.getHours(); + let minutes = date.getMinutes(); + const ampm = hours >= 12 ? 'pm' : 'am'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + minutes = minutes < 10 ? '0' + minutes : minutes; + return hours + ':' + minutes + ' ' + ampm; +}; From 2d31f4671eec7bface88204d2473d3fee1eed40c Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Tue, 30 Aug 2022 19:32:38 -0300 Subject: [PATCH 23/36] remove outpu --- .../components/ProposalCreate/StepOne/ImageChoiceUploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index 7a1352d1b..1ed11829d 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -61,7 +61,7 @@ export default function ImageChoiceUploader({ } = {}) { const [errorMessage, setErrorMessage] = useState(null); // existing image and component receives props - console.log('imageParam', imageParam); + const { imageUrl, text } = imageParam; const [image, setImage] = useState({ From 9652104c1d7ec83092eb81d1b418b9ae176f804c Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 10:37:27 -0300 Subject: [PATCH 24/36] updates --- .../client/src/components/common/CustomDatePicker.js | 1 - frontend/packages/client/src/pages/ProposalCreate.js | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/packages/client/src/components/common/CustomDatePicker.js b/frontend/packages/client/src/components/common/CustomDatePicker.js index a53308f9a..3f64e6d85 100644 --- a/frontend/packages/client/src/components/common/CustomDatePicker.js +++ b/frontend/packages/client/src/components/common/CustomDatePicker.js @@ -18,7 +18,6 @@ export default function CustomDatePicker({ name={fieldName} render={({ field }) => ( Date: Wed, 31 Aug 2022 11:09:12 -0300 Subject: [PATCH 25/36] update field name to match backend field --- .../client/src/components/ProposalCreate/FormConfig.js | 4 ++-- .../client/src/components/ProposalCreate/StepOne/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index ec5c2454b..e2b43d1f6 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -1,10 +1,10 @@ import yup from 'helpers/validation'; -const formFieldsStepOne = ['title', 'strategy', 'body', 'choices', 'tabOption']; +const formFieldsStepOne = ['name', 'strategy', 'body', 'choices', 'tabOption']; const formFieldsStepTwo = ['startDate', 'endDate', 'startTime', 'endTime']; const StepOneSchema = yup.object().shape({ - title: yup + name: yup .string() .trim() .required('Please enter a proposal title') diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index dcf342fe1..82c6cc1fa 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -85,8 +85,8 @@ const StepOne = ({
From 8894c347aa6787a09f25f874265222bd9e594b79 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:09:24 -0300 Subject: [PATCH 26/36] fix strategy payload --- frontend/packages/client/src/pages/ProposalCreate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/client/src/pages/ProposalCreate.js b/frontend/packages/client/src/pages/ProposalCreate.js index dc79e9d8a..6307e8791 100644 --- a/frontend/packages/client/src/pages/ProposalCreate.js +++ b/frontend/packages/client/src/pages/ProposalCreate.js @@ -107,7 +107,7 @@ export default function ProposalCreatePage() { creatorAddr, endTime, startTime, - strategy: strategy?.value, + strategy: strategy, ...(minBalance !== '' ? { minBalance: parseFloat(minBalance) } : undefined), From 8fc13661177eaa6df0b6a6fa2a9d1b5d813da113 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:11:05 -0300 Subject: [PATCH 27/36] remove output --- .../client/src/components/ProposalCreate/StepOne/index.js | 2 -- .../client/src/components/ProposalCreate/StepTwo/StepTwo.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 82c6cc1fa..299caf496 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -69,8 +69,6 @@ const StepOne = ({ setStepValid((isDirty || isValid) && !isSubmitting); }, [isDirty, isValid, isSubmitting, setStepValid]); - console.log('all errors ', errors); - return (
diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js index be4558193..e4740e675 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js @@ -82,8 +82,6 @@ const StepTwo = ({ const { errors, isValid, isDirty, isSubmitting } = formState; - console.log('errors ', errors); - const onSubmit = (data) => { onDataChange(data); moveToNextStep(); From e222574ab2eb577a4fa43ec4af08e111a53f67df Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:20:18 -0300 Subject: [PATCH 28/36] update component --- .../src/components/common/CustomDatePicker.js | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/frontend/packages/client/src/components/common/CustomDatePicker.js b/frontend/packages/client/src/components/common/CustomDatePicker.js index 3f64e6d85..486e4a646 100644 --- a/frontend/packages/client/src/components/common/CustomDatePicker.js +++ b/frontend/packages/client/src/components/common/CustomDatePicker.js @@ -2,6 +2,7 @@ import React from 'react'; import DatePicker from 'react-datepicker'; import { Controller } from 'react-hook-form'; import FadeIn from 'components/FadeIn'; +import { Calendar } from 'components/Svg'; export default function CustomDatePicker({ control, @@ -11,23 +12,50 @@ export default function CustomDatePicker({ maxDate, disabled = false, placeholderText, + errorMessage, } = {}) { return ( - ( - !notMobile && e.target.blur()} - onChange={(date) => field.onChange(date)} - className="border-light rounded-sm column is-full is-full-mobile p-3" - disabled={disabled} +
+
+ ( + !notMobile && e.target.blur()} + onChange={(date) => field.onChange(date)} + className="border-light rounded-sm column is-full is-full-mobile p-3" + disabled={disabled} + /> + )} /> +
+ +
+
+ {errorMessage && ( +
+ +
+

{errorMessage}

+
+
+
)} - /> +
); } From 8474a9ca7c487d01d264c464ad65db2220ec0fa5 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:20:39 -0300 Subject: [PATCH 29/36] update component --- .../ProposalCreate/StepTwo/StepTwo.js | 78 +++++-------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js index e4740e675..aae1bde1d 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { FadeIn } from 'components'; -import { Calendar, CaretDown } from 'components/Svg'; +import { CaretDown } from 'components/Svg'; import CustomDatePicker from 'components/common/CustomDatePicker'; import Form from 'components/common/Form'; import { useMediaQuery } from 'hooks'; @@ -122,29 +122,14 @@ const StepTwo = ({ Start date and time *
-
- -
- -
-
+
*
-
-
- -
- -
-
- {errors?.endTime?.message && ( -
- -
-

- {errors?.endTime?.message} -

-
-
-
- )} -
+
Date: Wed, 31 Aug 2022 11:20:48 -0300 Subject: [PATCH 30/36] remove import --- frontend/packages/client/src/components/StepByStep/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/packages/client/src/components/StepByStep/index.js b/frontend/packages/client/src/components/StepByStep/index.js index 5da62c0df..c1a3e757e 100644 --- a/frontend/packages/client/src/components/StepByStep/index.js +++ b/frontend/packages/client/src/components/StepByStep/index.js @@ -1,6 +1,5 @@ import React, { useCallback, useState } from 'react'; import { Prompt } from 'react-router-dom'; -import { isValid } from 'date-fns'; import Loader from '../Loader'; import { ArrowLeft, CheckMark } from '../Svg'; import NextButton from './NexStepButton'; From ff06745ad98b9e53475ad0a3e1a5fd292d44df35 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:52:24 -0300 Subject: [PATCH 31/36] add clear error --- .../StepOne/ChoiceOptionCreator.js | 2 ++ .../ProposalCreate/StepOne/TextBasedChoices.js | 16 +++++++++++++++- .../components/ProposalCreate/StepOne/index.js | 12 +++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js index 4fa8d29d9..c9cc39b41 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ChoiceOptionCreator.js @@ -9,6 +9,7 @@ export default function ChoiceOptionCreator({ register, fieldName, control, + clearErrors, } = {}) { const tabOption = useWatch({ control, name: 'tabOption' }); @@ -51,6 +52,7 @@ export default function ChoiceOptionCreator({ register={register} fieldName={fieldName} control={control} + clearErrors={clearErrors} /> )} {tabOption === 'visual' && ( diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js index fba024258..3ac67662e 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/TextBasedChoices.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useFieldArray } from 'react-hook-form'; import AddButton from 'components/AddButton'; import FadeIn from 'components/FadeIn'; @@ -9,6 +9,7 @@ const TextBasedChoices = ({ register, error, control, + clearErrors, } = {}) => { const { fields: choices, @@ -28,6 +29,19 @@ const TextBasedChoices = ({ }); }; + // when choices.length === 2 error for min(2) on schema is not removed: this causes the error to be listed + // without cleaning the error message user is able to submit (which is expected, and the error is removed) + // This useEffect handles cleaning the error message + useEffect(() => { + if ( + choices.length === 2 && + choices[1].value === '' && + error?.message === 'Please add a choice, minimun amout is two' + ) { + clearErrors('choices'); + } + }, [choices, clearErrors, error]); + return ( <> {choices?.map((choice, index) => { diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js index 299caf496..6fce72a90 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/index.js @@ -44,11 +44,12 @@ const StepOne = ({ pick(stepData || {}, stepOne.formFields) ); - const { register, handleSubmit, formState, control, setValue } = useForm({ - reValidateMode: 'onChange', - defaultValues: fieldsObj, - resolver: yupResolver(stepOne.Schema), - }); + const { register, handleSubmit, formState, control, setValue, clearErrors } = + useForm({ + reValidateMode: 'onChange', + defaultValues: fieldsObj, + resolver: yupResolver(stepOne.Schema), + }); const onSubmit = (data) => { let choices; @@ -154,6 +155,7 @@ const StepOne = ({ fieldName="choices" register={register} control={control} + clearErrors={clearErrors} />
From 40dbdcce265bf8e9757eb5786b9540b8bddc4f66 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 11:57:19 -0300 Subject: [PATCH 32/36] update error message --- .../client/src/components/ProposalCreate/FormConfig.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index e2b43d1f6..02a34c265 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -38,11 +38,17 @@ const StepOneSchema = yup.object().shape({ maxWeight: yup .string() .trim() - .matches(/\s+$|^$|(^[0-9]+$)/, 'Proposal max weight must be a number'), + .matches( + /\s+$|^$|(^[0-9]+$)/, + 'Proposal maximun weight must be a valid number' + ), minBalance: yup .string() .trim() - .matches(/\s+$|^$|(^[0-9]+$)/, 'Proposal min balance must be a number'), + .matches( + /\s+$|^$|(^[0-9]+$)/, + 'Proposal minimun balance must be a valid number' + ), }); const StepTwoSchema = yup.object().shape({ From ce1ecfe2eded8dd2f8c21a2f1c4b481141e92760 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 12:00:09 -0300 Subject: [PATCH 33/36] add Error message --- .../StepOne/ImageChoiceUploader.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index 1ed11829d..e77ece2b9 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -178,14 +178,26 @@ export default function ImageChoiceUploader({ useFsAccessApi: false, }); + console.log(errorParam); return (
{!image.imageUrl && ( - + <> + + {errorParam?.choiceImgUrl?.message && ( + +
+

+ {errorParam.choiceImgUrl.message} +

+
+
+ )} + )} {(image?.uploadStatus === IMAGE_STATUS.notStarted || image?.uploadStatus === IMAGE_STATUS.uploading) && ( From 141b62b6cb6963e1b695d323f02f391d667f9e17 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 12:01:01 -0300 Subject: [PATCH 34/36] remove error --- .../src/components/ProposalCreate/StepOne/ImageChoiceUploader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js index e77ece2b9..8c66c0a3c 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepOne/ImageChoiceUploader.js @@ -178,7 +178,6 @@ export default function ImageChoiceUploader({ useFsAccessApi: false, }); - console.log(errorParam); return (
{!image.imageUrl && ( From 16e09f620ee252d27c4b93b829ea2633cf090ced Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 12:08:30 -0300 Subject: [PATCH 35/36] clear error to fix dropdown styles --- .../ProposalCreate/StepTwo/StepTwo.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js index aae1bde1d..6e71c6764 100644 --- a/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js +++ b/frontend/packages/client/src/components/ProposalCreate/StepTwo/StepTwo.js @@ -51,24 +51,13 @@ const StepTwo = ({ const timeZone = detectTimeZone(); - const onSetStartTimeOpen = (e) => { - e.preventDefault(); - e.stopPropagation(); - setStartTimeOpen(true); - }; - const onSetEndTimeOpen = (e) => { - e.preventDefault(); - e.stopPropagation(); - setEndTimeOpen(true); - }; - const fieldsObj = Object.assign( {}, stepTwo.initialValues, pick(stepData || {}, stepTwo.formFields) ); - const { handleSubmit, formState, control, setValue } = useForm({ + const { handleSubmit, formState, control, setValue, clearErrors } = useForm({ defaultValues: fieldsObj, resolver: yupResolver(stepTwo.Schema), }); @@ -87,6 +76,20 @@ const StepTwo = ({ moveToNextStep(); }; + const onSetStartTimeOpen = (e) => { + e.preventDefault(); + e.stopPropagation(); + clearErrors('startTime'); + setStartTimeOpen(true); + }; + + const onSetEndTimeOpen = (e) => { + e.preventDefault(); + e.stopPropagation(); + clearErrors('endTime'); + setEndTimeOpen(true); + }; + const startDate = useWatch({ control, name: 'startDate' }); const startTime = useWatch({ control, name: 'startTime' }); const endDate = useWatch({ control, name: 'endDate' }); From e60b52632c2c3cacdc30f1b76822769425b1b079 Mon Sep 17 00:00:00 2001 From: German Urrustarazu Date: Wed, 31 Aug 2022 15:38:42 -0300 Subject: [PATCH 36/36] set focus on address --- .../src/components/CommunityCreate/StepThree.js | 15 ++++++++++++--- .../packages/client/src/components/common/Form.js | 6 +++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/packages/client/src/components/CommunityCreate/StepThree.js b/frontend/packages/client/src/components/CommunityCreate/StepThree.js index bb5a026a4..6923c1010 100644 --- a/frontend/packages/client/src/components/CommunityCreate/StepThree.js +++ b/frontend/packages/client/src/components/CommunityCreate/StepThree.js @@ -1,5 +1,5 @@ -import React from 'react'; -import { useForm } from 'react-hook-form'; +import React, { useEffect } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; import { useWebContext } from 'contexts/Web3'; import { ActionButton } from 'components'; import { ThresholdForm } from 'components/Community/ProposalThresholdEditor'; @@ -24,7 +24,7 @@ export default function StepThree({ const { isValidFlowAddress } = useWebContext(); - const { control, register, handleSubmit, formState } = useForm({ + const { control, register, handleSubmit, formState, setFocus } = useForm({ resolver: yupResolver(Schema(isValidFlowAddress)), defaultValues: { proposalThreshold, @@ -42,6 +42,15 @@ export default function StepThree({ const { isDirty, isSubmitting, errors, isValid } = formState; + const dropdownValue = useWatch({ control, name: 'contractType' }); + const contractAddressValue = useWatch({ control, name: 'contractAddress' }); + + useEffect(() => { + if (dropdownValue && contractAddressValue === '') { + setFocus('contractAddress'); + } + }, [dropdownValue, contractAddressValue, setFocus]); + return ( { + // TODO: make enter to jump to next input field on form + const checkKeyDown = (e) => { + if (e.code === 'Enter') e.preventDefault(); + }; return removeInnerForm ? ( <>{children} ) : ( - + checkKeyDown(e)}> {children} );