diff --git a/src/modules/client/components/ClientCustomDataElementsCard.tsx b/src/modules/client/components/ClientCustomDataElementsCard.tsx index 5391e8f5b..745bc1943 100644 --- a/src/modules/client/components/ClientCustomDataElementsCard.tsx +++ b/src/modules/client/components/ClientCustomDataElementsCard.tsx @@ -4,6 +4,7 @@ import { CommonDetailGridItem, } from '@/components/elements/CommonDetailGrid'; import TitleCard from '@/components/elements/TitleCard'; +import useAuth from '@/modules/auth/hooks/useAuth'; import OccurrencePointForm from '@/modules/form/components/OccurrencePointForm'; import { useClientDetailForms } from '@/modules/form/hooks/useClientDetailForms'; import { parseOccurrencePointFormDefinition } from '@/modules/form/util/formUtil'; @@ -15,12 +16,14 @@ interface Props { const ClientCustomDataElementsCard: React.FC = ({ client }) => { const { forms, loading } = useClientDetailForms(); - + const { user } = useAuth(); const rows = useMemo( () => forms.map((form) => { - const { displayTitle, isEditable, readOnlyDefinition } = - parseOccurrencePointFormDefinition(form.definition); + // Determine whether this form has any fields that are editable. + // Pass the user because there might be fields that are only editable by some users. + const { displayTitle, isEditable, definitionForDisplay } = + parseOccurrencePointFormDefinition(form.definition, user); return { id: form.id, @@ -29,14 +32,14 @@ const ClientCustomDataElementsCard: React.FC = ({ client }) => { ), }; }), - [client, forms] + [client, forms, user] ); if (loading) return null; diff --git a/src/modules/enrollment/components/EnrollmentDetails.tsx b/src/modules/enrollment/components/EnrollmentDetails.tsx index 5724edbdf..f9756c455 100644 --- a/src/modules/enrollment/components/EnrollmentDetails.tsx +++ b/src/modules/enrollment/components/EnrollmentDetails.tsx @@ -12,6 +12,7 @@ import Loading from '@/components/elements/Loading'; import NotCollectedText from '@/components/elements/NotCollectedText'; import RouterLink from '@/components/elements/RouterLink'; +import useAuth from '@/modules/auth/hooks/useAuth'; import { parseOccurrencePointFormDefinition } from '@/modules/form/util/formUtil'; import EnrollmentStatus from '@/modules/hmis/components/EnrollmentStatus'; import HmisEnum from '@/modules/hmis/components/HmisEnum'; @@ -27,6 +28,7 @@ const EnrollmentDetails = ({ }: { enrollment: DashboardEnrollment; }) => { + const { user } = useAuth(); const rows = useMemo(() => { const content: Record = {}; // If enrollment is incomplete, show that first @@ -64,14 +66,16 @@ const EnrollmentDetails = ({ // Occurrence point values (move in date, date of engagement, etc.) enrollment.occurrencePointForms.forEach(({ definition }) => { - const { displayTitle, isEditable, readOnlyDefinition } = - parseOccurrencePointFormDefinition(definition); + // Determine whether this form has any fields that are editable. + // Pass the user because there might be fields that are only editable by some users. + const { displayTitle, isEditable, definitionForDisplay } = + parseOccurrencePointFormDefinition(definition, user); content[displayTitle] = ( @@ -136,7 +140,7 @@ const EnrollmentDetails = ({ ), value, })); - }, [enrollment]); + }, [enrollment, user]); if (!enrollment || !rows) return ; diff --git a/src/modules/form/components/DynamicFormFields.tsx b/src/modules/form/components/DynamicFormFields.tsx index 3d3bddbd6..cad06a167 100644 --- a/src/modules/form/components/DynamicFormFields.tsx +++ b/src/modules/form/components/DynamicFormFields.tsx @@ -20,6 +20,7 @@ import { import DynamicField from './DynamicField'; import DynamicGroup from './DynamicGroup'; +import SentryErrorBoundary from '@/modules/errors/components/SentryErrorBoundary'; import DynamicViewField from '@/modules/form/components/viewable/DynamicViewField'; import { DisabledDisplay, @@ -145,33 +146,34 @@ const DynamicFormFields: React.FC = ({ /> ) : ( - + + + ); if (renderFn) { return renderFn(itemComponent); diff --git a/src/modules/form/components/OccurrencePointForm.tsx b/src/modules/form/components/OccurrencePointForm.tsx index 1f657dba2..d60923a9d 100644 --- a/src/modules/form/components/OccurrencePointForm.tsx +++ b/src/modules/form/components/OccurrencePointForm.tsx @@ -29,9 +29,12 @@ import { export interface OccurrencePointFormProps { record: SubmitFormAllowedTypes; - definition: FormDefinitionFieldsFragment; submitFormInputVariables?: SubmitFormInputVariables; - readOnlyDefinition: FormDefinitionJson; + /** Definition to use for displaying the occurrence point as a DynamicView */ + definitionForDisplay: FormDefinitionJson; + /** Definition to use for editing the Occurrence Point form values (if allowed) */ + definition: FormDefinitionFieldsFragment; + /** Whether the Occurence Point form is editable. Some Occurrence Point forms are always read-only (like 'number of units assigned'), and some are only editable by certain users (via editor_user_ids). */ editable?: boolean; dialogTitle?: string; localConstants?: LocalConstants; @@ -52,7 +55,7 @@ const OccurrencePointForm: React.FC = ({ localConstants: localConstantsProp, definition, editable, - readOnlyDefinition, + definitionForDisplay, dialogTitle, pickListArgs, submitFormInputVariables, @@ -114,7 +117,7 @@ const OccurrencePointForm: React.FC = ({ ) : ( diff --git a/src/modules/form/util/formUtil.ts b/src/modules/form/util/formUtil.ts index de00ef473..ee0ad5ea6 100644 --- a/src/modules/form/util/formUtil.ts +++ b/src/modules/form/util/formUtil.ts @@ -42,6 +42,7 @@ import { TypedObject, } from '../types'; +import { HmisUser } from '@/modules/auth/api/sessions'; import { evaluateFormula } from '@/modules/form/util/expressions/formula'; import { collectExpressionReferences } from '@/modules/form/util/expressions/references'; import { @@ -1492,7 +1493,8 @@ export const getFieldOnAssessment = ( }; export const parseOccurrencePointFormDefinition = ( - definition: FormDefinitionFieldsFragment + definition: FormDefinitionFieldsFragment, + user?: HmisUser ) => { let displayTitle = definition.title; let isEditable = false; @@ -1503,21 +1505,35 @@ export const parseOccurrencePointFormDefinition = ( ); } - const readOnlyDefinition = modifyFormDefinition( + // Modify form definition into a "read only definition" by removing + const definitionForDisplay = modifyFormDefinition( definition.definition, (item) => { + // Delete the 'text' from the item IF the text matches the title of the form. + // This is a hacky way to hide redundant labels for tiny Occurrence Point forms like Move-in Date. if (definition.title && matchesTitle(item, definition.title)) { displayTitle = item.readonlyText || item.text || displayTitle; delete item.text; delete item.readonlyText; } if (isQuestionItem(item) && !item.readOnly) { - isEditable = true; + if (item.editorUserIds && user) { + isEditable = item.editorUserIds.includes(user.id); + } else { + isEditable = true; + } } } ); - return { displayTitle, isEditable, readOnlyDefinition }; + return { + // Title to display in the left-hand column of the Enrollment/Client details card + displayTitle, + // Modified Form Definiton to use to display the form values as a DynamicView in the right-hand column of the Enrollment/Client details card + definitionForDisplay, + // Whether the form is editable by the current user + isEditable, + }; }; export const getFormStepperItems = (