mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-03-01 01:20:45 +00:00
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:
parent
a025aaa017
commit
7e128c5a55
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user