Feature/completed tasks on pi show (#591)

* added api to get all completed tasks for an instance and display it in a table w/ burnettk

* moved completed tasks table on pi show page to sub tabs

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-10-26 11:37:55 -04:00 committed by GitHub
parent a025aaa017
commit 7e128c5a55
5 changed files with 187 additions and 49 deletions

View File

@ -1499,6 +1499,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
# NOT USED
/tasks: /tasks:
parameters: parameters:
- name: process_instance_id - name: process_instance_id
@ -1662,6 +1663,41 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $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: /users/search:
parameters: parameters:
- name: username_prefix - name: username_prefix

View File

@ -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) 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: 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) return _get_tasks(page=page, per_page=per_page)

View File

@ -40,6 +40,7 @@ type OwnProps = {
canCompleteAllTasks?: boolean; canCompleteAllTasks?: boolean;
showActionsColumn?: boolean; showActionsColumn?: boolean;
showViewFormDataButton?: boolean; showViewFormDataButton?: boolean;
showCompletedBy?: boolean;
}; };
export default function TaskListTable({ export default function TaskListTable({
@ -63,6 +64,7 @@ export default function TaskListTable({
canCompleteAllTasks = false, canCompleteAllTasks = false,
showActionsColumn = true, showActionsColumn = true,
showViewFormDataButton = false, showViewFormDataButton = false,
showCompletedBy = false,
}: OwnProps) { }: OwnProps) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null); const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(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 taskUrl = `/tasks/${processInstanceTask.process_instance_id}/${processInstanceTask.task_id}`;
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
let hasAccessToCompleteTask = false; let hasAccessToCompleteTask = false;
if ( if (
@ -260,6 +261,40 @@ export default function TaskListTable({
) { ) {
hasAccessToCompleteTask = true; hasAccessToCompleteTask = true;
} }
const actions = [];
if (
!(
processInstanceTask.process_instance_status in
['suspended', 'completed', 'error']
) &&
!processInstanceTask.completed
) {
actions.push(
<Button
variant="primary"
href={taskUrl}
disabled={!hasAccessToCompleteTask}
size="sm"
>
Go
</Button>
);
}
if (showViewFormDataButton) {
actions.push(
<Button
variant="primary"
onClick={() => getFormSubmissionDataForTask(processInstanceTask)}
>
View task
</Button>
);
}
return actions;
};
const getTableRow = (processInstanceTask: ProcessInstanceTask) => {
const rowElements: ReactElement[] = []; const rowElements: ReactElement[] = [];
dealWithProcessCells(rowElements, processInstanceTask); dealWithProcessCells(rowElements, processInstanceTask);
@ -283,6 +318,9 @@ export default function TaskListTable({
<td>{getWaitingForTableCellComponent(processInstanceTask)}</td> <td>{getWaitingForTableCellComponent(processInstanceTask)}</td>
); );
} }
if (showCompletedBy) {
rowElements.push(<td>{processInstanceTask.completed_by_username}</td>);
}
if (showDateStarted) { if (showDateStarted) {
rowElements.push( rowElements.push(
<td> <td>
@ -300,36 +338,7 @@ export default function TaskListTable({
); );
} }
if (showActionsColumn) { if (showActionsColumn) {
const actions = []; rowElements.push(<td>{getActionButtons(processInstanceTask)}</td>);
if (
!(
processInstanceTask.process_instance_status in
['suspended', 'completed', 'error']
) &&
!processInstanceTask.completed
) {
actions.push(
<Button
variant="primary"
href={taskUrl}
disabled={!hasAccessToCompleteTask}
size="sm"
>
Go
</Button>
);
}
if (showViewFormDataButton) {
actions.push(
<Button
variant="primary"
onClick={() => getFormSubmissionDataForTask(processInstanceTask)}
>
View task
</Button>
);
}
rowElements.push(<td>{actions}</td>);
} }
return <tr key={processInstanceTask.id}>{rowElements}</tr>; return <tr key={processInstanceTask.id}>{rowElements}</tr>;
}; };
@ -349,6 +358,9 @@ export default function TaskListTable({
if (showWaitingOn) { if (showWaitingOn) {
tableHeaders.push('Waiting for'); tableHeaders.push('Waiting for');
} }
if (showCompletedBy) {
tableHeaders.push('Completed by');
}
if (showDateStarted) { if (showDateStarted) {
tableHeaders.push('Date started'); tableHeaders.push('Date started');
} }

View File

@ -131,6 +131,9 @@ export interface ProcessInstanceTask {
task_title?: string; task_title?: string;
task_name?: string; task_name?: string;
completed?: boolean; completed?: boolean;
// gets shoved onto HumanTaskModel in result
completed_by_username?: string;
} }
export interface ProcessReference { export interface ProcessReference {

View File

@ -118,6 +118,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
>(null); >(null);
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0); const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const [selectedTaskTabSubTab, setSelectedTaskTabSubTab] = useState<number>(0);
const [copiedShortLinkToClipboard, setCopiedShortLinkToClipboard] = const [copiedShortLinkToClipboard, setCopiedShortLinkToClipboard] =
useState<boolean>(false); useState<boolean>(false);
@ -250,6 +251,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (searchParams.get('tab')) { if (searchParams.get('tab')) {
setSelectedTabIndex(parseInt(searchParams.get('tab') || '0', 10)); setSelectedTabIndex(parseInt(searchParams.get('tab') || '0', 10));
} }
if (searchParams.get('taskSubTab')) {
setSelectedTaskTabSubTab(
parseInt(searchParams.get('taskSubTab') || '0', 10)
);
}
return undefined; return undefined;
}, [ }, [
permissionsLoaded, permissionsLoaded,
@ -1475,6 +1481,67 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
updateSearchParams(newTabIndex.selectedIndex, 'tab'); updateSearchParams(newTabIndex.selectedIndex, 'tab');
}; };
const updateSelectedTaskTabSubTab = (newTabIndex: any) => {
updateSearchParams(newTabIndex.selectedIndex, 'taskSubTab');
};
const taskTabSubTabs = () => {
if (!processInstance) {
return null;
}
return (
<Tabs
selectedIndex={selectedTaskTabSubTab}
onChange={updateSelectedTaskTabSubTab}
>
<TabList aria-label="List of tabs">
<Tab>Completed by me</Tab>
<Tab>All completed</Tab>
</TabList>
<TabPanels>
<TabPanel>
{selectedTaskTabSubTab === 0 ? (
<TaskListTable
apiPath={`/tasks/completed-by-me/${processInstance.id}`}
paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="You have not completed any tasks for this process instance."
shouldPaginateTable={false}
showProcessModelIdentifier={false}
showProcessId={false}
showStartedBy={false}
showTableDescriptionAsTooltip
showDateStarted={false}
showWaitingOn={false}
canCompleteAllTasks={false}
showViewFormDataButton
/>
) : null}
</TabPanel>
<TabPanel>
{selectedTaskTabSubTab === 1 ? (
<TaskListTable
apiPath={`/tasks/completed/${processInstance.id}`}
paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="There are no completed tasks for this process instance."
shouldPaginateTable={false}
showProcessModelIdentifier={false}
showProcessId={false}
showStartedBy={false}
showTableDescriptionAsTooltip
showDateStarted={false}
showWaitingOn={false}
canCompleteAllTasks={false}
showCompletedBy
showActionsColumn={false}
/>
) : null}
</TabPanel>
</TabPanels>
</Tabs>
);
};
if (processInstance && (tasks || tasksCallHadError) && permissionsLoaded) { if (processInstance && (tasks || tasksCallHadError) && permissionsLoaded) {
const processModelId = unModifyProcessIdentifierForPathParam( const processModelId = unModifyProcessIdentifierForPathParam(
params.process_model_id ? params.process_model_id : '' params.process_model_id ? params.process_model_id : ''
@ -1505,7 +1572,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
<Tab disabled={!canViewLogs}>Milestones</Tab> <Tab disabled={!canViewLogs}>Milestones</Tab>
<Tab disabled={!canViewLogs}>Events</Tab> <Tab disabled={!canViewLogs}>Events</Tab>
<Tab disabled={!canViewMsgs}>Messages</Tab> <Tab disabled={!canViewMsgs}>Messages</Tab>
<Tab>My completed tasks</Tab> <Tab>Tasks</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
@ -1537,22 +1604,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
{selectedTabIndex === 3 ? getMessageDisplay() : null} {selectedTabIndex === 3 ? getMessageDisplay() : null}
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
{selectedTabIndex === 4 ? ( {selectedTabIndex === 4 ? taskTabSubTabs() : null}
<TaskListTable
apiPath={`/tasks/completed-by-me/${processInstance.id}`}
paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="You have not completed any tasks for this process instance."
shouldPaginateTable={false}
showProcessModelIdentifier={false}
showProcessId={false}
showStartedBy={false}
showTableDescriptionAsTooltip
showDateStarted={false}
showWaitingOn={false}
canCompleteAllTasks={false}
showViewFormDataButton
/>
) : null}
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>