diff --git a/charmClient/hooks/proposals.ts b/charmClient/hooks/proposals.ts index 21cbf4099c..53c18f0ce9 100644 --- a/charmClient/hooks/proposals.ts +++ b/charmClient/hooks/proposals.ts @@ -124,7 +124,7 @@ export function useCreateProposalRewards(proposalId: MaybeString) { return usePOST(`/api/proposals/${proposalId}/rewards`); } -export function useUpdateProposalFormFields({ proposalId }: { proposalId: string }) { +export function useUpdateProposalFormFields({ proposalId }: { proposalId: MaybeString }) { return usePUT<{ formFields: FormFieldInput[] }, FormFieldInput[]>(`/api/proposals/${proposalId}/form`); } diff --git a/components/[pageId]/DocumentPage/DocumentPage.tsx b/components/[pageId]/DocumentPage/DocumentPage.tsx index bf1959523e..92a33c89cb 100644 --- a/components/[pageId]/DocumentPage/DocumentPage.tsx +++ b/components/[pageId]/DocumentPage/DocumentPage.tsx @@ -20,9 +20,10 @@ import { blockLoad, databaseViewsLoad } from 'components/common/DatabaseEditor/s import { useAppDispatch, useAppSelector } from 'components/common/DatabaseEditor/store/hooks'; import { makeSelectSortedViews } from 'components/common/DatabaseEditor/store/views'; import { FormFieldAnswers } from 'components/common/form/FormFieldAnswers'; -import { FormFieldsEditor } from 'components/common/form/FormFieldsEditor'; +import { ControlledFormFieldsEditor } from 'components/common/form/FormFieldsEditor'; import LoadingComponent from 'components/common/LoadingComponent'; import type { useProposalFormAnswers } from 'components/proposals/hooks/useProposalFormAnswers'; +import type { useProposalFormFieldsEditor } from 'components/proposals/hooks/useProposalFormFieldsEditor'; import { ProposalEvaluations } from 'components/proposals/ProposalPage/components/ProposalEvaluations/ProposalEvaluations'; import { ProposalStickyFooter } from 'components/proposals/ProposalPage/components/ProposalStickyFooter/ProposalStickyFooter'; import { RewardEvaluations } from 'components/rewards/components/RewardEvaluations/RewardEvaluations'; @@ -62,7 +63,10 @@ const RewardProperties = dynamic( () => import('components/[pageId]/DocumentPage/components/RewardProperties').then((r) => r.RewardProperties), { ssr: false } ); -export type ProposalProps = ReturnType & ReturnType; +export type ProposalProps = ReturnType & { + proposalAnswersProps: ReturnType; + proposalFormFieldsProps: ReturnType; +}; export type DocumentPageProps = { page: PageWithContent; @@ -93,15 +97,8 @@ function DocumentPageComponent({ onChangeWorkflow, onChangeRewardSettings, onChangeSelectedCredentialTemplates, - refreshProposalFormAnswers, - projectForm, - control, - formFields, - getFieldState, - onSave, - applyProject, - applyProjectMembers, - isLoadingAnswers + proposalAnswersProps, + proposalFormFieldsProps }: DocumentPageProps) { const { user } = useUser(); const { router } = useCharmRouter(); @@ -262,7 +259,7 @@ function DocumentPageComponent({ const proposalAuthors = proposal ? [proposal.createdBy, ...proposal.authors.map((author) => author.userId)] : []; return ( - + @@ -458,15 +455,13 @@ function DocumentPageComponent({ {proposal && proposal.formId ? ( page.type === 'proposal_template' ? ( - ) : ( - + { onChangeRewardSettings({ @@ -504,12 +499,9 @@ function DocumentPageComponent({ }); } }} - control={control} + {...proposalAnswersProps} disabled={!proposal.permissions.edit} enableComments={proposal.permissions.comment} - formFields={formFields} - getFieldState={getFieldState} - onSave={onSave} pageId={page.id} isAuthor={proposalAuthors.includes(user?.id || '')} threads={threads} @@ -517,8 +509,6 @@ function DocumentPageComponent({ key={proposal?.status === 'draft' ? 'draft' : 'published'} projectId={proposal.projectId} proposalId={proposal.id} - applyProject={applyProject} - applyProjectMembers={applyProjectMembers} /> ) @@ -571,7 +561,8 @@ function DocumentPageComponent({ )} diff --git a/components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx b/components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx index 2154794327..f16ba67a94 100644 --- a/components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx +++ b/components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx @@ -3,6 +3,7 @@ import { memo, useEffect, useState } from 'react'; import type { PageSidebarView } from 'components/[pageId]/DocumentPage/hooks/usePageSidebar'; import { useProposalFormAnswers } from 'components/proposals/hooks/useProposalFormAnswers'; +import { useProposalFormFieldsEditor } from 'components/proposals/hooks/useProposalFormFieldsEditor'; import { useCharmEditor } from 'hooks/useCharmEditor'; import { useCharmRouter } from 'hooks/useCharmRouter'; import { useMdScreen } from 'hooks/useMediaScreens'; @@ -52,6 +53,12 @@ function DocumentPageWithSidebarsComponent(props: DocumentPageWithSidebarsProps) const proposalAnswersProps = useProposalFormAnswers({ proposal }); + const proposalFormFieldsProps = useProposalFormFieldsEditor({ + proposalId, + formFields: proposal?.form?.formFields || undefined, + readOnly: props.readOnly ?? false, + expandFieldsByDefault: proposal?.status === 'draft' + }); const { onChangeRewardWorkflow, reward, updateReward, refreshReward } = useReward({ rewardId @@ -121,8 +128,9 @@ function DocumentPageWithSidebarsComponent(props: DocumentPageWithSidebarsProps) )} diff --git a/components/common/form/FormField.tsx b/components/common/form/FormField.tsx index fb02fd895b..01ab360262 100644 --- a/components/common/form/FormField.tsx +++ b/components/common/form/FormField.tsx @@ -104,6 +104,7 @@ function ExpandedFormField({ const { space } = useCurrentSpace(); const formFieldType = formField.type; + const filteredFormFieldTypes = useMemo(() => { if (!formFieldTypeFrequencyCount) { return formFieldTypes.filter((_formFieldType) => { @@ -284,13 +285,14 @@ function ExpandedFormField({ {evaluations.length > 0 && ( - + data-test='form-field-dependency-select' displayEmpty - value={formField.dependsOnStepIndex} - onChange={(e, value) => { + // use a string instead of number, MUI shows the number 0 as selected when the value is null for some reason + value={typeof formField.dependsOnStepIndex === 'number' ? `${formField.dependsOnStepIndex}` : null} + onChange={(e) => { updateFormField({ - dependsOnStepIndex: e.target.value as number, + dependsOnStepIndex: parseInt(e.target.value!), id: formField.id }); }} @@ -299,7 +301,7 @@ function ExpandedFormField({ }} variant='outlined' IconComponent={ - formField.dependsOnStepIndex + formField.dependsOnStepIndex !== null ? // eslint-disable-next-line react/no-unstable-nested-components () => ( ); } - return evaluations[value]?.title; + return evaluations[parseInt(value)]?.title; }} > {evaluations.map((evaluation, index) => { return ( - + {index + 1}. {evaluation.title} diff --git a/components/common/form/FormFieldsEditor.tsx b/components/common/form/FormFieldsEditor.tsx index 7480edf2ec..fb81afc062 100644 --- a/components/common/form/FormFieldsEditor.tsx +++ b/components/common/form/FormFieldsEditor.tsx @@ -2,68 +2,23 @@ import type { FormFieldType } from '@charmverse/core/prisma-client'; import AddIcon from '@mui/icons-material/Add'; import { Stack } from '@mui/material'; import type { SelectOptionType, FormFieldInput } from '@root/lib/proposals/forms/interfaces'; -import debounce from 'lodash/debounce'; -import { useRef, useEffect, useState, useMemo } from 'react'; +import { useRef, useEffect } from 'react'; import { v4 } from 'uuid'; -import { useUpdateProposalFormFields } from 'charmClient/hooks/proposals'; import { emptyDocument } from 'lib/prosemirror/constants'; import { Button } from '../Button'; import { FormField } from './FormField'; -export function FormFieldsEditor({ - proposalId, - evaluations, - expandFieldsByDefault, - formFields: initialFormFields, - readOnly -}: { - proposalId: string; - evaluations: { id: string; title: string }[]; - expandFieldsByDefault?: boolean; +export type ControlledFormFieldsEditorProps = { formFields: FormFieldInput[]; - readOnly?: boolean; -}) { - const [formFields, setFormFields] = useState([...initialFormFields]); - const [collapsedFieldIds, setCollapsedFieldIds] = useState( - expandFieldsByDefault ? [] : formFields.map((field) => field.id) - ); - const { trigger } = useUpdateProposalFormFields({ proposalId }); - const debouncedUpdate = useMemo(() => { - return debounce(trigger, 200); - }, [trigger]); - - async function updateFormFields(_formFields: FormFieldInput[]) { - if (readOnly) { - return; - } - setFormFields(_formFields); - try { - await debouncedUpdate({ formFields: _formFields }); - } catch (error) { - // dont show error modal, the UI should show red borders now instead - } - } - - return ( - { - if (collapsedFieldIds.includes(fieldId)) { - setCollapsedFieldIds(collapsedFieldIds.filter((id) => id !== fieldId)); - } else { - setCollapsedFieldIds([...collapsedFieldIds, fieldId]); - } - }} - readOnly={readOnly} - /> - ); -} + setFormFields: (formFields: FormFieldInput[]) => void; + collapsedFieldIds: string[]; + toggleCollapse: (fieldId: string) => void; + evaluations: { id: string; title: string }[]; + readOnly: boolean; +}; export function ControlledFormFieldsEditor({ formFields, @@ -72,14 +27,7 @@ export function ControlledFormFieldsEditor({ toggleCollapse, evaluations, readOnly -}: { - formFields: FormFieldInput[]; - setFormFields: (updatedFormFields: FormFieldInput[]) => void; - collapsedFieldIds: string[]; - toggleCollapse: (fieldId: string) => void; - evaluations: { id: string; title: string }[]; - readOnly?: boolean; -}) { +}: ControlledFormFieldsEditorProps) { // Using a ref to keep the formFields state updated, since it becomes stale inside the functions const formFieldsRef = useRef(formFields); const lastInsertedIndexRef = useRef(undefined); diff --git a/components/proposals/ProposalPage/components/ProposalStickyFooter/ProposalStickyFooter.tsx b/components/proposals/ProposalPage/components/ProposalStickyFooter/ProposalStickyFooter.tsx index e2a798ac10..df33a416a1 100644 --- a/components/proposals/ProposalPage/components/ProposalStickyFooter/ProposalStickyFooter.tsx +++ b/components/proposals/ProposalPage/components/ProposalStickyFooter/ProposalStickyFooter.tsx @@ -1,6 +1,6 @@ import type { PageType } from '@charmverse/core/prisma'; import { Box } from '@mui/material'; -import type { FormFieldValue } from '@root/lib/proposals/forms/interfaces'; +import type { FormFieldValue, FormFieldInput, TypedFormField } from '@root/lib/proposals/forms/interfaces'; import type { Control } from 'react-hook-form'; import { useFormContext, useFormState, useWatch } from 'react-hook-form'; @@ -18,11 +18,13 @@ import type { ProposalWithUsersAndRubric } from 'lib/proposals/interfaces'; export function ProposalStickyFooter({ proposal, formAnswersControl, + formFields: allFormFields, page, isStructuredProposal }: { proposal: ProposalWithUsersAndRubric; formAnswersControl: Control, any>; + formFields?: FormFieldInput[]; page: { title: string; hasContent?: boolean; sourceTemplateId: string | null; type: PageType }; isStructuredProposal: boolean; }) { @@ -49,9 +51,11 @@ export function ProposalStickyFooter({ const projectProfileField = proposal?.form?.formFields?.find((field) => field.type === 'project_profile'); const formFields = page.type === 'proposal_template' - ? proposal.form?.formFields - : proposal.form?.formFields?.filter( - (field) => !field.isHiddenByDependency && (field.type === 'project_profile' || field.type === 'milestone') + ? allFormFields + : allFormFields?.filter( + (field) => + !(field as TypedFormField).isHiddenByDependency && + (field.type === 'project_profile' || field.type === 'milestone') ); const projectProfileAnswer = projectProfileField ? answerFormValues[projectProfileField.id] : null; diff --git a/components/proposals/hooks/useProposalFormFieldsEditor.ts b/components/proposals/hooks/useProposalFormFieldsEditor.ts new file mode 100644 index 0000000000..2d99311da7 --- /dev/null +++ b/components/proposals/hooks/useProposalFormFieldsEditor.ts @@ -0,0 +1,64 @@ +import type { FormFieldInput } from '@root/lib/proposals/forms/interfaces'; +import debounce from 'lodash/debounce'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useUpdateProposalFormFields } from 'charmClient/hooks/proposals'; +import type { ControlledFormFieldsEditorProps } from 'components/common/form/FormFieldsEditor'; + +export function useProposalFormFieldsEditor({ + proposalId, + expandFieldsByDefault, + formFields: initialFormFields, + readOnly +}: { + proposalId?: string | null; + expandFieldsByDefault?: boolean; + formFields?: FormFieldInput[]; + readOnly: boolean; +}): Omit { + const [formFields, setFormFields] = useState([]); + const [collapsedFieldIds, setCollapsedFieldIds] = useState([]); + const { trigger } = useUpdateProposalFormFields({ proposalId }); + const debouncedUpdate = useMemo(() => { + return debounce(trigger, 200); + }, [trigger]); + + const setFormFieldsQuietly = useCallback( + async function updateFormFields(_formFields: FormFieldInput[]) { + if (readOnly) { + return; + } + setFormFields(_formFields); + try { + await debouncedUpdate({ formFields: _formFields }); + } catch (error) { + // dont show error modal, the UI should show red borders now instead + } + }, + [debouncedUpdate, readOnly] + ); + + const toggleCollapse = useCallback( + (fieldId: string) => { + if (collapsedFieldIds.includes(fieldId)) { + setCollapsedFieldIds(collapsedFieldIds.filter((id) => id !== fieldId)); + } else { + setCollapsedFieldIds([...collapsedFieldIds, fieldId]); + } + }, + [collapsedFieldIds, setCollapsedFieldIds] + ); + + useEffect(() => { + if (initialFormFields) { + setFormFields(initialFormFields); + if (expandFieldsByDefault) { + setCollapsedFieldIds([]); + } else { + setCollapsedFieldIds(initialFormFields.map((field) => field.id)); + } + } + }, [!!initialFormFields, expandFieldsByDefault]); + + return { readOnly, setFormFields: setFormFieldsQuietly, formFields, collapsedFieldIds, toggleCollapse }; +} diff --git a/scripts/rewardBuilders.ts b/scripts/rewardBuilders.ts index 70375e4162..e21a153105 100644 --- a/scripts/rewardBuilders.ts +++ b/scripts/rewardBuilders.ts @@ -38,6 +38,7 @@ async function query() { claimedAt: new Date(), value: 100, recipientId: builderId, + season: currentSeason, activities: { create: { type: 'points', diff --git a/stories/FormFields/FormFields.stories.tsx b/stories/FormFields/FormFields.stories.tsx index c90a88944a..758370c3d0 100644 --- a/stories/FormFields/FormFields.stories.tsx +++ b/stories/FormFields/FormFields.stories.tsx @@ -49,6 +49,7 @@ export function FormFieldsEditor() { return (