Feature/homepage extension filter refactor (#919)

* WIP: initial work to have a home page created from an extension w/ burnettk

* added support to display an extension as the root page w/ burnettk

* allow extensions to add new routes to base routes w/ burnettk

* use page instead of creating new key route w/ burnettk

* added components to support pi tables in extensions w/ burnettk

* allow using asterisks to mark words as bold in process instance list table

* moved table component from InstancesListTable to own component w/ burnettk

* filters are somewhat working again w/ burnettk

* default homepage uses the table without filters component now w/ burnettk

* renamed instance list tables to be more appropriate w/ burnettk

* display errors if list table is used incorrectly w/ burnettk

* fixed issue where columns were not displaying in the filter list

* pyl

* rely on changes in report hash to determine if report hash state needs updating

* only show link to report if there are instances to show

* many updates for filtering to remove the apply button and clean things up w/ burnettk

* some more fixes for too many renderings w/ burnettk

* advanced filters are working again w/ burnettk

* clear is working again w/ burnettk

* fixed a few linting errors and warnings w/ burnettk

* fixed some cypress tests

* if there are errors then display them right away instead of trying to put together the other elements

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2024-01-26 11:41:07 -05:00 committed by GitHub
parent 4758634c99
commit f0f4bcce12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 2177 additions and 1863 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from sqlalchemy.orm import relationship
@ -21,13 +22,14 @@ class GroupNotFoundError(Exception):
pass
@dataclass
class GroupModel(SpiffworkflowBaseDBModel):
__tablename__ = "group"
__table_args__ = {"extend_existing": True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), index=True)
identifier = db.Column(db.String(255), index=True)
name: str = db.Column(db.String(255), index=True)
identifier: str = db.Column(db.String(255), index=True)
user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete")
user_group_assignments_waiting = relationship("UserGroupAssignmentWaitingModel", cascade="delete") # type: ignore

View File

@ -96,32 +96,7 @@ def _run_extension(
_raise_unless_extensions_api_enabled()
process_model_identifier = _get_process_model_identifier(modified_process_model_identifier)
try:
process_model = _get_process_model(process_model_identifier)
except ApiError as ex:
if ex.error_code == "process_model_cannot_be_found":
# if process_model_identifier.startswith(current_app.config["SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX"])
raise ApiError(
error_code="invalid_process_model_extension",
message=(
f"Process Model '{process_model_identifier}' could not be found as an extension. It must be in the"
" correct Process Group:"
f" {current_app.config['SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX']}"
),
status_code=403,
) from ex
raise ex
if process_model.primary_file_name is None:
raise ApiError(
error_code="process_model_missing_primary_bpmn_file",
message=(
f"Process Model '{process_model_identifier}' does not have a primary"
" bpmn file. One must be set in order to instantiate this model."
),
status_code=400,
)
process_model = _get_process_model_or_raise(process_model_identifier)
ui_schema_action = None
persistence_level = "none"
@ -220,3 +195,32 @@ def _add_extension_group_identifier_it_not_present(process_model_identifier: str
if process_model_identifier.startswith(f"{extension_prefix}/"):
return process_model_identifier
return f"{extension_prefix}/{process_model_identifier}"
def _get_process_model_or_raise(process_model_identifier: str) -> ProcessModelInfo:
try:
process_model = _get_process_model(process_model_identifier)
except ApiError as ex:
if ex.error_code == "process_model_cannot_be_found":
# if process_model_identifier.startswith(current_app.config["SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX"])
raise ApiError(
error_code="invalid_process_model_extension",
message=(
f"Process Model '{process_model_identifier}' could not be found as an extension. It must be in the"
" correct Process Group:"
f" {current_app.config['SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX']}"
),
status_code=403,
) from ex
raise ex
if process_model.primary_file_name is None:
raise ApiError(
error_code="process_model_missing_primary_bpmn_file",
message=(
f"Process Model '{process_model_identifier}' does not have a primary"
" bpmn file. One must be set in order to instantiate this model."
),
status_code=400,
)
return process_model

View File

@ -1024,7 +1024,7 @@ def _get_tasks(
process_model_identifier_column = ProcessInstanceModel.process_model_identifier
process_instance_status_column = ProcessInstanceModel.status.label("process_instance_status") # type: ignore
user_username_column = UserModel.username.label("process_initiator_username") # type: ignore
group_identifier_column = GroupModel.identifier.label("assigned_user_group_identifier")
group_identifier_column = GroupModel.identifier.label("assigned_user_group_identifier") # type: ignore
if current_app.config["SPIFFWORKFLOW_BACKEND_DATABASE_TYPE"] == "postgres":
process_model_identifier_column = func.max(ProcessInstanceModel.process_model_identifier).label(
"process_model_identifier"

View File

@ -32,7 +32,7 @@ class GetAllPermissions(Script):
.add_columns(
PermissionAssignmentModel.permission,
PermissionTargetModel.uri,
GroupModel.identifier.label("group_identifier"),
GroupModel.identifier.label("group_identifier"), # type: ignore
)
.all()
)

View File

@ -1,7 +1,6 @@
"""Get current user."""
from typing import Any
from flask import current_app
from flask import g
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
from spiffworkflow_backend.scripts.script import Script
@ -19,5 +18,4 @@ class GetCurrentUser(Script):
def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any:
# dump the user using our json encoder and then load it back up as a dict
# to remove unwanted field types
user_as_json_string = current_app.json.dumps(g.user)
return current_app.json.loads(user_as_json_string)
return g.user.as_dict()

View File

@ -0,0 +1,30 @@
from typing import Any
from flask import current_app
from flask import g
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
from spiffworkflow_backend.scripts.script import Script
class GetGroupsForUser(Script):
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str:
return """Return the list of groups for the current user."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
groups = g.user.groups
group_items = [
group for group in groups if group.identifier != current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]
]
group_as_json_string = current_app.json.dumps(group_items)
return current_app.json.loads(group_as_json_string)

View File

@ -865,7 +865,7 @@ class AuthorizationService:
# do not remove the default user group
added_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"])
added_group_identifiers.add(SPIFF_GUEST_GROUP)
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all()
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all() # type: ignore
for gtd in groups_to_delete:
db.session.delete(gtd)

View File

@ -0,0 +1,35 @@
from flask import g
from flask.app import Flask
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
from spiffworkflow_backend.scripts.get_groups_for_user import GetGroupsForUser
from spiffworkflow_backend.services.user_service import UserService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
class TestGetGroupsForUser(BaseTest):
def test_get_groups_for_user(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
testuser1 = self.find_or_create_user("testuser1")
group1 = UserService.find_or_create_group("group1")
group2 = UserService.find_or_create_group("group2")
UserService.find_or_create_group("group3")
UserService.add_user_to_group(testuser1, group1)
UserService.add_user_to_group(testuser1, group2)
g.user = testuser1
script_attributes_context = ScriptAttributesContext(
task=None,
environment_identifier="testing",
process_instance_id=None,
process_model_identifier=None,
)
result = GetGroupsForUser().run(
script_attributes_context,
)
assert len(result) == 2
group_names = [g["identifier"] for g in result]
assert group_names == ["group1", "group2"]

View File

@ -6,10 +6,17 @@ const filterByDate = (fromDate) => {
cy.get('#date-picker-start-from').clear();
cy.get('#date-picker-start-from').type(format(fromDate, DATE_FORMAT));
cy.contains('Start date to').click();
// this can sometimes run a couple mintues after the instances are completed
// so avoid failing tests for that by setting the time as well
cy.get('#time-picker-start-from').clear();
cy.get('#time-picker-start-from').type(format(fromDate, 'HH:mm'));
cy.get('#date-picker-end-from').clear();
cy.get('#date-picker-end-from').type(format(fromDate, DATE_FORMAT));
cy.contains('End date to').click();
cy.getBySel('filter-button').click();
cy.get('#time-picker-end-from').clear();
cy.get('#time-picker-end-from').type(format(fromDate, 'HH:mm'));
};
const updateDmnText = (oldText, newText, elementId = 'wonderful_process') => {
@ -192,7 +199,6 @@ describe('process-instances', () => {
cy.get(statusSelect).click();
cy.get(statusSelect).contains(titleizeString(processStatus)).click();
clickOnHeaderToMakeSureMultiSelectComponentStateIsStable();
cy.getBySel('filter-button').click();
// make sure that there is 1 status item selected in the multiselect
cy.get(`${statusSelect} .cds--tag`).contains('1');

View File

@ -4,27 +4,42 @@ type OwnProps = {
displayLocation: string;
elementCallback: Function;
extensionUxElements?: UiSchemaUxElement[] | null;
elementCallbackIfNotFound?: Function;
};
export default function ExtensionUxElementForDisplay({
export function ExtensionUxElementMap({
displayLocation,
elementCallback,
extensionUxElements,
elementCallbackIfNotFound,
}: OwnProps) {
if (!extensionUxElements) {
return null;
}
const mainElement = () => {
return extensionUxElements.map(
let foundElement = false;
const elementMap = extensionUxElements.map(
(uxElement: UiSchemaUxElement, index: number) => {
if (uxElement.display_location === displayLocation) {
foundElement = true;
return elementCallback(uxElement, index);
}
return null;
}
);
if (!foundElement && elementCallbackIfNotFound) {
return elementCallbackIfNotFound();
}
return elementMap;
};
return <>{mainElement()}</>;
return mainElement();
}
export default function ExtensionUxElementForDisplay(args: OwnProps) {
const result = ExtensionUxElementMap(args);
if (result === null) {
return null;
}
return result;
}

View File

@ -63,16 +63,22 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
documentationUrl = DOCUMENTATION_URL;
}
const processGroupPath = '/process-groups';
const versionInfo = appVersionInfo();
useEffect(() => {
let newActiveKey = '/process-groups';
let newActiveKey = 'unknown';
if (location.pathname.match(/^\/messages\b/)) {
newActiveKey = '/messages';
} else if (location.pathname.match(/^\/process-instances\/reports\b/)) {
newActiveKey = '/process-instances/reports';
} else if (location.pathname.match(/^\/process-instances\b/)) {
newActiveKey = '/process-instances';
} else if (location.pathname.match(/^\/process-(groups|models)\b/)) {
newActiveKey = processGroupPath;
} else if (location.pathname.match(/^\/editor\b/)) {
newActiveKey = processGroupPath;
} else if (location.pathname.match(/^\/configuration\b/)) {
newActiveKey = '/configuration';
} else if (location.pathname.match(/^\/data-stores\b/)) {
@ -231,8 +237,8 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
</HeaderMenuItem>
<Can I="GET" a={targetUris.processGroupListPath} ability={ability}>
<HeaderMenuItem
href="/process-groups"
isCurrentPage={isActivePage('/process-groups')}
href={processGroupPath}
isCurrentPage={isActivePage(processGroupPath)}
data-qa="header-nav-processes"
>
Processes

View File

@ -6,7 +6,7 @@ import {
Modal,
// @ts-ignore
} from '@carbon/react';
import { ProcessInstanceReport } from '../interfaces';
import { ProcessInstanceReport, ReportMetadata } from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
@ -14,7 +14,7 @@ type OwnProps = {
buttonText?: string;
buttonClassName?: string;
processInstanceReportSelection?: ProcessInstanceReport | null;
getReportMetadataCallback: Function;
reportMetadata: ReportMetadata | null;
};
export default function ProcessInstanceListSaveAsReport({
@ -22,7 +22,7 @@ export default function ProcessInstanceListSaveAsReport({
processInstanceReportSelection,
buttonClassName,
buttonText = 'Save as Perspective',
getReportMetadataCallback,
reportMetadata,
}: OwnProps) {
const [identifier, setIdentifier] = useState<string>(
processInstanceReportSelection?.identifier || ''
@ -50,11 +50,6 @@ export default function ProcessInstanceListSaveAsReport({
const addProcessInstanceReport = (event: any) => {
event.preventDefault();
const reportMetadata = getReportMetadataCallback();
if (!reportMetadata) {
return;
}
let path = `/process-instances/reports`;
let httpMethod = 'POST';
if (isEditMode() && processInstanceReportSelection) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Tabs, TabList, Tab } from '@carbon/react';
import { SpiffTab } from '../interfaces';
type OwnProps = {
tabs: SpiffTab[];
};
export default function SpiffTabs({ tabs }: OwnProps) {
const location = useLocation();
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
useEffect(() => {
let newSelectedTabIndex = tabs.findIndex((spiffTab: SpiffTab) => {
return location.pathname === spiffTab.path;
});
if (newSelectedTabIndex === -1) {
newSelectedTabIndex = 0;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, tabs]);
const tabComponents = tabs.map((spiffTab: SpiffTab) => {
return (
<Tab onClick={() => navigate(spiffTab.path)}>{spiffTab.display_name}</Tab>
);
});
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">{tabComponents}</TabList>
</Tabs>
<br />
</>
);
}

View File

@ -1,41 +1,17 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Tabs, TabList, Tab } from '@carbon/react';
import { useLocation } from 'react-router-dom';
import { SpiffTab } from '../interfaces';
import SpiffTabs from './SpiffTabs';
export default function TaskRouteTabs() {
const location = useLocation();
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
useEffect(() => {
// Do not remove errors here, or they always get removed.
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 1;
} else if (location.pathname.match(/^\/tasks\/create-new-instance\b/)) {
newSelectedTabIndex = 2;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location]);
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
return null;
}
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
{/* <Tab onClick={() => navigate('/tasks/my-tasks')}>My Tasks</Tab> */}
<Tab onClick={() => navigate('/tasks/grouped')}>In Progress</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed
</Tab>
<Tab onClick={() => navigate('/tasks/create-new-instance')}>
Start New +
</Tab>
</TabList>
</Tabs>
<br />
</>
);
const spiffTabs: SpiffTab[] = [
{ path: '/tasks/in-progress', display_name: 'In Progress' },
{ path: '/tasks/completed-instances', display_name: 'Completed' },
{ path: '/tasks/create-new-instance', display_name: 'Start New +' },
];
return <SpiffTabs tabs={spiffTabs} />;
}

View File

@ -29,6 +29,7 @@ export interface UiSchemaForm {
export interface UiSchemaAction {
api_path: string;
ui_schema_page_components_variable?: string;
persistence_level?: UiSchemaPersistenceLevel;
process_id_to_run?: string;
results_markdown_filename?: string;
@ -76,5 +77,5 @@ export interface ExtensionApiResponse {
task_data: any;
rendered_results_markdown?: string;
redirect_to?: string;
ui_schema_page_components?: UiSchemaPageComponent[];
}

View File

@ -830,7 +830,7 @@ li.cds--accordion__item {
max-width: 100%;
}
.process-model-search-combobox li{
.process-model-search-combobox li {
max-width: 100%;
}

View File

@ -503,3 +503,13 @@ export interface KeyboardShortcut {
export interface KeyboardShortcuts {
[key: string]: KeyboardShortcut;
}
export interface SpiffTab {
path: string;
display_name: string;
}
export interface SpiffTableHeader {
tooltip_text: string;
text: string;
}

View File

@ -1,4 +1,5 @@
import { Route, Routes } from 'react-router-dom';
import { Loading } from '@carbon/react';
import Configuration from './Configuration';
import MessageListPage from './MessageListPage';
import DataStoreRoutes from './DataStoreRoutes';
@ -14,36 +15,70 @@ import Page404 from './Page404';
import AdminRedirect from './AdminRedirect';
import RootRoute from './RootRoute';
import LoginHandler from '../components/LoginHandler';
import { ExtensionUxElementMap } from '../components/ExtensionUxElementForDisplay';
import Extension from './Extension';
type OwnProps = {
extensionUxElements?: UiSchemaUxElement[] | null;
};
export default function BaseRoutes({ extensionUxElements }: OwnProps) {
const elementCallback = (uxElement: UiSchemaUxElement) => {
return (
<Route
path={uxElement.page}
element={<Extension pageIdentifier={uxElement.page} />}
/>
);
};
if (extensionUxElements) {
const extensionRoutes = ExtensionUxElementMap({
displayLocation: 'routes',
elementCallback,
extensionUxElements,
});
return (
<div className="fixed-width-container">
<ErrorDisplay />
<LoginHandler />
<Routes>
{extensionRoutes}
<Route path="/" element={<RootRoute />} />
<Route path="tasks/*" element={<HomeRoutes />} />
<Route path="process-groups/*" element={<ProcessGroupRoutes />} />
<Route path="process-models/*" element={<ProcessModelRoutes />} />
<Route
path="process-instances/*"
element={<ProcessInstanceRoutes />}
/>
<Route
path="i/:process_instance_id"
element={<ProcessInstanceShortLink />}
/>
<Route
path="configuration/*"
element={
<Configuration extensionUxElements={extensionUxElements} />
}
/>
<Route path="messages" element={<MessageListPage />} />
<Route path="data-stores/*" element={<DataStoreRoutes />} />
<Route path="about" element={<About />} />
<Route path="admin/*" element={<AdminRedirect />} />
<Route path="/*" element={<Page404 />} />
</Routes>
</div>
);
}
const style = { margin: '50px 0 50px 50px' };
return (
<div className="fixed-width-container">
<ErrorDisplay />
<LoginHandler />
<Routes>
<Route path="/" element={<RootRoute />} />
<Route path="tasks/*" element={<HomeRoutes />} />
<Route path="process-groups/*" element={<ProcessGroupRoutes />} />
<Route path="process-models/*" element={<ProcessModelRoutes />} />
<Route path="process-instances/*" element={<ProcessInstanceRoutes />} />
<Route
path="i/:process_instance_id"
element={<ProcessInstanceShortLink />}
/>
<Route
path="configuration/*"
element={<Configuration extensionUxElements={extensionUxElements} />}
/>
<Route path="messages" element={<MessageListPage />} />
<Route path="data-stores/*" element={<DataStoreRoutes />} />
<Route path="about" element={<About />} />
<Route path="admin/*" element={<AdminRedirect />} />
<Route path="/*" element={<Page404 />} />
</Routes>
</div>
<Loading
description="Active loading indicator"
withOverlay={false}
style={style}
/>
);
}

View File

@ -20,30 +20,27 @@ export default function CompletedInstances() {
return userGroups.map((userGroup: string) => {
const titleText = `This is a list of instances with tasks that were completed by the ${userGroup} group.`;
const headerElement = (
<h2 title={titleText} className="process-instance-table-header">
Instances with tasks completed by <strong>{userGroup}</strong>
</h2>
);
const headerElement = {
tooltip_text: titleText,
text: `Instances with tasks completed by **${userGroup}**`,
};
const identifierForTable = `completed-by-group-${slugifyString(
userGroup
)}`;
return (
<ProcessInstanceListTable
headerElement={headerElement}
tableHtmlId={identifierForTable}
showLinkToReport
filtersEnabled={false}
paginationQueryParamPrefix="group_completed_instances"
paginationClassName="with-large-bottom-margin"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_completed_instances"
showReports={false}
textToShowIfEmpty="This group has no completed instances at this time."
additionalReportFilters={[
{ field_name: 'user_group_identifier', field_value: userGroup },
]}
header={headerElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix="group_completed_instances"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_completed_instances"
showActionsColumn
showLinkToReport
tableHtmlId={identifierForTable}
textToShowIfEmpty="This group has no completed instances at this time."
/>
);
});
@ -51,50 +48,40 @@ export default function CompletedInstances() {
const startedByMeTitleText =
'This is a list of instances you started that are now complete.';
const startedByMeHeaderElement = (
<h2 title={startedByMeTitleText} className="process-instance-table-header">
Started by me
</h2>
);
const startedByMeHeaderElement = {
tooltip_text: startedByMeTitleText,
text: 'Started by me',
};
const withTasksCompletedByMeTitleText =
'This is a list of instances where you have completed tasks.';
const withTasksHeaderElement = (
<h2
title={withTasksCompletedByMeTitleText}
className="process-instance-table-header"
>
Instances with tasks completed by me
</h2>
);
const withTasksHeaderElement = {
tooltip_text: withTasksCompletedByMeTitleText,
text: 'Instances with tasks completed by me',
};
return (
<>
<ProcessInstanceListTable
headerElement={startedByMeHeaderElement}
tableHtmlId="my-completed-instances"
showLinkToReport
filtersEnabled={false}
header={startedByMeHeaderElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix="my_completed_instances"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_completed_instances_initiated_by_me"
showReports={false}
textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin"
autoReload
showActionsColumn
showLinkToReport
tableHtmlId="my-completed-instances"
textToShowIfEmpty="You have no completed instances at this time."
/>
<ProcessInstanceListTable
headerElement={withTasksHeaderElement}
tableHtmlId="with-tasks-completed-by-me"
showLinkToReport
filtersEnabled={false}
header={withTasksHeaderElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix="my_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_completed_instances_with_tasks_completed_by_me"
showReports={false}
textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin"
showActionsColumn
showLinkToReport
tableHtmlId="with-tasks-completed-by-me"
textToShowIfEmpty="You have no completed instances at this time."
/>
{groupTableComponents()}
</>

View File

@ -14,6 +14,7 @@ import { recursivelyChangeNullAndUndefined } from '../helpers';
import CustomForm from '../components/CustomForm';
import { BACKEND_BASE_URL } from '../config';
import {
ExtensionApiResponse,
ExtensionPostBody,
ExtensionUiSchema,
UiSchemaPageComponent,
@ -24,13 +25,20 @@ import FormattingService from '../services/FormattingService';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
import MarkdownRenderer from '../components/MarkdownRenderer';
import LoginHandler from '../components/LoginHandler';
import SpiffTabs from '../components/SpiffTabs';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import CreateNewInstance from './CreateNewInstance';
type OwnProps = {
pageIdentifier?: string;
displayErrors?: boolean;
};
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Extension({ displayErrors = true }: OwnProps) {
export default function Extension({
pageIdentifier,
displayErrors = true,
}: OwnProps) {
const { targetUris } = useUriListForPermissions();
const params = useParams();
const [searchParams] = useSearchParams();
@ -52,12 +60,18 @@ export default function Extension({ displayErrors = true }: OwnProps) {
useState<UiSchemaPageDefinition | null>(null);
const [readyForComponentsToDisplay, setReadyForComponentsToDisplay] =
useState<boolean>(false);
const [uiSchemaPageComponents, setuiSchemaPageComponents] = useState<
UiSchemaPageComponent[] | null
>(null);
const { addError, removeError } = useAPIError();
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
ProcessInstanceRun,
CreateNewInstance,
MarkdownRenderer,
ProcessInstanceListTable,
ProcessInstanceRun,
SpiffTabs,
};
const interpolateNavigationString = useCallback(
@ -83,9 +97,8 @@ export default function Extension({ displayErrors = true }: OwnProps) {
[]
);
const processLoadResult = useCallback(
(result: any, pageDefinition: UiSchemaPageDefinition) => {
(result: ExtensionApiResponse, pageDefinition: UiSchemaPageDefinition) => {
setFormData(result.task_data);
console.log('pageDefinition', pageDefinition);
if (pageDefinition.navigate_to_on_load) {
const optionString = interpolateNavigationString(
pageDefinition.navigate_to_on_load,
@ -101,6 +114,16 @@ export default function Extension({ displayErrors = true }: OwnProps) {
);
setMarkdownToRenderOnLoad(newMarkdown);
}
if (
pageDefinition.on_load &&
pageDefinition.on_load.ui_schema_page_components_variable
) {
setuiSchemaPageComponents(
result.task_data[
pageDefinition.on_load.ui_schema_page_components_variable
]
);
}
setReadyForComponentsToDisplay(true);
},
[interpolateNavigationString]
@ -117,13 +140,17 @@ export default function Extension({ displayErrors = true }: OwnProps) {
(extensionUiSchemaFile as any).file_contents
);
const pageIdentifier = `/${params.page_identifier}`;
let pageIdentifierToUse = pageIdentifier;
if (!pageIdentifierToUse) {
pageIdentifierToUse = `/${params.page_identifier}`;
}
if (
extensionUiSchema.pages &&
Object.keys(extensionUiSchema.pages).includes(pageIdentifier)
Object.keys(extensionUiSchema.pages).includes(pageIdentifierToUse)
) {
const pageDefinition = extensionUiSchema.pages[pageIdentifier];
const pageDefinition = extensionUiSchema.pages[pageIdentifierToUse];
setUiSchemaPageDefinition(pageDefinition);
setuiSchemaPageComponents(pageDefinition.components || null);
setProcessModel(pm);
pm.files.forEach((file: ProcessFile) => {
filesByName[file.name] = file;
@ -144,7 +171,7 @@ export default function Extension({ displayErrors = true }: OwnProps) {
postBody.ui_schema_action = pageDefinition.on_load;
HttpService.makeCallToBackend({
path: `${targetUris.extensionListPath}/${pageDefinition.on_load.api_path}`,
successCallback: (result: any) =>
successCallback: (result: ExtensionApiResponse) =>
processLoadResult(result, pageDefinition),
httpMethod: 'POST',
postBody,
@ -161,6 +188,7 @@ export default function Extension({ displayErrors = true }: OwnProps) {
searchParams,
filesByName,
processLoadResult,
pageIdentifier,
]
);
@ -187,7 +215,7 @@ export default function Extension({ displayErrors = true }: OwnProps) {
targetUris.extensionPath,
]);
const processSubmitResult = (result: any) => {
const processSubmitResult = (result: ExtensionApiResponse) => {
if (
uiSchemaPageDefinition &&
uiSchemaPageDefinition.navigate_to_on_form_submit
@ -301,21 +329,19 @@ export default function Extension({ displayErrors = true }: OwnProps) {
);
}
if (uiSchemaPageDefinition.components) {
uiSchemaPageDefinition.components.forEach(
(component: UiSchemaPageComponent) => {
if (supportedComponents[component.name]) {
const argumentsForComponent: any = component.arguments;
componentsToDisplay.push(
supportedComponents[component.name](argumentsForComponent)
);
} else {
console.error(
`Extension tried to use component with name '${component.name}' but that is not allowed.`
);
}
if (uiSchemaPageComponents) {
uiSchemaPageComponents.forEach((component: UiSchemaPageComponent) => {
if (supportedComponents[component.name]) {
const argumentsForComponent: any = component.arguments;
componentsToDisplay.push(
supportedComponents[component.name](argumentsForComponent)
);
} else {
console.error(
`Extension tried to use component with name '${component.name}' but that is not allowed.`
);
}
);
});
}
const uiSchemaForm = uiSchemaPageDefinition.form;

View File

@ -16,7 +16,7 @@ export default function HomeRoutes() {
<Route path="/" element={<InProgressInstances />} />
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<InProgressInstances />} />
<Route path="in-progress" element={<InProgressInstances />} />
<Route path="completed-instances" element={<CompletedInstances />} />
<Route path="create-new-instance" element={<CreateNewInstance />} />
</Routes>

View File

@ -20,30 +20,26 @@ export default function InProgressInstances() {
return userGroups.map((userGroup: string) => {
const titleText = `This is a list of instances with tasks that are waiting for the ${userGroup} group.`;
const headerElement = (
<h2 title={titleText} className="process-instance-table-header">
Waiting for <strong>{userGroup}</strong>
</h2>
);
const headerElement = {
tooltip_text: titleText,
text: `Waiting for **${userGroup}**`,
};
const identifierForTable = `waiting-for-${slugifyString(userGroup)}`;
return (
<ProcessInstanceListTable
headerElement={headerElement}
tableHtmlId={identifierForTable}
showLinkToReport
filtersEnabled={false}
paginationQueryParamPrefix={identifierForTable.replace('-', '_')}
paginationClassName="with-large-bottom-margin"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_in_progress_instances_with_tasks"
showReports={false}
textToShowIfEmpty="This group has no instances waiting on it at this time."
additionalReportFilters={[
{ field_name: 'user_group_identifier', field_value: userGroup },
]}
canCompleteAllTasks
showActionsColumn
autoReload
header={headerElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix={identifierForTable.replace('-', '_')}
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_in_progress_instances_with_tasks"
showActionsColumn
showLinkToReport
tableHtmlId={identifierForTable}
textToShowIfEmpty="This group has no instances waiting on it at this time."
/>
);
});
@ -51,50 +47,43 @@ export default function InProgressInstances() {
const startedByMeTitleText =
'This is a list of open instances that you started.';
const startedByMeHeaderElement = (
<h2 title={startedByMeTitleText} className="process-instance-table-header">
Started by me
</h2>
);
const startedByMeHeaderElement = {
tooltip_text: startedByMeTitleText,
text: 'Started by me',
};
const waitingForMeTitleText =
'This is a list of instances that have tasks that you can complete.';
const waitingForMeHeaderElement = (
<h2 title={waitingForMeTitleText} className="process-instance-table-header">
Waiting for me
</h2>
);
const waitingForMeHeaderElement = {
tooltip_text: waitingForMeTitleText,
text: 'Waiting for me',
};
return (
<>
<ProcessInstanceListTable
headerElement={startedByMeHeaderElement}
tableHtmlId="open-instances-started-by-me"
filtersEnabled={false}
autoReload
header={startedByMeHeaderElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix="open_instances_started_by_me"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_in_progress_instances_initiated_by_me"
showReports={false}
textToShowIfEmpty="There are no open instances you started at this time."
paginationClassName="with-large-bottom-margin"
showLinkToReport
showActionsColumn
autoReload
showLinkToReport
tableHtmlId="open-instances-started-by-me"
textToShowIfEmpty="There are no open instances you started at this time."
/>
<ProcessInstanceListTable
headerElement={waitingForMeHeaderElement}
tableHtmlId="waiting-for-me"
showLinkToReport
filtersEnabled={false}
autoReload
header={waitingForMeHeaderElement}
paginationClassName="with-large-bottom-margin"
paginationQueryParamPrefix="waiting_for_me"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_in_progress_instances_with_tasks_for_me"
showReports={false}
textToShowIfEmpty="There are no instances waiting on you at this time."
paginationClassName="with-large-bottom-margin"
canCompleteAllTasks
showActionsColumn
autoReload
showLinkToReport
tableHtmlId="waiting-for-me"
textToShowIfEmpty="There are no instances waiting on you at this time."
/>
{groupTableComponents()}
</>

View File

@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
import 'react-datepicker/dist/react-datepicker.css';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import ProcessInstanceListTableWithFilters from '../components/ProcessInstanceListTableWithFilters';
import {
getProcessModelFullIdentifierFromSearchParams,
setPageTitle,
@ -53,7 +53,10 @@ export default function ProcessInstanceList({ variant }: OwnProps) {
<br />
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
<ProcessInstanceListTable variant={variant} showActionsColumn />
<ProcessInstanceListTableWithFilters
variant={variant}
showActionsColumn
/>
</>
);
}

View File

@ -40,13 +40,13 @@ import {
} from '../helpers';
import { PermissionsToCheck, ProcessFile, ProcessModel } from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
import { Notification } from '../components/Notification';
import ProcessModelTestRun from '../components/ProcessModelTestRun';
import MarkdownDisplayForFile from '../components/MarkdownDisplayForFile';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
export default function ProcessModelShow() {
const params = useParams();
@ -680,9 +680,6 @@ export default function ProcessModelShow() {
ability={ability}
>
<ProcessInstanceListTable
filtersEnabled={false}
showLinkToReport
variant="for-me"
additionalReportFilters={[
{
field_name: 'process_model_identifier',
@ -690,7 +687,8 @@ export default function ProcessModelShow() {
},
]}
perPageOptions={[2, 5, 25]}
showReports={false}
showLinkToReport
variant="for-me"
/>
</Can>
)}

View File

@ -1,6 +1,6 @@
import TaskRouteTabs from '../components/TaskRouteTabs';
import InProgressInstances from './InProgressInstances';
import OnboardingView from './OnboardingView';
import TaskRouteTabs from '../components/TaskRouteTabs';
export default function RootRoute() {
return (