diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 59d156e6..825a24b4 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1333,6 +1333,12 @@ paths: /tasks: parameters: + - name: process_instance_id + in: query + required: false + description: The process instance id to search by. + schema: + type: integer - name: page in: query required: false diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml index ee40f839..558b9eaf 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml @@ -106,6 +106,11 @@ permissions: users: [] allowed_permissions: [create, read, update, delete] uri: /process-instances/reports/* + read-process-instances-find-by-id: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /process-instances/find-by-id/* processes-read: groups: [everybody] users: [] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index a7d3bf86..01b3741d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -67,35 +67,48 @@ class ReactJsonSchemaSelectOption(TypedDict): # TODO: see comment for before_request # @process_api_blueprint.route("/v1.0/tasks", methods=["GET"]) -def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: +def task_list_my_tasks( + process_instance_id: Optional[int] = None, page: int = 1, per_page: int = 100 +) -> flask.wrappers.Response: """Task_list_my_tasks.""" principal = _find_principal_or_raise() - human_tasks = ( + human_task_query = ( HumanTaskModel.query.order_by(desc(HumanTaskModel.id)) # type: ignore - .join(ProcessInstanceModel) - .join(HumanTaskUserModel) - .filter_by(user_id=principal.user_id) - .filter(HumanTaskModel.completed == False) # noqa: E712 - # just need this add_columns to add the process_model_identifier. Then add everything back that was removed. - .add_columns( - ProcessInstanceModel.process_model_identifier, - ProcessInstanceModel.process_model_display_name, - ProcessInstanceModel.status, - HumanTaskModel.task_name, - HumanTaskModel.task_title, - HumanTaskModel.task_type, - HumanTaskModel.task_status, - HumanTaskModel.task_id, - HumanTaskModel.id, - HumanTaskModel.process_model_display_name, - HumanTaskModel.process_instance_id, + .group_by(HumanTaskModel.id) + .join( + ProcessInstanceModel, + ProcessInstanceModel.id == HumanTaskModel.process_instance_id, ) - .paginate(page=page, per_page=per_page, error_out=False) + .join(HumanTaskUserModel, HumanTaskUserModel.human_task_id == HumanTaskModel.id) + .filter(HumanTaskUserModel.user_id == principal.user_id) + .join(UserModel, UserModel.id == HumanTaskUserModel.user_id) + .filter(HumanTaskModel.completed == False) # noqa: E712 + .outerjoin(GroupModel, GroupModel.id == HumanTaskModel.lane_assignment_id) ) - tasks = [HumanTaskModel.to_task(human_task) for human_task in human_tasks.items] + + if process_instance_id is not None: + human_task_query = human_task_query.filter( + ProcessInstanceModel.id == process_instance_id + ) + + human_tasks = human_task_query.add_columns( + ProcessInstanceModel.process_model_identifier, + ProcessInstanceModel.status.label("process_instance_status"), # type: ignore + ProcessInstanceModel.updated_at_in_seconds, + ProcessInstanceModel.created_at_in_seconds, + UserModel.username.label("process_initiator_username"), # type: ignore + GroupModel.identifier.label("assigned_user_group_identifier"), + HumanTaskModel.task_name, + HumanTaskModel.task_title, + HumanTaskModel.process_model_display_name, + HumanTaskModel.process_instance_id, + func.group_concat(UserModel.username.distinct()).label( # type: ignore + "potential_owner_usernames" + ), + ).paginate(page=page, per_page=per_page, error_out=False) response_json = { - "results": tasks, + "results": human_tasks.items, "pagination": { "count": len(human_tasks.items), "total": human_tasks.total, @@ -416,6 +429,7 @@ def _get_tasks( HumanTaskModel.id == HumanTaskUserModel.human_task_id, ), ) + if has_lane_assignment_id: if user_group_identifier: human_tasks_query = human_tasks_query.filter( diff --git a/spiffworkflow-frontend/src/components/TaskListTable.tsx b/spiffworkflow-frontend/src/components/TaskListTable.tsx index 2e53bcea..287742e9 100644 --- a/spiffworkflow-frontend/src/components/TaskListTable.tsx +++ b/spiffworkflow-frontend/src/components/TaskListTable.tsx @@ -29,6 +29,13 @@ type OwnProps = { showStartedBy?: boolean; showWaitingOn?: boolean; textToShowIfEmpty?: string; + shouldPaginateTable?: boolean; + showProcessId?: boolean; + showProcessModelIdentifier?: boolean; + showTableDescriptionAsTooltip?: boolean; + showDateStarted?: boolean; + showLastUpdated?: boolean; + hideIfNoTasks?: boolean; }; export default function TaskListTable({ @@ -42,6 +49,13 @@ export default function TaskListTable({ autoReload = false, showStartedBy = true, showWaitingOn = true, + shouldPaginateTable = true, + showProcessId = true, + showProcessModelIdentifier = true, + showTableDescriptionAsTooltip = false, + showDateStarted = true, + showLastUpdated = true, + hideIfNoTasks = false, }: OwnProps) { const [searchParams] = useSearchParams(); const [tasks, setTasks] = useState(null); @@ -89,10 +103,6 @@ export default function TaskListTable({ ) => { let fullUsernameString = ''; let shortUsernameString = ''; - if (processInstanceTask.assigned_user_group_identifier) { - fullUsernameString = processInstanceTask.assigned_user_group_identifier; - shortUsernameString = processInstanceTask.assigned_user_group_identifier; - } if (processInstanceTask.potential_owner_usernames) { fullUsernameString = processInstanceTask.potential_owner_usernames; const usernames = @@ -103,82 +113,133 @@ export default function TaskListTable({ } shortUsernameString = firstTwoUsernames.join(','); } + if (processInstanceTask.assigned_user_group_identifier) { + fullUsernameString = processInstanceTask.assigned_user_group_identifier; + shortUsernameString = processInstanceTask.assigned_user_group_identifier; + } return {shortUsernameString}; }; - const buildTable = () => { - if (!tasks) { - return null; - } - const rows = tasks.map((row: ProcessInstanceTask) => { - const taskUrl = `/tasks/${row.process_instance_id}/${row.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(row.process_model_identifier); + const getTableRow = (processInstanceTask: ProcessInstanceTask) => { + const taskUrl = `/tasks/${processInstanceTask.process_instance_id}/${processInstanceTask.task_id}`; + const modifiedProcessModelIdentifier = modifyProcessIdentifierForPathParam( + processInstanceTask.process_model_identifier + ); - const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); - let hasAccessToCompleteTask = false; - if (row.potential_owner_usernames.match(regex)) { - hasAccessToCompleteTask = true; - } - return ( - - - - {row.process_instance_id} - - - - - {row.process_model_display_name} - - - + - {row.task_title} - - {showStartedBy ? {row.process_initiator_username} : ''} - {showWaitingOn ? {getWaitingForTableCellComponent(row)} : ''} - - {convertSecondsToFormattedDateTime(row.created_at_in_seconds) || - '-'} - - - - - - + {processInstanceTask.process_instance_id} + + ); - }); - let tableHeaders = ['Id', 'Process', 'Task']; + } + if (showProcessModelIdentifier) { + rowElements.push( + + + {processInstanceTask.process_model_display_name} + + + ); + } + + rowElements.push( + + {processInstanceTask.task_title} + + ); + if (showStartedBy) { + rowElements.push( + {processInstanceTask.process_initiator_username} + ); + } + if (showWaitingOn) { + rowElements.push( + {getWaitingForTableCellComponent(processInstanceTask)} + ); + } + if (showDateStarted) { + rowElements.push( + + {convertSecondsToFormattedDateTime( + processInstanceTask.created_at_in_seconds + ) || '-'} + + ); + } + if (showLastUpdated) { + rowElements.push( + + ); + } + rowElements.push( + + + + ); + return {rowElements}; + }; + + const getTableHeaders = () => { + let tableHeaders = []; + if (showProcessId) { + tableHeaders.push('Id'); + } + if (showProcessModelIdentifier) { + tableHeaders.push('Process'); + } + tableHeaders.push('Task'); if (showStartedBy) { tableHeaders.push('Started By'); } if (showWaitingOn) { tableHeaders.push('Waiting For'); } - tableHeaders = tableHeaders.concat([ - 'Date Started', - 'Last Updated', - 'Actions', - ]); + if (showDateStarted) { + tableHeaders.push('Date Started'); + } + if (showLastUpdated) { + tableHeaders.push('Last Updated'); + } + tableHeaders = tableHeaders.concat(['Actions']); + return tableHeaders; + }; + + const buildTable = () => { + if (!tasks) { + return null; + } + const tableHeaders = getTableHeaders(); + const rows = tasks.map((processInstanceTask: ProcessInstanceTask) => { + return getTableRow(processInstanceTask); + }); return ( @@ -207,24 +268,41 @@ export default function TaskListTable({ undefined, paginationQueryParamPrefix ); - return ( - + let tableElement = ( +
{buildTable()}
); + if (shouldPaginateTable) { + tableElement = ( + + ); + } + return tableElement; }; - if (tasks) { + const tableAndDescriptionElement = () => { + if (showTableDescriptionAsTooltip) { + return

{tableTitle}

; + } return ( <>

{tableTitle}

{tableDescription}

+ + ); + }; + + if (tasks && (tasks.length > 0 || hideIfNoTasks === false)) { + return ( + <> + {tableAndDescriptionElement()} {tasksComponent()} ); diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 5025855b..f0c164bb 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -34,12 +34,12 @@ export interface ProcessInstanceTask { process_identifier: string; name: string; process_initiator_username: string; - assigned_user_group_identifier: string; created_at_in_seconds: number; updated_at_in_seconds: number; current_user_is_potential_owner: number; - potential_owner_usernames: string; calling_subprocess_task_id: string; + potential_owner_usernames?: string; + assigned_user_group_identifier?: string; } export interface ProcessReference { diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 6e73463e..279322a6 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -48,6 +48,7 @@ import { } from '../interfaces'; import { usePermissionFetcher } from '../hooks/PermissionService'; import ProcessInstanceClass from '../classes/ProcessInstanceClass'; +import TaskListTable from '../components/TaskListTable'; type OwnProps = { variant: string; @@ -1009,6 +1010,26 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {

+ + + + + {getInfoTag()}
{taskUpdateDisplayArea()}