- Filter available form intakers so that no form intaker can be in two workflows at once.
- add getApprovalWorkflowByIntaker to prepare workflow start after exam creation. - fix builder bug with step keys - ignore edit view for now because it will only be available for active workflows and not configured workflows.
This commit is contained in:
@@ -13,12 +13,12 @@ interface Props {
|
|||||||
workflow: EditableApprovalWorkflow;
|
workflow: EditableApprovalWorkflow;
|
||||||
onWorkflowChange: (workflow: EditableApprovalWorkflow) => void;
|
onWorkflowChange: (workflow: EditableApprovalWorkflow) => void;
|
||||||
entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
|
entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
|
||||||
|
entityAvailableFormIntakers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isRedirecting: boolean;
|
isRedirecting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, isLoading, isRedirecting }: Props) {
|
export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, entityAvailableFormIntakers, isLoading, isRedirecting }: Props) {
|
||||||
const [stepCounter, setStepCounter] = useState<number>(3); // to guarantee unique keys used for animations
|
|
||||||
const lastStep = workflow.steps[workflow.steps.length - 1];
|
const lastStep = workflow.steps[workflow.steps.length - 1];
|
||||||
|
|
||||||
const renumberSteps = (steps: EditableWorkflowStep[]): EditableWorkflowStep[] => {
|
const renumberSteps = (steps: EditableWorkflowStep[]): EditableWorkflowStep[] => {
|
||||||
@@ -30,7 +30,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
|
|||||||
|
|
||||||
const addStep = () => {
|
const addStep = () => {
|
||||||
const newStep: EditableWorkflowStep = {
|
const newStep: EditableWorkflowStep = {
|
||||||
key: stepCounter,
|
key: Date.now(),
|
||||||
stepType: "approval-by",
|
stepType: "approval-by",
|
||||||
stepNumber: workflow.steps.length,
|
stepNumber: workflow.steps.length,
|
||||||
completed: false,
|
completed: false,
|
||||||
@@ -38,7 +38,6 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
|
|||||||
firstStep: false,
|
firstStep: false,
|
||||||
finalStep: false,
|
finalStep: false,
|
||||||
};
|
};
|
||||||
setStepCounter((count) => count + 1);
|
|
||||||
|
|
||||||
const updatedSteps = [
|
const updatedSteps = [
|
||||||
...workflow.steps.slice(0, -1),
|
...workflow.steps.slice(0, -1),
|
||||||
@@ -137,7 +136,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
|
|||||||
finalStep={step.finalStep}
|
finalStep={step.finalStep}
|
||||||
onDelete={() => handleDelete(step.key)}
|
onDelete={() => handleDelete(step.key)}
|
||||||
onSelectChange={(numberOfSelects, idx, option) => handleSelectChange(step.key, numberOfSelects, idx, option)}
|
onSelectChange={(numberOfSelects, idx, option) => handleSelectChange(step.key, numberOfSelects, idx, option)}
|
||||||
entityApprovers={entityApprovers}
|
entityApprovers={step.stepNumber === 1 ? entityAvailableFormIntakers : entityApprovers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{step.finalStep &&
|
{step.finalStep &&
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const configuredWorkflows: ApprovalWorkflow[] = filteredWorkflows;
|
const configuredWorkflows: ApprovalWorkflow[] = filteredWorkflows;
|
||||||
const entitiesIds: string[] = userEntitiesWithLabel.map((e) => e.id);
|
const entitiesIds: string[] = userEntitiesWithLabel.map((e) => e.id);
|
||||||
|
|
||||||
await replaceApprovalWorkflowsByEntities("configured-workflows", configuredWorkflows, entitiesIds);
|
await replaceApprovalWorkflowsByEntities(configuredWorkflows, entitiesIds);
|
||||||
|
|
||||||
return res.status(201).json({ ok: true });
|
return res.status(201).json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { GrClearOption } from "react-icons/gr";
|
|||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflow, getFormIntakersByEntity } from "@/utils/approval.workflows.be";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
|
||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
@@ -47,6 +47,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
|||||||
user,
|
user,
|
||||||
workflow,
|
workflow,
|
||||||
userEntitiesWithLabel,
|
userEntitiesWithLabel,
|
||||||
|
entityUnavailableFormIntakers: await getFormIntakersByEntity(workflow.entityId),
|
||||||
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -56,10 +57,11 @@ interface Props {
|
|||||||
user: User,
|
user: User,
|
||||||
workflow: ApprovalWorkflow,
|
workflow: ApprovalWorkflow,
|
||||||
userEntitiesWithLabel: Entity[],
|
userEntitiesWithLabel: Entity[],
|
||||||
|
entityUnavailableFormIntakers: string[],
|
||||||
userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ user, workflow, userEntitiesWithLabel, userEntitiesApprovers }: Props) {
|
export default function Home({ user, workflow, userEntitiesWithLabel, entityUnavailableFormIntakers, userEntitiesApprovers }: Props) {
|
||||||
const [cloneWorkflow, setCloneWorkflow] = useState<EditableApprovalWorkflow | null>(null);
|
const [cloneWorkflow, setCloneWorkflow] = useState<EditableApprovalWorkflow | null>(null);
|
||||||
const [entityId, setEntityId] = useState<string | null | undefined>(workflow.entityId);
|
const [entityId, setEntityId] = useState<string | null | undefined>(workflow.entityId);
|
||||||
const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
|
const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
|
||||||
@@ -276,6 +278,7 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti
|
|||||||
workflow={cloneWorkflow}
|
workflow={cloneWorkflow}
|
||||||
onWorkflowChange={onWorkflowChange}
|
onWorkflowChange={onWorkflowChange}
|
||||||
entityApprovers={entityApprovers}
|
entityApprovers={entityApprovers}
|
||||||
|
entityAvailableFormIntakers={entityApprovers.filter(approver => !entityUnavailableFormIntakers.includes(approver.id))}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isRedirecting={isRedirecting}
|
isRedirecting={isRedirecting}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab
|
|||||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined);
|
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined);
|
||||||
const [entityId, setEntityId] = useState<string | null | undefined>(null);
|
const [entityId, setEntityId] = useState<string | null | undefined>(null);
|
||||||
const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
|
const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
|
||||||
|
const [entityAvailableFormIntakers, setEntityAvailableFormIntakers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
|
||||||
const [isAdding, setIsAdding] = useState<boolean>(false); // used to temporary timeout new workflow button. With animations, clicking too fast might cause state inconsistencies between renders.
|
const [isAdding, setIsAdding] = useState<boolean>(false); // used to temporary timeout new workflow button. With animations, clicking too fast might cause state inconsistencies between renders.
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [isRedirecting, setIsRedirecting] = useState<boolean>(false);
|
const [isRedirecting, setIsRedirecting] = useState<boolean>(false);
|
||||||
@@ -72,11 +73,40 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab
|
|||||||
approver.entities.some(entity => entity.id === entityId)
|
approver.entities.some(entity => entity.id === entityId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
setEntityApprovers([]);
|
|
||||||
}
|
}
|
||||||
}, [entityId, userEntitiesApprovers]);
|
}, [entityId, userEntitiesApprovers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (entityId) {
|
||||||
|
// Get all workflows for the selected entity
|
||||||
|
const workflowsForEntity = workflows.filter(wf => wf.entityId === entityId);
|
||||||
|
|
||||||
|
// For all workflows except the current one, collect the first step assignees
|
||||||
|
const assignedFormIntakers = workflowsForEntity.reduce<string[]>((acc, wf) => {
|
||||||
|
if (wf.id === selectedWorkflowId) return acc; // skip current workflow so its selection isn’t removed
|
||||||
|
|
||||||
|
const formIntakeStep = wf.steps.find(step => step.stepType === "form-intake");
|
||||||
|
if (formIntakeStep) {
|
||||||
|
// Only consider non-null assignees
|
||||||
|
const validAssignees = formIntakeStep.assignees.filter(
|
||||||
|
(assignee): assignee is string => !!assignee
|
||||||
|
);
|
||||||
|
return acc.concat(validAssignees);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Now filter out any user from entityApprovers whose id is in the assignedFormIntakers list.
|
||||||
|
// (The selected one in the current workflow is allowed even if it is in the list.)
|
||||||
|
const availableFormIntakers = entityApprovers.filter(assignee =>
|
||||||
|
!assignedFormIntakers.includes(assignee.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
setEntityAvailableFormIntakers(availableFormIntakers);
|
||||||
|
}
|
||||||
|
}, [entityId, entityApprovers, workflows, selectedWorkflowId]);
|
||||||
|
|
||||||
|
|
||||||
const currentWorkflow = workflows.find(wf => wf.id === selectedWorkflowId);
|
const currentWorkflow = workflows.find(wf => wf.id === selectedWorkflowId);
|
||||||
|
|
||||||
const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({
|
const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({
|
||||||
@@ -316,6 +346,7 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab
|
|||||||
workflow={currentWorkflow}
|
workflow={currentWorkflow}
|
||||||
onWorkflowChange={onWorkflowChange}
|
onWorkflowChange={onWorkflowChange}
|
||||||
entityApprovers={entityApprovers}
|
entityApprovers={entityApprovers}
|
||||||
|
entityAvailableFormIntakers={entityAvailableFormIntakers}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isRedirecting={isRedirecting}
|
isRedirecting={isRedirecting}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,39 @@ export const getApprovalWorkflowsByEntities = async (collection: string, ids: st
|
|||||||
.toArray();
|
.toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getApprovalWorkflowByFormIntaker = async (entityId: string, formIntakerId: string) => {
|
||||||
|
return await db.collection<ApprovalWorkflow>("configured-workflows").findOne({
|
||||||
|
entityId,
|
||||||
|
steps: {
|
||||||
|
$elemMatch: {
|
||||||
|
stepNumber: 1,
|
||||||
|
assignees: formIntakerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFormIntakersByEntity = async (entityId: string) => {
|
||||||
|
const results = await db
|
||||||
|
.collection<ApprovalWorkflow>("configured-workflows")
|
||||||
|
.aggregate([
|
||||||
|
// 1. Match workflows with the provided entityId
|
||||||
|
{ $match: { entityId } },
|
||||||
|
// 2. Unwind the steps array to process each step individually
|
||||||
|
{ $unwind: "$steps" },
|
||||||
|
// 3. Filter for the first step (you could also check for a "firstStep" flag if you prefer)
|
||||||
|
{ $match: { "steps.stepNumber": 1 } },
|
||||||
|
// 4. Unwind the assignees array so that each assignee is handled separately
|
||||||
|
{ $unwind: "$steps.assignees" },
|
||||||
|
// 5. Group by null (i.e. all documents) and add each assignee to a set to remove duplicates
|
||||||
|
{ $group: { _id: null, assignees: { $addToSet: "$steps.assignees" } } },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
// Return the assignees if the aggregation found any; otherwise return an empty array
|
||||||
|
return results.length > 0 ? results[0].assignees : [];
|
||||||
|
};
|
||||||
|
|
||||||
export const createApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => {
|
export const createApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => {
|
||||||
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
|
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
|
||||||
return await db.collection(collection).insertOne(workflowWithoutId);
|
return await db.collection(collection).insertOne(workflowWithoutId);
|
||||||
@@ -35,7 +68,7 @@ export const createApprovalWorkflows = async (collection: string, workflows: App
|
|||||||
|
|
||||||
export const updateApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => {
|
export const updateApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => {
|
||||||
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
|
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
|
||||||
return await db.collection(collection).replaceOne({ _id: _id }, workflowWithoutId);
|
return await db.collection(collection).replaceOne({ _id: new ObjectId(_id) }, workflowWithoutId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateApprovalWorkflows = async (collection: string, workflows: ApprovalWorkflow[]) => {
|
export const updateApprovalWorkflows = async (collection: string, workflows: ApprovalWorkflow[]) => {
|
||||||
@@ -56,7 +89,7 @@ export const deleteApprovalWorkflow = async (collection: string, id: string) =>
|
|||||||
return await db.collection(collection).deleteOne({ _id: new ObjectId(id) });
|
return await db.collection(collection).deleteOne({ _id: new ObjectId(id) });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const replaceApprovalWorkflowsByEntities = async (collection: string, workflows: ApprovalWorkflow[], entityIds: string[]) => {
|
export const replaceApprovalWorkflowsByEntities = async (workflows: ApprovalWorkflow[], entityIds: string[]) => {
|
||||||
// 1. Keep track of the _id values of all workflows we want to end up with
|
// 1. Keep track of the _id values of all workflows we want to end up with
|
||||||
const finalIds = new Set<string>();
|
const finalIds = new Set<string>();
|
||||||
|
|
||||||
@@ -64,17 +97,17 @@ export const replaceApprovalWorkflowsByEntities = async (collection: string, wor
|
|||||||
for (const workflow of workflows) {
|
for (const workflow of workflows) {
|
||||||
if (workflow._id) {
|
if (workflow._id) {
|
||||||
// Replace the existing ones
|
// Replace the existing ones
|
||||||
await updateApprovalWorkflow(collection, workflow);
|
await updateApprovalWorkflow("configured-workflows", workflow);
|
||||||
finalIds.add(workflow._id.toString());
|
finalIds.add(workflow._id.toString());
|
||||||
} else {
|
} else {
|
||||||
// Insert if no _id
|
// Insert if no _id
|
||||||
const insertResult = await createApprovalWorkflow(collection, workflow);
|
const insertResult = await createApprovalWorkflow("configured-workflows", workflow);
|
||||||
finalIds.add(insertResult.insertedId.toString());
|
finalIds.add(insertResult.insertedId.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Delete any existing workflow (within these entityIds) that wasn't in the final list
|
// 3. Delete any existing workflow (within these entityIds) that wasn't in the final list
|
||||||
await db.collection(collection).deleteMany({
|
await db.collection("configured-workflows").deleteMany({
|
||||||
_id: {
|
_id: {
|
||||||
$nin: Array.from(finalIds).map((id) => new ObjectId(id)),
|
$nin: Array.from(finalIds).map((id) => new ObjectId(id)),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user