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:
parent
4758634c99
commit
f0f4bcce12
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
File diff suppressed because it is too large
Load Diff
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -830,7 +830,7 @@ li.cds--accordion__item {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.process-model-search-combobox li{
|
||||
.process-model-search-combobox li {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
||||
</>
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue