diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 764ba543..2836dac2 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1082,6 +1082,12 @@ paths: /tasks/for-my-groups: parameters: + - name: group_identifier + in: query + required: false + description: The identifier of the group to get the tasks for + schema: + type: string - name: page in: query required: false @@ -1109,6 +1115,22 @@ paths: items: $ref: "#/components/schemas/Task" + /tasks/user-groups: + get: + tags: + - Process Instances + operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_user_groups + summary: Group identifiers for current logged in user + responses: + "200": + description: list of user groups + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Task" + /task-data/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: modified_process_model_identifier diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml index 419c925f..1f38e02b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml @@ -50,6 +50,7 @@ groups: fin, fin1, harmeet, + jason, sasha, manuchehr, lead, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index c29cf214..2f194af6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1301,7 +1301,6 @@ def task_list_for_my_open_processes( def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: - """Task_list_for_processes_started_by_others.""" return get_tasks( processes_started_by_user=False, has_lane_assignment_id=False, @@ -1311,10 +1310,18 @@ def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Respo def task_list_for_my_groups( + group_identifier: str = None, page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: - """Task_list_for_processes_started_by_others.""" - return get_tasks(processes_started_by_user=False, page=page, per_page=per_page) + return get_tasks(group_identifier=group_identifier, processes_started_by_user=False, page=page, per_page=per_page) + + +def task_list_user_groups( +) -> flask.wrappers.Response: + groups = g.user.groups + # TODO: filter out the default group and have a way to know what is the default group + group_identifiers = [i.identifier for i in groups if i.identifier != 'everybody'] + return make_response(jsonify(sorted(group_identifiers)), 200) def get_tasks( @@ -1322,6 +1329,7 @@ def get_tasks( has_lane_assignment_id: bool = True, page: int = 1, per_page: int = 100, + group_identifier: str = None, ) -> flask.wrappers.Response: """Get_tasks.""" user_id = g.user.id @@ -1358,9 +1366,14 @@ def get_tasks( ), ) if has_lane_assignment_id: - active_tasks_query = active_tasks_query.filter( - ActiveTaskModel.lane_assignment_id.is_not(None) # type: ignore - ) + if group_identifier: + active_tasks_query = active_tasks_query.filter( + GroupModel.identifier == group_identifier + ) + else: + active_tasks_query = active_tasks_query.filter( + ActiveTaskModel.lane_assignment_id.is_not(None) # type: ignore + ) else: active_tasks_query = active_tasks_query.filter(ActiveTaskModel.lane_assignment_id.is_(None)) # type: ignore diff --git a/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx b/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx index deb2030e..7ee7edec 100644 --- a/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx +++ b/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx @@ -1,156 +1,17 @@ -import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, - refreshAtInterval, -} from '../helpers'; -import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; +import TasksTable from './TasksTable'; -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const paginationQueryParamPrefix = 'tasks_for_my_open_processes'; -const REFRESH_INTERVAL = 5; -const REFRESH_TIMEOUT = 600; export default function MyOpenProcesses() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); - - useEffect(() => { - const getTasks = () => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }; - getTasks(); - refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); - }, [searchParams]); - - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); - return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - - ); - }); - return ( - - - - - - - - - - - - - {rows} -
IdProcessTaskWaiting ForDate StartedLast UpdatedActions
- ); - }; - - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- There are no tasks for processes you started at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - return ( - - ); - }; - return ( - <> -

My open instances

-

- These tasks are for processes you started which are not complete. You - may not have an action to take at this time. See below for tasks waiting - on you. -

- {tasksComponent()} - + ); } diff --git a/spiffworkflow-frontend/src/components/TasksTable.tsx b/spiffworkflow-frontend/src/components/TasksTable.tsx new file mode 100644 index 00000000..1c4d15e9 --- /dev/null +++ b/spiffworkflow-frontend/src/components/TasksTable.tsx @@ -0,0 +1,200 @@ +import { useEffect, useState } from 'react'; +// @ts-ignore +import { Button, Table } from '@carbon/react'; +import { Link, useSearchParams } from 'react-router-dom'; +import PaginationForTable from './PaginationForTable'; +import { + convertSecondsToFormattedDateTime, + getPageInfoFromSearchParams, + modifyProcessIdentifierForPathParam, + refreshAtInterval, +} from '../helpers'; +import HttpService from '../services/HttpService'; +import { PaginationObject, ProcessInstanceTask } from '../interfaces'; +import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; + +const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; +const REFRESH_INTERVAL = 5; +const REFRESH_TIMEOUT = 600; + +type OwnProps = { + apiPath: string; + tableTitle: string; + tableDescription: string; + additionalParams?: string; + paginationQueryParamPrefix?: string; + paginationClassName?: string; + autoReload?: boolean; + showStartedBy?: boolean; + showWaitingOn?: boolean; +}; + +export default function TasksTable({ + apiPath, + tableTitle, + tableDescription, + additionalParams, + paginationQueryParamPrefix, + paginationClassName, + autoReload = false, + showStartedBy = true, + showWaitingOn = true, +}: OwnProps) { + const [searchParams] = useSearchParams(); + const [tasks, setTasks] = useState(null); + const [pagination, setPagination] = useState(null); + + useEffect(() => { + const getTasks = () => { + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE, + undefined, + paginationQueryParamPrefix + ); + const setTasksFromResult = (result: any) => { + setTasks(result.results); + setPagination(result.pagination); + }; + let params = `?per_page=${perPage}&page=${page}`; + if (additionalParams) { + params = `${params}&${additionalParams}`; + } + HttpService.makeCallToBackend({ + path: `${apiPath}${params}`, + successCallback: setTasksFromResult, + }); + }; + getTasks(); + if (autoReload) { + refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); + } + }, [ + searchParams, + additionalParams, + apiPath, + paginationQueryParamPrefix, + autoReload, + ]); + + const buildTable = () => { + if (!tasks) { + return null; + } + const rows = tasks.map((row) => { + const rowToUse = row as any; + const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; + const modifiedProcessModelIdentifier = + modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); + return ( + + + + {rowToUse.process_instance_id} + + + + + {rowToUse.process_model_display_name} + + + + {rowToUse.task_title} + + {showStartedBy ? {rowToUse.username} : ''} + {showWaitingOn ? {rowToUse.group_identifier || '-'} : ''} + + {convertSecondsToFormattedDateTime( + rowToUse.created_at_in_seconds + ) || '-'} + + + + + + + ); + }); + let tableHeaders = ['Id', 'Process', 'Task']; + if (showStartedBy) { + tableHeaders.push('Started By'); + } + if (showWaitingOn) { + tableHeaders.push('Waiting For'); + } + tableHeaders = tableHeaders.concat([ + 'Date Started', + 'Last Updated', + 'Actions', + ]); + return ( + + + + {tableHeaders.map((tableHeader: string) => { + return ; + })} + + + {rows} +
{tableHeader}
+ ); + }; + + const tasksComponent = () => { + if (pagination && pagination.total < 1) { + return ( +

+ Your groups have no task assignments at this time. +

+ ); + } + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE, + undefined, + paginationQueryParamPrefix + ); + return ( + + ); + }; + + if (tasks) { + return ( + <> +

{tableTitle}

+

{tableDescription}

+ {tasksComponent()} + + ); + } + return null; +} diff --git a/spiffworkflow-frontend/src/components/TasksWaitingForMe.tsx b/spiffworkflow-frontend/src/components/TasksWaitingForMe.tsx index 7d06b7a3..e253afd6 100644 --- a/spiffworkflow-frontend/src/components/TasksWaitingForMe.tsx +++ b/spiffworkflow-frontend/src/components/TasksWaitingForMe.tsx @@ -1,149 +1,15 @@ -import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, -} from '../helpers'; -import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; - -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; +import TasksTable from './TasksTable'; export default function TasksWaitingForMe() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); - - useEffect(() => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - 'tasks_waiting_for_me' - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-me?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }, [searchParams]); - - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); - return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.username} - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - - ); - }); - return ( - - - - - - - - - - - - - - {rows} -
IdProcessTaskStarted ByWaiting ForDate StartedLast UpdatedActions
- ); - }; - - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- You have no task assignments at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - 'tasks_waiting_for_me' - ); - return ( - - ); - }; - return ( - <> -

Tasks waiting for me

-

- These processes are waiting on you to complete the next task. All are - processes created by others that are now actionable by you. -

- {tasksComponent()} - + ); } diff --git a/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx b/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx index 565cd4a5..93d21640 100644 --- a/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx +++ b/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx @@ -1,156 +1,39 @@ import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, - refreshAtInterval, -} from '../helpers'; import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; - -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; -const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups'; -const REFRESH_INTERVAL = 5; -const REFRESH_TIMEOUT = 600; +import TasksTable from './TasksTable'; export default function TasksWaitingForMyGroups() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); + const [userGroups, setUserGroups] = useState(null); useEffect(() => { - const getTasks = () => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }; - getTasks(); - refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); - }, [searchParams]); + HttpService.makeCallToBackend({ + path: `/tasks/user-groups`, + successCallback: setUserGroups, + }); + }, [setUserGroups]); + const tableComponents = () => { + if (!userGroups) { + return null; + } - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); + return userGroups.map((userGroup: string) => { return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.username} - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - + ); }); - return ( - - - - - - - - - - - - - - {rows} -
IdProcessTaskStarted ByWaiting ForDate StartedLast UpdatedActions
- ); }; - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- Your groups have no task assignments at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - return ( - - ); - }; - - return ( - <> -

Tasks waiting for my groups

-

- This is a list of tasks for groups you belong to that can be completed - by any member of the group. -

- {tasksComponent()} - - ); + if (userGroups) { + return <>{tableComponents()}; + } + return null; } diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index 6781ada9..fcc3371b 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -203,10 +203,10 @@ export const refreshAtInterval = ( timeout: number, func: Function ) => { - const intervalRef = setInterval(() => func(), interval * 1000); - const timeoutRef = setTimeout( - () => clearInterval(intervalRef), - timeout * 1000 - ); - return [intervalRef, timeoutRef]; + // const intervalRef = setInterval(() => func(), interval * 1000); + // const timeoutRef = setTimeout( + // () => clearInterval(intervalRef), + // timeout * 1000 + // ); + // return [intervalRef, timeoutRef]; }; diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 079e4cdc..cc3180e5 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -11,6 +11,16 @@ export interface RecentProcessModel { processModelDisplayName: string; } +export interface ProcessInstanceTask { + id: number; + process_model_display_name: string; + process_model_identifier: string; + task_title: string; + lane_assignment_id: string; + process_instance_status: number; + updated_at_in_seconds: number; +} + export interface ProcessReference { name: string; // The process or decision Display name. identifier: string; // The unique id of the process