diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index b954d5db2..46a7cd0dd 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1499,6 +1499,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" + # NOT USED /tasks: parameters: - name: process_instance_id @@ -1662,6 +1663,41 @@ paths: items: $ref: "#/components/schemas/Task" + /tasks/completed/{process_instance_id}: + parameters: + - name: page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + - name: per_page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + - name: process_instance_id + in: path + required: true + description: The unique id of an existing process instance. + schema: + type: integer + get: + tags: + - Process Instances + operationId: spiffworkflow_backend.routes.tasks_controller.task_list_completed + summary: returns the list of tasks for that the current user has completed + responses: + "200": + description: list of tasks + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Task" + /users/search: parameters: - name: username_prefix diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 9a6640519..752909bc4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -174,6 +174,41 @@ def task_list_completed_by_me(process_instance_id: int, page: int = 1, per_page: return make_response(jsonify(response_json), 200) +def task_list_completed(process_instance_id: int, page: int = 1, per_page: int = 100) -> flask.wrappers.Response: + human_tasks_query = ( + db.session.query(HumanTaskModel) # type: ignore + .join(UserModel, UserModel.id == HumanTaskModel.completed_by_user_id) + .filter( + HumanTaskModel.completed == True, # noqa: E712 + HumanTaskModel.process_instance_id == process_instance_id, + ) + .add_columns( + HumanTaskModel.task_name, + HumanTaskModel.task_title, + HumanTaskModel.process_model_display_name, + HumanTaskModel.process_instance_id, + HumanTaskModel.updated_at_in_seconds, + HumanTaskModel.created_at_in_seconds, + UserModel.username.label("completed_by_username"), # type: ignore + ) + ) + + human_tasks = human_tasks_query.order_by(desc(HumanTaskModel.id)).paginate( # type: ignore + page=page, per_page=per_page, error_out=False + ) + + response_json = { + "results": human_tasks.items, + "pagination": { + "count": len(human_tasks.items), + "total": human_tasks.total, + "pages": human_tasks.pages, + }, + } + + return make_response(jsonify(response_json), 200) + + def task_list_for_my_open_processes(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: return _get_tasks(page=page, per_page=per_page) diff --git a/spiffworkflow-frontend/src/components/TaskListTable.tsx b/spiffworkflow-frontend/src/components/TaskListTable.tsx index 075853b67..4c780f6f5 100644 --- a/spiffworkflow-frontend/src/components/TaskListTable.tsx +++ b/spiffworkflow-frontend/src/components/TaskListTable.tsx @@ -40,6 +40,7 @@ type OwnProps = { canCompleteAllTasks?: boolean; showActionsColumn?: boolean; showViewFormDataButton?: boolean; + showCompletedBy?: boolean; }; export default function TaskListTable({ @@ -63,6 +64,7 @@ export default function TaskListTable({ canCompleteAllTasks = false, showActionsColumn = true, showViewFormDataButton = false, + showCompletedBy = false, }: OwnProps) { const [searchParams] = useSearchParams(); const [tasks, setTasks] = useState(null); @@ -249,9 +251,8 @@ export default function TaskListTable({ } }; - const getTableRow = (processInstanceTask: ProcessInstanceTask) => { + const getActionButtons = (processInstanceTask: ProcessInstanceTask) => { const taskUrl = `/tasks/${processInstanceTask.process_instance_id}/${processInstanceTask.task_id}`; - const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); let hasAccessToCompleteTask = false; if ( @@ -260,6 +261,40 @@ export default function TaskListTable({ ) { hasAccessToCompleteTask = true; } + + const actions = []; + if ( + !( + processInstanceTask.process_instance_status in + ['suspended', 'completed', 'error'] + ) && + !processInstanceTask.completed + ) { + actions.push( + + ); + } + if (showViewFormDataButton) { + actions.push( + + ); + } + return actions; + }; + + const getTableRow = (processInstanceTask: ProcessInstanceTask) => { const rowElements: ReactElement[] = []; dealWithProcessCells(rowElements, processInstanceTask); @@ -283,6 +318,9 @@ export default function TaskListTable({ {getWaitingForTableCellComponent(processInstanceTask)} ); } + if (showCompletedBy) { + rowElements.push({processInstanceTask.completed_by_username}); + } if (showDateStarted) { rowElements.push( @@ -300,36 +338,7 @@ export default function TaskListTable({ ); } if (showActionsColumn) { - const actions = []; - if ( - !( - processInstanceTask.process_instance_status in - ['suspended', 'completed', 'error'] - ) && - !processInstanceTask.completed - ) { - actions.push( - - ); - } - if (showViewFormDataButton) { - actions.push( - - ); - } - rowElements.push({actions}); + rowElements.push({getActionButtons(processInstanceTask)}); } return {rowElements}; }; @@ -349,6 +358,9 @@ export default function TaskListTable({ if (showWaitingOn) { tableHeaders.push('Waiting for'); } + if (showCompletedBy) { + tableHeaders.push('Completed by'); + } if (showDateStarted) { tableHeaders.push('Date started'); } diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 476afcb0e..dbd3eb079 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -131,6 +131,9 @@ export interface ProcessInstanceTask { task_title?: string; task_name?: string; completed?: boolean; + + // gets shoved onto HumanTaskModel in result + completed_by_username?: string; } export interface ProcessReference { diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 7c0de6803..9a24c0ed0 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -118,6 +118,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { >(null); const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const [selectedTaskTabSubTab, setSelectedTaskTabSubTab] = useState(0); const [copiedShortLinkToClipboard, setCopiedShortLinkToClipboard] = useState(false); @@ -250,6 +251,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { if (searchParams.get('tab')) { setSelectedTabIndex(parseInt(searchParams.get('tab') || '0', 10)); } + if (searchParams.get('taskSubTab')) { + setSelectedTaskTabSubTab( + parseInt(searchParams.get('taskSubTab') || '0', 10) + ); + } return undefined; }, [ permissionsLoaded, @@ -1475,6 +1481,67 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { updateSearchParams(newTabIndex.selectedIndex, 'tab'); }; + const updateSelectedTaskTabSubTab = (newTabIndex: any) => { + updateSearchParams(newTabIndex.selectedIndex, 'taskSubTab'); + }; + + const taskTabSubTabs = () => { + if (!processInstance) { + return null; + } + + return ( + + + Completed by me + All completed + + + + {selectedTaskTabSubTab === 0 ? ( + + ) : null} + + + {selectedTaskTabSubTab === 1 ? ( + + ) : null} + + + + ); + }; + if (processInstance && (tasks || tasksCallHadError) && permissionsLoaded) { const processModelId = unModifyProcessIdentifierForPathParam( params.process_model_id ? params.process_model_id : '' @@ -1505,7 +1572,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { Milestones Events Messages - My completed tasks + Tasks @@ -1537,22 +1604,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { {selectedTabIndex === 3 ? getMessageDisplay() : null} - {selectedTabIndex === 4 ? ( - - ) : null} + {selectedTabIndex === 4 ? taskTabSubTabs() : null}