diff --git a/backend/src/openarchiefbeheer/destruction/api/serializers.py b/backend/src/openarchiefbeheer/destruction/api/serializers.py index fc825618..64f30259 100644 --- a/backend/src/openarchiefbeheer/destruction/api/serializers.py +++ b/backend/src/openarchiefbeheer/destruction/api/serializers.py @@ -114,12 +114,19 @@ def validate(self, attrs): .assignees.filter(role=ListRole.co_reviewer) .count() ) - if ( - current_number_co_reviewers - + len(attrs.get("add", [])) - - len(attrs.get("remove", [])) - > MAX_NUMBER_CO_REVIEWERS - ): + + # (New) number of co reviewers depends on whether a partial update has been mode or a full update is provided. + number_of_co_reviewers = ( + ( + current_number_co_reviewers + + len(attrs.get("add", [])) + - len(attrs.get("remove", [])) + ) + if self.partial + else len(attrs.get("add", [])) + ) + + if number_of_co_reviewers > MAX_NUMBER_CO_REVIEWERS: raise ValidationError( _("The maximum number of allowed co-reviewers is %(max_co_reviewers)s.") % {"max_co_reviewers": MAX_NUMBER_CO_REVIEWERS} diff --git a/frontend/src/components/DestructionListReviewer/DestructionListReviewer.tsx b/frontend/src/components/DestructionListReviewer/DestructionListReviewer.tsx index a932be80..c4cf9676 100644 --- a/frontend/src/components/DestructionListReviewer/DestructionListReviewer.tsx +++ b/frontend/src/components/DestructionListReviewer/DestructionListReviewer.tsx @@ -3,11 +3,13 @@ import { Button, FormField, P, + SerializedFormData, Solid, useAlert, useFormDialog, + validateForm, } from "@maykin-ui/admin-ui"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigation, useRevalidator } from "react-router-dom"; import { @@ -57,11 +59,41 @@ export function DestructionListReviewer({ const assignedCoReviewers = useDestructionListCoReviewers(destructionList); const user = useWhoAmI(); + const [ + assignCoReviewersFormValuesState, + setAssignCoReviewersFormValuesState, + ] = useState({}); + + useEffect(() => { + setAssignCoReviewersFormValuesState({ + ...assignCoReviewersFormValuesState, + coReviewer: assignedCoReviewers.map((r) => r.user.pk.toString()), + }); + }, [assignedCoReviewers]); + + const [assignCoReviewerModalOpenState, setAssignCoReviewerModalOpenState] = + useState(false); + + /** + * Updates `assignCoReviewersFormValuesState` with `values`. + * This allows `field` to use filtered options based on it's value. + * @param values + */ + const handleValidate = (values: SerializedFormData) => { + // Ignore first run. + if (!Object.keys(values).length) { + return; + } + setAssignCoReviewersFormValuesState(values); + return validateForm(values, fields); + }; + /** * Gets called when the change is confirmed. */ const handleSubmit = (data: DestructionListReviewerFormType) => { const { coReviewer, reviewer, comment } = data; + setAssignCoReviewerModalOpenState(false); const promises: Promise[] = []; @@ -122,6 +154,10 @@ export function DestructionListReviewer({ (assignee) => assignee.role === "main_reviewer", ); + /** + * The fields to show in the form dialog, can be either for (re)assigning a + * reviewer or for (re)assigning co-reviewers. + */ const fields = useMemo(() => { if (!user) return []; @@ -145,29 +181,58 @@ export function DestructionListReviewer({ required: true, }; + const activeCoReviewers = + (assignCoReviewersFormValuesState.coReviewer as string[]) || []; + if (canReviewDestructionList(user, destructionList)) { const coReviewerFields = new Array(5) .fill({ label: "Medebeoordelaar", name: "coReviewer", type: "string", - options: coReviewers.map((user) => ({ - label: formatUser(user), - value: user.pk, - })), required: false, }) - .map((f, i) => ({ - ...f, - label: `Medebeoordelaar ${1 + i}`, - value: assignedCoReviewers[i]?.user.pk, - })); + .map((f, i) => { + return { + ...f, + label: `Medebeoordelaar ${1 + i}`, + value: activeCoReviewers?.[i], + options: coReviewers + // Don't show the co-reviewer as option if: + // - The co-reviewer is already selected AND + // - The co-reviewer is not selected as value for the current + // field. + .filter((c) => { + const selectedIndex = activeCoReviewers.indexOf( + c.pk.toString(), + ); + if (selectedIndex < 0 || selectedIndex === i) { + return true; + } + return false; + }) + .map((user) => ({ + label: formatUser(user), + value: user.pk, + })), + }; + }); return [...coReviewerFields, comment]; } return [reviewer, comment]; - }, [user, destructionList, reviewers, assignedCoReviewers]); + }, [ + user, + destructionList, + reviewers, + assignedCoReviewers, + assignCoReviewersFormValuesState, + ]); + /** + * Contains the co-reviewers that are assigned to the destruction list as + * items for the AttributeTable. + */ const coReviewerItems = useMemo( () => assignedCoReviewers.reduce((acc, coReviewer, i) => { @@ -194,6 +259,29 @@ export function DestructionListReviewer({ [assignedCoReviewers, coReviews], ); + /** + * Opens a dialog to assign a co-reviewer and updates it when `fields` change. + */ + useEffect(() => { + formDialog( + "Beoordelaar toewijzen", + null, + fields, + "Toewijzen", + "Annuleren", + handleSubmit, + undefined, + { + allowClose: true, + open: assignCoReviewerModalOpenState, + onClose: () => setAssignCoReviewerModalOpenState(false), + }, + { + validate: handleValidate, + }, + ); + }, [assignCoReviewerModalOpenState, fields]); + return ( <> {reviewer && ( @@ -217,18 +305,9 @@ export function DestructionListReviewer({ } size="xs" variant="secondary" - onClick={(e) => { - formDialog( - "Beoordelaar toewijzen", - null, - fields, - "Toewijzen", - "Annuleren", - handleSubmit, - undefined, - { allowClose: true }, - ); - }} + onClick={() => + setAssignCoReviewerModalOpenState(true) + } >