mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-01-12 18:44:14 +00:00
Feature/user preference extension (#472)
* added ability to display navigation items in user profile toggle * updated naming of some extension elements * added user property table and updates for extensions to use it w/ burnettk * moved extension ui interfaces to own file and linting issues * some updates to render markdown results on load w/ burnettk * added migration merge file w/ burnettk * moved code to fix linting issues w/ burnettk * resolved db migration conflict * removed unnecessary migrations and added just one w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
655d384645
commit
4cf33b62fc
@ -28,8 +28,8 @@ if [[ -n "${SPIFFWORKFLOW_BACKEND_DATABASE_URI:-}" ]]; then
|
|||||||
database_host=$(grep -oP "^[^:]+://.*@\K(.+?)[:/]" <<<"$SPIFFWORKFLOW_BACKEND_DATABASE_URI" | sed -E 's/[:\/]$//')
|
database_host=$(grep -oP "^[^:]+://.*@\K(.+?)[:/]" <<<"$SPIFFWORKFLOW_BACKEND_DATABASE_URI" | sed -E 's/[:\/]$//')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# this will fix branching conflicts
|
# uncomment this line to fix branching conflicts
|
||||||
# poetry run flask db merge heads -m "merging two heads"
|
# poetry run flask db merge heads -m "merging heads"
|
||||||
|
|
||||||
tasks=""
|
tasks=""
|
||||||
if [[ "${1:-}" == "clean" ]]; then
|
if [[ "${1:-}" == "clean" ]]; then
|
||||||
|
44
spiffworkflow-backend/migrations/versions/844cee572018_.py
Normal file
44
spiffworkflow-backend/migrations/versions/844cee572018_.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 844cee572018
|
||||||
|
Revises: 57df21dc569d
|
||||||
|
Create Date: 2023-09-07 14:18:12.357989
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '844cee572018'
|
||||||
|
down_revision = '57df21dc569d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('user_property',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('key', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('value', sa.String(length=255), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id', 'key', name='user_id_key_uniq')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('user_property', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_user_property_key'), ['key'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_user_property_user_id'), ['user_id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user_property', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_user_property_user_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_user_property_key'))
|
||||||
|
|
||||||
|
op.drop_table('user_property')
|
||||||
|
# ### end Alembic commands ###
|
@ -88,5 +88,8 @@ from spiffworkflow_backend.models.task_draft_data import (
|
|||||||
from spiffworkflow_backend.models.configuration import (
|
from spiffworkflow_backend.models.configuration import (
|
||||||
ConfigurationModel,
|
ConfigurationModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
|
from spiffworkflow_backend.models.user_property import (
|
||||||
|
UserPropertyModel,
|
||||||
|
) # noqa: F401
|
||||||
|
|
||||||
add_listeners()
|
add_listeners()
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||||
|
from spiffworkflow_backend.models.db import db
|
||||||
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserPropertyModel(SpiffworkflowBaseDBModel):
|
||||||
|
__tablename__ = "user_property"
|
||||||
|
__table_args__ = (db.UniqueConstraint("user_id", "key", name="user_id_key_uniq"),)
|
||||||
|
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) # type: ignore
|
||||||
|
key: str = db.Column(db.String(255), nullable=False, index=True)
|
||||||
|
value: str | None = db.Column(db.String(255))
|
@ -21,6 +21,7 @@ from spiffworkflow_backend.services.process_instance_processor import CustomBpmn
|
|||||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
|
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
|
||||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsNotEnqueuedError
|
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsNotEnqueuedError
|
||||||
|
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||||
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
||||||
@ -121,17 +122,25 @@ def _run_extension(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
ui_schema_page_definition = None
|
ui_schema_action = None
|
||||||
if body and "ui_schema_page_definition" in body:
|
persistence_level = "none"
|
||||||
ui_schema_page_definition = body["ui_schema_page_definition"]
|
if body and "ui_schema_action" in body:
|
||||||
|
ui_schema_action = body["ui_schema_action"]
|
||||||
|
persistence_level = ui_schema_action.get("persistence_level", "none")
|
||||||
|
|
||||||
process_instance = ProcessInstanceModel(
|
process_instance = None
|
||||||
status=ProcessInstanceStatus.not_started.value,
|
if persistence_level == "none":
|
||||||
process_initiator_id=g.user.id,
|
process_instance = ProcessInstanceModel(
|
||||||
process_model_identifier=process_model.id,
|
status=ProcessInstanceStatus.not_started.value,
|
||||||
process_model_display_name=process_model.display_name,
|
process_initiator_id=g.user.id,
|
||||||
persistence_level="none",
|
process_model_identifier=process_model.id,
|
||||||
)
|
process_model_display_name=process_model.display_name,
|
||||||
|
persistence_level=persistence_level,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||||
|
process_model_identifier, g.user
|
||||||
|
)
|
||||||
|
|
||||||
processor = None
|
processor = None
|
||||||
try:
|
try:
|
||||||
@ -170,10 +179,10 @@ def _run_extension(
|
|||||||
task_data = processor.get_data()
|
task_data = processor.get_data()
|
||||||
result: dict[str, Any] = {"task_data": task_data}
|
result: dict[str, Any] = {"task_data": task_data}
|
||||||
|
|
||||||
if ui_schema_page_definition:
|
if ui_schema_action:
|
||||||
if "results_markdown_filename" in ui_schema_page_definition:
|
if "results_markdown_filename" in ui_schema_action:
|
||||||
file_contents = SpecFileService.get_data(
|
file_contents = SpecFileService.get_data(
|
||||||
process_model, ui_schema_page_definition["results_markdown_filename"]
|
process_model, ui_schema_action["results_markdown_filename"]
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
form_contents = JinjaService.render_jinja_template(file_contents, task_data=task_data)
|
form_contents = JinjaService.render_jinja_template(file_contents, task_data=task_data)
|
||||||
result["rendered_results_markdown"] = form_contents
|
result["rendered_results_markdown"] = form_contents
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
|
||||||
|
from spiffworkflow_backend.models.user_property import UserPropertyModel
|
||||||
|
from spiffworkflow_backend.scripts.script import Script
|
||||||
|
|
||||||
|
|
||||||
|
class GetUserProperties(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 """Gets the user properties for current user."""
|
||||||
|
|
||||||
|
def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any:
|
||||||
|
user_properties = UserPropertyModel.query.filter_by(user_id=g.user.id).all()
|
||||||
|
dict_to_return = {}
|
||||||
|
for up in user_properties:
|
||||||
|
dict_to_return[up.key] = up.value
|
||||||
|
return dict_to_return
|
@ -31,6 +31,10 @@ class ProcessModelIdentifierMissingError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidArgsGivenToScriptError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Script:
|
class Script:
|
||||||
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
|
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
|
||||||
|
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from spiffworkflow_backend.models.db import db
|
||||||
|
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
|
||||||
|
from spiffworkflow_backend.models.user_property import UserPropertyModel
|
||||||
|
from spiffworkflow_backend.scripts.script import InvalidArgsGivenToScriptError
|
||||||
|
from spiffworkflow_backend.scripts.script import Script
|
||||||
|
|
||||||
|
|
||||||
|
class SetUserProperties(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 """Sets given user properties on current user."""
|
||||||
|
|
||||||
|
def run(self, script_attributes_context: ScriptAttributesContext, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
properties = args[0]
|
||||||
|
if not isinstance(properties, dict):
|
||||||
|
raise InvalidArgsGivenToScriptError(
|
||||||
|
f"Args to set_user_properties must be a dict. '{properties}' is invalid."
|
||||||
|
)
|
||||||
|
# consider using engine-specific insert or update metaphor in future: https://stackoverflow.com/a/68431412/6090676
|
||||||
|
for property_key, property_value in properties.items():
|
||||||
|
user_property = UserPropertyModel.query.filter_by(user_id=g.user.id, key=property_key).first()
|
||||||
|
if user_property is None:
|
||||||
|
user_property = UserPropertyModel(
|
||||||
|
user_id=g.user.id,
|
||||||
|
key=property_key,
|
||||||
|
)
|
||||||
|
user_property.value = property_value
|
||||||
|
db.session.add(user_property)
|
||||||
|
db.session.commit()
|
@ -48,11 +48,7 @@ export default function App() {
|
|||||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||||
<Route path="/editor/*" element={<EditorRoutes />} />
|
<Route path="/editor/*" element={<EditorRoutes />} />
|
||||||
<Route
|
<Route
|
||||||
path="/extensions/:process_model"
|
path="/extensions/:page_identifier"
|
||||||
element={<Extension />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/extensions/:process_model/:extension_route"
|
|
||||||
element={<Extension />}
|
element={<Extension />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -24,13 +24,11 @@ import { Can } from '@casl/react';
|
|||||||
import logo from '../logo.svg';
|
import logo from '../logo.svg';
|
||||||
import UserService from '../services/UserService';
|
import UserService from '../services/UserService';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
|
import { PermissionsToCheck, ProcessModel, ProcessFile } from '../interfaces';
|
||||||
import {
|
import {
|
||||||
PermissionsToCheck,
|
|
||||||
ProcessModel,
|
|
||||||
ProcessFile,
|
|
||||||
ExtensionUiSchema,
|
ExtensionUiSchema,
|
||||||
UiSchemaNavItem,
|
UiSchemaUxElement,
|
||||||
} from '../interfaces';
|
} from '../extension_ui_schema_interfaces';
|
||||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||||
import HttpService, { UnauthenticatedError } from '../services/HttpService';
|
import HttpService, { UnauthenticatedError } from '../services/HttpService';
|
||||||
import { DOCUMENTATION_URL, SPIFF_ENVIRONMENT } from '../config';
|
import { DOCUMENTATION_URL, SPIFF_ENVIRONMENT } from '../config';
|
||||||
@ -49,7 +47,7 @@ export default function NavigationBar() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [activeKey, setActiveKey] = useState<string>('');
|
const [activeKey, setActiveKey] = useState<string>('');
|
||||||
const [extensionNavigationItems, setExtensionNavigationItems] = useState<
|
const [extensionNavigationItems, setExtensionNavigationItems] = useState<
|
||||||
UiSchemaNavItem[] | null
|
UiSchemaUxElement[] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const { targetUris } = useUriListForPermissions();
|
const { targetUris } = useUriListForPermissions();
|
||||||
@ -109,7 +107,7 @@ export default function NavigationBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const processExtensionResult = (processModels: ProcessModel[]) => {
|
const processExtensionResult = (processModels: ProcessModel[]) => {
|
||||||
const eni: UiSchemaNavItem[] = processModels
|
const eni: UiSchemaUxElement[] = processModels
|
||||||
.map((processModel: ProcessModel) => {
|
.map((processModel: ProcessModel) => {
|
||||||
const extensionUiSchemaFile = processModel.files.find(
|
const extensionUiSchemaFile = processModel.files.find(
|
||||||
(file: ProcessFile) => file.name === 'extension_uischema.json'
|
(file: ProcessFile) => file.name === 'extension_uischema.json'
|
||||||
@ -119,8 +117,8 @@ export default function NavigationBar() {
|
|||||||
const extensionUiSchema: ExtensionUiSchema = JSON.parse(
|
const extensionUiSchema: ExtensionUiSchema = JSON.parse(
|
||||||
extensionUiSchemaFile.file_contents
|
extensionUiSchemaFile.file_contents
|
||||||
);
|
);
|
||||||
if (extensionUiSchema.navigation_items) {
|
if (extensionUiSchema.ux_elements) {
|
||||||
return extensionUiSchema.navigation_items;
|
return extensionUiSchema.ux_elements;
|
||||||
}
|
}
|
||||||
} catch (jsonParseError: any) {
|
} catch (jsonParseError: any) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -128,7 +126,7 @@ export default function NavigationBar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [] as UiSchemaNavItem[];
|
return [] as UiSchemaUxElement[];
|
||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
if (eni) {
|
if (eni) {
|
||||||
@ -157,6 +155,27 @@ export default function NavigationBar() {
|
|||||||
const userEmail = UserService.getUserEmail();
|
const userEmail = UserService.getUserEmail();
|
||||||
const username = UserService.getPreferredUsername();
|
const username = UserService.getPreferredUsername();
|
||||||
|
|
||||||
|
const extensionNavigationElementsForDisplayLocation = (
|
||||||
|
displayLocation: string,
|
||||||
|
elementCallback: Function
|
||||||
|
) => {
|
||||||
|
if (!extensionNavigationItems) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionNavigationItems.map((uxElement: UiSchemaUxElement) => {
|
||||||
|
if (uxElement.display_location === displayLocation) {
|
||||||
|
return elementCallback(uxElement);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const extensionUserProfileElement = (uxElement: UiSchemaUxElement) => {
|
||||||
|
const navItemPage = `/extensions${uxElement.page}`;
|
||||||
|
return <a href={navItemPage}>{uxElement.label}</a>;
|
||||||
|
};
|
||||||
|
|
||||||
const profileToggletip = (
|
const profileToggletip = (
|
||||||
<div style={{ display: 'flex' }} id="user-profile-toggletip">
|
<div style={{ display: 'flex' }} id="user-profile-toggletip">
|
||||||
<Toggletip isTabTip align="bottom-right">
|
<Toggletip isTabTip align="bottom-right">
|
||||||
@ -177,6 +196,10 @@ export default function NavigationBar() {
|
|||||||
<a target="_blank" href={documentationUrl} rel="noreferrer">
|
<a target="_blank" href={documentationUrl} rel="noreferrer">
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
|
{extensionNavigationElementsForDisplayLocation(
|
||||||
|
'user_profile_item',
|
||||||
|
extensionUserProfileElement
|
||||||
|
)}
|
||||||
{!UserService.authenticationDisabled() ? (
|
{!UserService.authenticationDisabled() ? (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
@ -258,27 +281,21 @@ export default function NavigationBar() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const extensionNavigationElements = () => {
|
const extensionHeaderMenuItemElement = (uxElement: UiSchemaUxElement) => {
|
||||||
if (!extensionNavigationItems) {
|
const navItemPage = `/extensions${uxElement.page}`;
|
||||||
return null;
|
const regexp = new RegExp(`^${navItemPage}$`);
|
||||||
|
if (regexp.test(location.pathname)) {
|
||||||
|
setActiveKey(navItemPage);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
return extensionNavigationItems.map((navItem: UiSchemaNavItem) => {
|
<HeaderMenuItem
|
||||||
const navItemRoute = `/extensions${navItem.route}`;
|
href={navItemPage}
|
||||||
const regexp = new RegExp(`^${navItemRoute}`);
|
isCurrentPage={isActivePage(navItemPage)}
|
||||||
if (regexp.test(location.pathname)) {
|
data-qa={`extension-${slugifyString(uxElement.label)}`}
|
||||||
setActiveKey(navItemRoute);
|
>
|
||||||
}
|
{uxElement.label}
|
||||||
return (
|
</HeaderMenuItem>
|
||||||
<HeaderMenuItem
|
);
|
||||||
href={navItemRoute}
|
|
||||||
isCurrentPage={isActivePage(navItemRoute)}
|
|
||||||
data-qa={`extension-${slugifyString(navItem.label)}`}
|
|
||||||
>
|
|
||||||
{navItem.label}
|
|
||||||
</HeaderMenuItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerMenuItems = () => {
|
const headerMenuItems = () => {
|
||||||
@ -328,7 +345,10 @@ export default function NavigationBar() {
|
|||||||
</HeaderMenuItem>
|
</HeaderMenuItem>
|
||||||
</Can>
|
</Can>
|
||||||
{configurationElement()}
|
{configurationElement()}
|
||||||
{extensionNavigationElements()}
|
{extensionNavigationElementsForDisplayLocation(
|
||||||
|
'header_menu_item',
|
||||||
|
extensionHeaderMenuItemElement
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
49
spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts
Normal file
49
spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
export enum UiSchemaDisplayLocation {
|
||||||
|
header_menu_item = 'header_menu_item',
|
||||||
|
user_profile_item = 'user_profile_item',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UiSchemaPersistenceLevel {
|
||||||
|
full = 'full',
|
||||||
|
none = 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiSchemaUxElement {
|
||||||
|
label: string;
|
||||||
|
page: string;
|
||||||
|
display_location: UiSchemaDisplayLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiSchemaAction {
|
||||||
|
api_path: string;
|
||||||
|
|
||||||
|
persistence_level?: UiSchemaPersistenceLevel;
|
||||||
|
navigate_to_on_form_submit?: string;
|
||||||
|
results_markdown_filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiSchemaPageDefinition {
|
||||||
|
header: string;
|
||||||
|
api: string;
|
||||||
|
|
||||||
|
on_load?: UiSchemaAction;
|
||||||
|
on_form_submit?: UiSchemaAction;
|
||||||
|
form_schema_filename?: any;
|
||||||
|
form_ui_schema_filename?: any;
|
||||||
|
markdown_instruction_filename?: string;
|
||||||
|
navigate_to_on_form_submit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiSchemaPage {
|
||||||
|
[key: string]: UiSchemaPageDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionUiSchema {
|
||||||
|
ux_elements?: UiSchemaUxElement[];
|
||||||
|
pages: UiSchemaPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionPostBody {
|
||||||
|
extension_input: any;
|
||||||
|
ui_schema_action?: UiSchemaAction;
|
||||||
|
}
|
@ -435,29 +435,3 @@ export interface DataStore {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UiSchemaNavItem {
|
|
||||||
label: string;
|
|
||||||
route: string;
|
|
||||||
}
|
|
||||||
export interface UiSchemaPageDefinition {
|
|
||||||
header: string;
|
|
||||||
api: string;
|
|
||||||
|
|
||||||
form_schema_filename?: any;
|
|
||||||
form_ui_schema_filename?: any;
|
|
||||||
markdown_instruction_filename?: string;
|
|
||||||
navigate_to_on_form_submit?: string;
|
|
||||||
}
|
|
||||||
export interface UiSchemaRoute {
|
|
||||||
[key: string]: UiSchemaPageDefinition;
|
|
||||||
}
|
|
||||||
export interface ExtensionUiSchema {
|
|
||||||
navigation_items?: UiSchemaNavItem[];
|
|
||||||
routes: UiSchemaRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExtensionPostBody {
|
|
||||||
extension_input: any;
|
|
||||||
ui_schema_page_definition?: UiSchemaPageDefinition;
|
|
||||||
}
|
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import MDEditor from '@uiw/react-md-editor';
|
import MDEditor from '@uiw/react-md-editor';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Editor } from '@monaco-editor/react';
|
import { Editor } from '@monaco-editor/react';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
import {
|
import { ProcessFile, ProcessModel } from '../interfaces';
|
||||||
ExtensionPostBody,
|
|
||||||
ExtensionUiSchema,
|
|
||||||
ProcessFile,
|
|
||||||
ProcessModel,
|
|
||||||
UiSchemaPageDefinition,
|
|
||||||
} from '../interfaces';
|
|
||||||
import HttpService from '../services/HttpService';
|
import HttpService from '../services/HttpService';
|
||||||
import useAPIError from '../hooks/UseApiError';
|
import useAPIError from '../hooks/UseApiError';
|
||||||
import { recursivelyChangeNullAndUndefined } from '../helpers';
|
import { recursivelyChangeNullAndUndefined } from '../helpers';
|
||||||
import CustomForm from '../components/CustomForm';
|
import CustomForm from '../components/CustomForm';
|
||||||
import { BACKEND_BASE_URL } from '../config';
|
import { BACKEND_BASE_URL } from '../config';
|
||||||
|
import {
|
||||||
|
ExtensionPostBody,
|
||||||
|
ExtensionUiSchema,
|
||||||
|
UiSchemaPageDefinition,
|
||||||
|
} from '../extension_ui_schema_interfaces';
|
||||||
import ErrorDisplay from '../components/ErrorDisplay';
|
import ErrorDisplay from '../components/ErrorDisplay';
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@ -26,7 +25,12 @@ export default function Extension() {
|
|||||||
const [formData, setFormData] = useState<any>(null);
|
const [formData, setFormData] = useState<any>(null);
|
||||||
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
|
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
|
||||||
const [processedTaskData, setProcessedTaskData] = useState<any>(null);
|
const [processedTaskData, setProcessedTaskData] = useState<any>(null);
|
||||||
const [markdownToRender, setMarkdownToRender] = useState<string | null>(null);
|
const [markdownToRenderOnSubmit, setMarkdownToRenderOnSubmit] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [markdownToRenderOnLoad, setMarkdownToRenderOnLoad] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
const [filesByName] = useState<{
|
const [filesByName] = useState<{
|
||||||
[key: string]: ProcessFile;
|
[key: string]: ProcessFile;
|
||||||
}>({});
|
}>({});
|
||||||
@ -35,18 +39,15 @@ export default function Extension() {
|
|||||||
|
|
||||||
const { addError, removeError } = useAPIError();
|
const { addError, removeError } = useAPIError();
|
||||||
|
|
||||||
useEffect(() => {
|
const setConfigsIfDesiredSchemaFile = useCallback(
|
||||||
const processExtensionResult = (pm: ProcessModel) => {
|
(extensionUiSchemaFile: ProcessFile | null, pm: ProcessModel) => {
|
||||||
setProcessModel(pm);
|
const processLoadResult = (result: any) => {
|
||||||
let extensionUiSchemaFile: ProcessFile | null = null;
|
setFormData(result.task_data);
|
||||||
pm.files.forEach((file: ProcessFile) => {
|
if (result.rendered_results_markdown) {
|
||||||
filesByName[file.name] = file;
|
setMarkdownToRenderOnLoad(result.rendered_results_markdown);
|
||||||
if (file.name === 'extension_uischema.json') {
|
|
||||||
extensionUiSchemaFile = file;
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// typescript is really confused by extensionUiSchemaFile so force it since we are properly checking
|
|
||||||
if (
|
if (
|
||||||
extensionUiSchemaFile &&
|
extensionUiSchemaFile &&
|
||||||
(extensionUiSchemaFile as ProcessFile).file_contents
|
(extensionUiSchemaFile as ProcessFile).file_contents
|
||||||
@ -55,24 +56,61 @@ export default function Extension() {
|
|||||||
(extensionUiSchemaFile as any).file_contents
|
(extensionUiSchemaFile as any).file_contents
|
||||||
);
|
);
|
||||||
|
|
||||||
let routeIdentifier = `/${params.process_model}`;
|
const pageIdentifier = `/${params.page_identifier}`;
|
||||||
if (params.extension_route) {
|
if (
|
||||||
routeIdentifier = `${routeIdentifier}/${params.extension_route}`;
|
extensionUiSchema.pages &&
|
||||||
|
Object.keys(extensionUiSchema.pages).includes(pageIdentifier)
|
||||||
|
) {
|
||||||
|
const pageDefinition = extensionUiSchema.pages[pageIdentifier];
|
||||||
|
setUiSchemaPageDefinition(pageDefinition);
|
||||||
|
setProcessModel(pm);
|
||||||
|
|
||||||
|
const postBody: ExtensionPostBody = { extension_input: {} };
|
||||||
|
postBody.ui_schema_action = pageDefinition.on_load;
|
||||||
|
if (pageDefinition.on_load) {
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `${targetUris.extensionListPath}/${pageDefinition.on_load.api_path}`,
|
||||||
|
successCallback: processLoadResult,
|
||||||
|
httpMethod: 'POST',
|
||||||
|
postBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setUiSchemaPageDefinition(extensionUiSchema.routes[routeIdentifier]);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[targetUris.extensionListPath, params]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processExtensionResult = (processModels: ProcessModel[]) => {
|
||||||
|
processModels.forEach((pm: ProcessModel) => {
|
||||||
|
let extensionUiSchemaFile: ProcessFile | null = null;
|
||||||
|
pm.files.forEach((file: ProcessFile) => {
|
||||||
|
filesByName[file.name] = file;
|
||||||
|
if (file.name === 'extension_uischema.json') {
|
||||||
|
extensionUiSchemaFile = file;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setConfigsIfDesiredSchemaFile(extensionUiSchemaFile, pm);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: targetUris.extensionPath,
|
path: targetUris.extensionListPath,
|
||||||
successCallback: processExtensionResult,
|
successCallback: processExtensionResult,
|
||||||
});
|
});
|
||||||
}, [targetUris.extensionPath, params, filesByName]);
|
}, [
|
||||||
|
filesByName,
|
||||||
|
params,
|
||||||
|
setConfigsIfDesiredSchemaFile,
|
||||||
|
targetUris.extensionListPath,
|
||||||
|
targetUris.extensionPath,
|
||||||
|
]);
|
||||||
|
|
||||||
const processSubmitResult = (result: any) => {
|
const processSubmitResult = (result: any) => {
|
||||||
setProcessedTaskData(result.task_data);
|
setProcessedTaskData(result.task_data);
|
||||||
if (result.rendered_results_markdown) {
|
if (result.rendered_results_markdown) {
|
||||||
setMarkdownToRender(result.rendered_results_markdown);
|
setMarkdownToRenderOnSubmit(result.rendered_results_markdown);
|
||||||
}
|
}
|
||||||
setFormButtonsDisabled(false);
|
setFormButtonsDisabled(false);
|
||||||
};
|
};
|
||||||
@ -111,15 +149,15 @@ export default function Extension() {
|
|||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = `${BACKEND_BASE_URL}/extensions-get-data/${params.process_model}/${optionString}`;
|
const url = `${BACKEND_BASE_URL}/extensions-get-data/${params.page_identifier}/${optionString}`;
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
setFormButtonsDisabled(false);
|
setFormButtonsDisabled(false);
|
||||||
} else {
|
} else {
|
||||||
const postBody: ExtensionPostBody = { extension_input: dataToSubmit };
|
const postBody: ExtensionPostBody = { extension_input: dataToSubmit };
|
||||||
let apiPath = targetUris.extensionPath;
|
let apiPath = targetUris.extensionPath;
|
||||||
if (uiSchemaPageDefinition && uiSchemaPageDefinition.api) {
|
if (uiSchemaPageDefinition && uiSchemaPageDefinition.on_form_submit) {
|
||||||
apiPath = `${targetUris.extensionListPath}/${uiSchemaPageDefinition.api}`;
|
apiPath = `${targetUris.extensionListPath}/${uiSchemaPageDefinition.on_form_submit.api_path}`;
|
||||||
postBody.ui_schema_page_definition = uiSchemaPageDefinition;
|
postBody.ui_schema_action = uiSchemaPageDefinition.on_form_submit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
|
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
|
||||||
@ -141,22 +179,30 @@ export default function Extension() {
|
|||||||
|
|
||||||
if (uiSchemaPageDefinition) {
|
if (uiSchemaPageDefinition) {
|
||||||
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
|
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
|
||||||
|
const markdownContentsToRender = [];
|
||||||
|
|
||||||
if (uiSchemaPageDefinition.markdown_instruction_filename) {
|
if (uiSchemaPageDefinition.markdown_instruction_filename) {
|
||||||
const markdownFile =
|
const markdownFile =
|
||||||
filesByName[uiSchemaPageDefinition.markdown_instruction_filename];
|
filesByName[uiSchemaPageDefinition.markdown_instruction_filename];
|
||||||
|
|
||||||
if (markdownFile.file_contents) {
|
if (markdownFile.file_contents) {
|
||||||
componentsToDisplay.push(
|
markdownContentsToRender.push(markdownFile.file_contents);
|
||||||
<div data-color-mode="light">
|
|
||||||
<MDEditor.Markdown
|
|
||||||
linkTarget="_blank"
|
|
||||||
source={markdownFile.file_contents}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (markdownToRenderOnLoad) {
|
||||||
|
markdownContentsToRender.push(markdownToRenderOnLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markdownContentsToRender.length > 0) {
|
||||||
|
componentsToDisplay.push(
|
||||||
|
<div data-color-mode="light" className="with-bottom-margin">
|
||||||
|
<MDEditor.Markdown
|
||||||
|
linkTarget="_blank"
|
||||||
|
source={markdownContentsToRender.join('\n')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (uiSchemaPageDefinition.form_schema_filename) {
|
if (uiSchemaPageDefinition.form_schema_filename) {
|
||||||
const formSchemaFile =
|
const formSchemaFile =
|
||||||
@ -180,13 +226,13 @@ export default function Extension() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (processedTaskData) {
|
if (processedTaskData) {
|
||||||
if (markdownToRender) {
|
if (markdownToRenderOnSubmit) {
|
||||||
componentsToDisplay.push(
|
componentsToDisplay.push(
|
||||||
<div data-color-mode="light" className="with-top-margin">
|
<div data-color-mode="light" className="with-top-margin">
|
||||||
<MDEditor.Markdown
|
<MDEditor.Markdown
|
||||||
className="onboarding"
|
className="onboarding"
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
source={markdownToRender}
|
source={markdownToRenderOnSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user