Merged in approval-workflows (pull request #151)
Approval workflows Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -19,7 +19,7 @@ interface SettingsEditorProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
canPreview: boolean;
|
canPreview: boolean;
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
submitModule: () => void;
|
submitModule: (requiresApproval: boolean) => void;
|
||||||
preview: () => void;
|
preview: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,19 +148,31 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{children}
|
{children}
|
||||||
<div className="flex flex-row justify-between mt-4">
|
<div className="flex flex-col gap-3 mt-4">
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||||
"disabled:cursor-not-allowed disabled:text-gray-200"
|
"disabled:cursor-not-allowed disabled:text-gray-200"
|
||||||
)}
|
)}
|
||||||
onClick={submitModule}
|
onClick={() => submitModule(true)}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
>
|
>
|
||||||
<FaFileUpload className="mr-2" size={18} />
|
<FaFileUpload className="mr-2" size={18} />
|
||||||
Submit Module as Exam
|
Submit Module as Exam
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||||
|
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||||
|
"disabled:cursor-not-allowed disabled:text-gray-200"
|
||||||
|
)}
|
||||||
|
onClick={() => submitModule(false)}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
>
|
||||||
|
<FaFileUpload className="mr-2" size={18} />
|
||||||
|
Submit Module as Exam Without Approval Process
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const LevelSettings: React.FC = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitLevel = async () => {
|
const submitLevel = async (requiresApproval: boolean) => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
@@ -195,7 +195,8 @@ const LevelSettings: React.FC = () => {
|
|||||||
category: s.settings.category
|
category: s.settings.category
|
||||||
};
|
};
|
||||||
}).filter(part => part.exercises.length > 0),
|
}).filter(part => part.exercises.length > 0),
|
||||||
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
requiresApproval: requiresApproval,
|
||||||
|
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "level",
|
module: "level",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const ListeningSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const submitListening = async () => {
|
const submitListening = async (requiresApproval: boolean) => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
@@ -138,7 +138,8 @@ const ListeningSettings: React.FC = () => {
|
|||||||
category: s.settings.category
|
category: s.settings.category
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
requiresApproval: requiresApproval,
|
||||||
|
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "listening",
|
module: "listening",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const ReadingSettings: React.FC = () => {
|
|||||||
(s.state as ReadingPart).exercises.length > 0
|
(s.state as ReadingPart).exercises.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitReading = () => {
|
const submitReading = (requiresApproval: boolean) => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
@@ -74,7 +74,8 @@ const ReadingSettings: React.FC = () => {
|
|||||||
category: localSettings.category,
|
category: localSettings.category,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
requiresApproval: requiresApproval,
|
||||||
|
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "reading",
|
module: "reading",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const SpeakingSettings: React.FC = () => {
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const submitSpeaking = async () => {
|
const submitSpeaking = async (requiresApproval: boolean) => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
@@ -181,7 +181,8 @@ const SpeakingSettings: React.FC = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
id: title,
|
id: title,
|
||||||
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
requiresApproval: requiresApproval,
|
||||||
|
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
instructorGender: "varied",
|
instructorGender: "varied",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitWriting = async () => {
|
const submitWriting = async (requiresApproval: boolean) => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
@@ -131,7 +131,8 @@ const WritingSettings: React.FC = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "writing",
|
module: "writing",
|
||||||
id: title,
|
id: title,
|
||||||
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
requiresApproval: requiresApproval,
|
||||||
|
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
access,
|
access,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface ExamBase {
|
|||||||
createdAt?: string; // option as it has been added later
|
createdAt?: string; // option as it has been added later
|
||||||
access: AccessType;
|
access: AccessType;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
requiresApproval?: boolean;
|
||||||
}
|
}
|
||||||
export interface ReadingExam extends ExamBase {
|
export interface ReadingExam extends ExamBase {
|
||||||
module: "reading";
|
module: "reading";
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export async function createApprovalWorkflowOnExamCreation(examAuthor: string, e
|
|||||||
configuredWorkflow.examId = examId;
|
configuredWorkflow.examId = examId;
|
||||||
configuredWorkflow.entityId = entity;
|
configuredWorkflow.entityId = entity;
|
||||||
configuredWorkflow.startDate = Date.now();
|
configuredWorkflow.startDate = Date.now();
|
||||||
|
configuredWorkflow.steps[0].completed = true;
|
||||||
|
configuredWorkflow.steps[0].completedBy = examAuthor;
|
||||||
|
configuredWorkflow.steps[0].completedDate = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createApprovalWorkflow("active-workflows", configuredWorkflow);
|
await createApprovalWorkflow("active-workflows", configuredWorkflow);
|
||||||
|
|||||||
@@ -98,21 +98,25 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// create workflow only if exam is being created for the first time
|
// create workflow only if exam is being created for the first time
|
||||||
if (docSnap === null) {
|
if (docSnap === null) {
|
||||||
try {
|
try {
|
||||||
const { successCount, totalCount } = await createApprovalWorkflowOnExamCreation(exam.createdBy, exam.entities, exam.id, module);
|
if (exam.requiresApproval === false) {
|
||||||
|
responseStatus = 200;
|
||||||
if (isAdmin(user)) {
|
responseMessage = `Successfully created exam "${exam.id}" and skipped Approval Workflow due to user request.`;
|
||||||
|
} else if (isAdmin(user)) {
|
||||||
responseStatus = 200;
|
responseStatus = 200;
|
||||||
responseMessage = `Successfully created exam "${exam.id}" and skipped Approval Workflow due to admin rights.`;
|
responseMessage = `Successfully created exam "${exam.id}" and skipped Approval Workflow due to admin rights.`;
|
||||||
} else if (successCount === totalCount) {
|
|
||||||
responseStatus = 200;
|
|
||||||
responseMessage = `Successfully created exam "${exam.id}" and started its Approval Workflow.`;
|
|
||||||
/* responseMessage = `Successfully created exam "${exam.id}" and started its Approval Workflow(s).`; */
|
|
||||||
} else if (successCount > 0) {
|
|
||||||
responseStatus = 207;
|
|
||||||
responseMessage = `Successfully created exam with ID: "${exam.id}" but was not able to start/find an Approval Workflow for all the author's entities.`;
|
|
||||||
} else {
|
} else {
|
||||||
responseStatus = 207;
|
const { successCount, totalCount } = await createApprovalWorkflowOnExamCreation(exam.createdBy, exam.entities, exam.id, module);
|
||||||
responseMessage = `Successfully created exam with ID: "${exam.id}" but skipping approval process because no approval workflow was found configured for the exam author.`;
|
|
||||||
|
if (successCount === totalCount) {
|
||||||
|
responseStatus = 200;
|
||||||
|
responseMessage = `Successfully created exam "${exam.id}" and started its Approval Workflow.`;
|
||||||
|
} else if (successCount > 0) {
|
||||||
|
responseStatus = 207;
|
||||||
|
responseMessage = `Successfully created exam with ID: "${exam.id}" but was not able to start/find an Approval Workflow for all the author's entities.`;
|
||||||
|
} else {
|
||||||
|
responseStatus = 207;
|
||||||
|
responseMessage = `Successfully created exam with ID: "${exam.id}" but skipping approval process because no approval workflow was found configured for the exam author.`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Workflow creation error:", error);
|
console.error("Workflow creation error:", error);
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
|||||||
const allAssigneeIds: string[] = [
|
const allAssigneeIds: string[] = [
|
||||||
...new Set(
|
...new Set(
|
||||||
workflow.steps
|
workflow.steps
|
||||||
.map(step => step.assignees)
|
.map((step) => {
|
||||||
|
const assignees = step.assignees;
|
||||||
|
if (step.completedBy) {
|
||||||
|
assignees.push(step.completedBy);
|
||||||
|
}
|
||||||
|
return assignees;
|
||||||
|
})
|
||||||
.flat()
|
.flat()
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import Tip from "@/components/ApprovalWorkflows/Tip";
|
import Tip from "@/components/ApprovalWorkflows/Tip";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
||||||
import { useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission } from "@/hooks/useEntityPermissions";
|
|
||||||
import { Module, ModuleTypeLabels } from "@/interfaces";
|
import { Module, ModuleTypeLabels } from "@/interfaces";
|
||||||
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
|
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
|
||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
|
||||||
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
|
import { doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getSpecificUsers } from "@/utils/users.be";
|
import { getSpecificUsers } from "@/utils/users.be";
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
@@ -192,7 +190,15 @@ export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAss
|
|||||||
{info.getValue().map((module: Module, index: number) => (
|
{info.getValue().map((module: Module, index: number) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-indigo-100 border border-indigo-300 text-indigo-900">
|
/* className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-indigo-100 border border-indigo-300 text-indigo-900"> */
|
||||||
|
className={clsx("inline-block rounded-full px-3 py-1 text-sm font-medium text-white",
|
||||||
|
module === "speaking" ? "bg-ielts-speaking" :
|
||||||
|
module === "reading" ? "bg-ielts-reading" :
|
||||||
|
module === "writing" ? "bg-ielts-writing" :
|
||||||
|
module === "listening" ? "bg-ielts-listening" :
|
||||||
|
module === "level" ? "bg-ielts-level" :
|
||||||
|
"bg-slate-700"
|
||||||
|
)}>
|
||||||
{ModuleTypeLabels[module]}
|
{ModuleTypeLabels[module]}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -297,10 +303,20 @@ export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAss
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredWorkflows,
|
data: filteredWorkflows,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
onPaginationChange: setPagination,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -396,6 +412,43 @@ export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAss
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div className="mt-2 flex flex-row gap-2 w-full justify-end items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
className="px-3 py-2 rounded-md text-sm font-semibold text-mti-purple-ultradark border border-mti-purple-light
|
||||||
|
bg-white hover:bg-mti-purple-light hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{"<<"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
className="px-3 py-2 rounded-md text-sm font-semibold text-mti-purple-ultradark border border-mti-purple-light
|
||||||
|
bg-white hover:bg-mti-purple-light hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{"<"}
|
||||||
|
</button>
|
||||||
|
<span className="px-4 text-sm font-medium">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
className="px-3 py-2 rounded-md text-sm font-semibold text-mti-purple-ultradark border border-mti-purple-light
|
||||||
|
bg-white hover:bg-mti-purple-light hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{">"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
className="px-3 py-2 rounded-md text-sm font-semibold text-mti-purple-ultradark border border-mti-purple-light
|
||||||
|
bg-white hover:bg-mti-purple-light hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{">>"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user