Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix proposal form when requiring workflow steps #5146

Merged
merged 7 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charmClient/hooks/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function useCreateProposalRewards(proposalId: MaybeString) {
return usePOST<undefined, ProposalWithUsersAndRubric>(`/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`);
}

Expand Down
45 changes: 18 additions & 27 deletions components/[pageId]/DocumentPage/DocumentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
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';
Expand Down Expand Up @@ -62,7 +63,10 @@
() => import('components/[pageId]/DocumentPage/components/RewardProperties').then((r) => r.RewardProperties),
{ ssr: false }
);
export type ProposalProps = ReturnType<typeof useProposal> & ReturnType<typeof useProposalFormAnswers>;
export type ProposalProps = ReturnType<typeof useProposal> & {
proposalAnswersProps: ReturnType<typeof useProposalFormAnswers>;
proposalFormFieldsProps: ReturnType<typeof useProposalFormFieldsEditor>;
};

export type DocumentPageProps = {
page: PageWithContent;
Expand Down Expand Up @@ -93,15 +97,8 @@
onChangeWorkflow,
onChangeRewardSettings,
onChangeSelectedCredentialTemplates,
refreshProposalFormAnswers,
projectForm,
control,
formFields,
getFieldState,
onSave,
applyProject,
applyProjectMembers,
isLoadingAnswers
proposalAnswersProps,
proposalFormFieldsProps
}: DocumentPageProps) {
const { user } = useUser();
const { router } = useCharmRouter();
Expand Down Expand Up @@ -201,7 +198,7 @@
}
}
},
[router.query, showCard]

Check warning on line 201 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useCallback has missing dependencies: 'insideModal', 'navigateToSpacePath', and 'updateURLQuery'. Either include them or remove the dependency array

Check warning on line 201 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useCallback has missing dependencies: 'insideModal', 'navigateToSpacePath', and 'updateURLQuery'. Either include them or remove the dependency array
);

function onConnectionEvent(event: ConnectionEvent) {
Expand All @@ -223,7 +220,7 @@
dispatch(blockLoad({ blockId: page.parentId as string }));
}
}
}, [page.id]);

Check warning on line 223 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has missing dependencies: 'card', 'dispatch', 'page.parentId', and 'page?.type'. Either include them or remove the dependency array

Check warning on line 223 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has missing dependencies: 'card', 'dispatch', 'page.parentId', and 'page?.type'. Either include them or remove the dependency array

// reset error and sidebar state whenever page id changes
useEffect(() => {
Expand All @@ -249,7 +246,7 @@
printRef
});
}
}, [printRef, _printRef]);

Check warning on line 249 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has a missing dependency: 'setPageProps'. Either include it or remove the dependency array

Check warning on line 249 in components/[pageId]/DocumentPage/DocumentPage.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has a missing dependency: 'setPageProps'. Either include it or remove the dependency array

const containerWidthRef = useRef<HTMLDivElement>(null);
const { width: containerWidth = 0 } = useResizeObserver({ ref: containerWidthRef });
Expand All @@ -262,7 +259,7 @@
const proposalAuthors = proposal ? [proposal.createdBy, ...proposal.authors.map((author) => author.userId)] : [];
return (
<Box id='file-drop-container' display='flex' flexDirection='column' height='100%'>
<FormProvider {...projectForm}>
<FormProvider {...proposalAnswersProps.projectForm}>
<Box
ref={printRef}
className={`document-print-container ${fontClassName} drag-area-container`}
Expand Down Expand Up @@ -394,7 +391,7 @@
onChangeEvaluation={onChangeEvaluation}
onChangeTemplate={onChangeTemplate}
refreshProposal={refreshProposal}
refreshProposalFormAnswers={refreshProposalFormAnswers}
refreshProposalFormAnswers={proposalAnswersProps.refreshProposalFormAnswers}
onChangeWorkflow={onChangeWorkflow}
onChangeSelectedCredentialTemplates={onChangeSelectedCredentialTemplates}
/>
Expand Down Expand Up @@ -458,15 +455,13 @@
</CardPropertiesWrapper>
{proposal && proposal.formId ? (
page.type === 'proposal_template' ? (
<FormFieldsEditor
readOnly={(!isAdmin && (!user || !proposalAuthors.includes(user.id))) || !!proposal?.archived}
proposalId={proposal.id}
<ControlledFormFieldsEditor
{...proposalFormFieldsProps}
evaluations={proposal.evaluations}
expandFieldsByDefault={proposal.status === 'draft'}
formFields={proposal.form?.formFields ?? []}
readOnly={(!isAdmin && (!user || !proposalAuthors.includes(user.id))) || !!proposal?.archived}
/>
) : (
<LoadingComponent isLoading={isLoadingAnswers}>
<LoadingComponent isLoading={proposalAnswersProps.isLoadingAnswers}>
<FormFieldAnswers
milestoneProps={{
containerWidth,
Expand Down Expand Up @@ -494,7 +489,7 @@
})
});
}
refreshProposalFormAnswers();
proposalAnswersProps.refreshProposalFormAnswers();
},
onDelete: (draftId: string) => {
onChangeRewardSettings({
Expand All @@ -504,21 +499,16 @@
});
}
}}
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}
// This is required to reinstate the form field state after the proposal is published, necessary to show the correct project id
key={proposal?.status === 'draft' ? 'draft' : 'published'}
projectId={proposal.projectId}
proposalId={proposal.id}
applyProject={applyProject}
applyProjectMembers={applyProjectMembers}
/>
</LoadingComponent>
)
Expand Down Expand Up @@ -571,7 +561,8 @@
<ProposalStickyFooter
page={page}
proposal={proposal}
formAnswersControl={control}
formAnswersControl={proposalAnswersProps.control}
formFields={proposalFormFieldsProps.formFields}
isStructuredProposal={isStructuredProposal}
/>
)}
Expand Down
10 changes: 9 additions & 1 deletion components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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';
Expand Down Expand Up @@ -52,6 +53,12 @@
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
Expand All @@ -73,7 +80,7 @@
if (threadsPageId !== page.id) {
closeSidebar();
}
}, [page.id, threadsPageId]);

Check warning on line 83 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has a missing dependency: 'closeSidebar'. Either include it or remove the dependency array

Check warning on line 83 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has a missing dependency: 'closeSidebar'. Either include it or remove the dependency array

// show page sidebar by default if there are comments or votes
useEffect(() => {
Expand All @@ -99,7 +106,7 @@
return setActiveView('comments');
}
}
}, [isLoadingThreads, page.id, threadsPageId]);

Check warning on line 109 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has missing dependencies: 'isMdScreen', 'setActiveView', 'sidebarView', and 'threads'. Either include them or remove the dependency array

Check warning on line 109 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has missing dependencies: 'isMdScreen', 'setActiveView', 'sidebarView', and 'threads'. Either include them or remove the dependency array

// having `internalSidebarView` allows us to have the sidebar open by default, because usePageSidebar() does not allow us to do this currently
const [defaultSidebarView, setDefaultView] = useState<PageSidebarView | null>(
Expand All @@ -114,15 +121,16 @@
// clear sidebar so we can show left sidebar
setActiveView(null);
};
}, []);

Check warning on line 124 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has missing dependencies: 'defaultSidebarView' and 'setActiveView'. Either include them or remove the dependency array

Check warning on line 124 in components/[pageId]/DocumentPage/DocumentPageWithSidebars.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has missing dependencies: 'defaultSidebarView' and 'setActiveView'. Either include them or remove the dependency array

return (
<DocumentColumnLayout data-test='document-page'>
<DocumentColumn>
<DocumentPage
{...props}
{...proposalAnswersProps}
{...proposalProps}
proposalAnswersProps={proposalAnswersProps}
proposalFormFieldsProps={proposalFormFieldsProps}
setEditorState={setEditorState}
setSidebarView={setActiveView}
sidebarView={internalSidebarView}
Expand Down
10 changes: 9 additions & 1 deletion components/common/PageDialog/PageDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { Button } from 'components/common/Button';
import Dialog from 'components/common/DatabaseEditor/components/dialog';
import { useProposalFormAnswers } from 'components/proposals/hooks/useProposalFormAnswers';
import { useProposalFormFieldsEditor } from 'components/proposals/hooks/useProposalFormFieldsEditor';
import { useCharmEditor } from 'hooks/useCharmEditor';
import { useCurrentPage } from 'hooks/useCurrentPage';
import { useCurrentSpace } from 'hooks/useCurrentSpace';
Expand Down Expand Up @@ -51,6 +52,12 @@
const proposalAnswersProps = useProposalFormAnswers({
proposal: proposalProps.proposal
});
const proposalFormFieldsProps = useProposalFormFieldsEditor({
proposalId: page?.proposalId,
formFields: proposalProps.proposal?.form?.formFields || undefined,
readOnly: props.readOnly ?? false,
expandFieldsByDefault: proposalProps.proposal?.status === 'draft'
});

const pagePermissions = page?.permissionFlags || new AvailablePagePermissions().full;
const fullPageUrl = page?.path ? `/${page?.path}` : null;
Expand All @@ -72,7 +79,7 @@
if (contentType) {
popupState.open();
}
}, [!!contentType]);

Check warning on line 82 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has missing dependencies: 'contentType' and 'popupState'. Either include them or remove the dependency array

Check warning on line 82 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked

Check warning on line 82 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has missing dependencies: 'contentType' and 'popupState'. Either include them or remove the dependency array

Check warning on line 82 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked

useEffect(() => {
if (page?.id && space) {
Expand All @@ -91,7 +98,7 @@
});
}
}
}, [page?.id, space?.customDomain]);

Check warning on line 101 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has missing dependencies: 'page.path', 'page.spaceId', 'page.type', and 'space'. Either include them or remove the dependency array

Check warning on line 101 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has missing dependencies: 'page.path', 'page.spaceId', 'page.type', and 'space'. Either include them or remove the dependency array

function close() {
popupState.close();
Expand All @@ -102,7 +109,7 @@
if (contentType === null && popupState.isOpen) {
close();
}
}, [contentType, popupState.isOpen]);

Check warning on line 112 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Test apps

React Hook useEffect has a missing dependency: 'close'. Either include it or remove the dependency array

Check warning on line 112 in components/common/PageDialog/PageDialog.tsx

View workflow job for this annotation

GitHub Actions / Validate code

React Hook useEffect has a missing dependency: 'close'. Either include it or remove the dependency array

useEffect(() => {
if (page?.id) {
Expand Down Expand Up @@ -189,7 +196,8 @@
savePage={savePage}
readOnly={readOnlyPage}
{...proposalProps}
{...proposalAnswersProps}
proposalAnswersProps={proposalAnswersProps}
proposalFormFieldsProps={proposalFormFieldsProps}
/>
)}
</Dialog>
Expand Down
20 changes: 13 additions & 7 deletions components/common/form/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function ExpandedFormField({
const { space } = useCurrentSpace();

const formFieldType = formField.type;

const filteredFormFieldTypes = useMemo(() => {
if (!formFieldTypeFrequencyCount) {
return formFieldTypes.filter((_formFieldType) => {
Expand Down Expand Up @@ -284,13 +285,14 @@ function ExpandedFormField({

{evaluations.length > 0 && (
<FieldWrapper label='Workflow step'>
<Select<number | null>
<Select<string | null>
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
});
}}
Expand All @@ -299,7 +301,7 @@ function ExpandedFormField({
}}
variant='outlined'
IconComponent={
formField.dependsOnStepIndex
formField.dependsOnStepIndex !== null
? // eslint-disable-next-line react/no-unstable-nested-components
() => (
<IconButton
Expand All @@ -326,12 +328,16 @@ function ExpandedFormField({
</Typography>
);
}
return evaluations[value]?.title;
return evaluations[parseInt(value)]?.title;
}}
>
{evaluations.map((evaluation, index) => {
return (
<MenuItem data-test={`form-field-dependency-option-${evaluation.id}`} key={evaluation.id} value={index}>
<MenuItem
data-test={`form-field-dependency-option-${evaluation.id}`}
key={evaluation.id}
value={`${index}`}
>
<Stack flexDirection='row' gap={1} alignItems='center'>
{index + 1}. {evaluation.title}
</Stack>
Expand Down
70 changes: 9 additions & 61 deletions components/common/form/FormFieldsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>(
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 (
<ControlledFormFieldsEditor
collapsedFieldIds={collapsedFieldIds}
formFields={formFields}
setFormFields={updateFormFields}
evaluations={evaluations}
toggleCollapse={(fieldId) => {
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,
Expand All @@ -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<number | undefined>(undefined);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -18,11 +18,13 @@ import type { ProposalWithUsersAndRubric } from 'lib/proposals/interfaces';
export function ProposalStickyFooter({
proposal,
formAnswersControl,
formFields: allFormFields,
page,
isStructuredProposal
}: {
proposal: ProposalWithUsersAndRubric;
formAnswersControl: Control<Record<string, FormFieldValue>, any>;
formFields?: FormFieldInput[];
page: { title: string; hasContent?: boolean; sourceTemplateId: string | null; type: PageType };
isStructuredProposal: boolean;
}) {
Expand All @@ -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;

Expand Down
Loading
Loading