diff --git a/spiffworkflow-backend/bin/recreate_db b/spiffworkflow-backend/bin/recreate_db index 5af0209c..6a9d286a 100755 --- a/spiffworkflow-backend/bin/recreate_db +++ b/spiffworkflow-backend/bin/recreate_db @@ -28,8 +28,8 @@ if [[ -n "${SPIFFWORKFLOW_BACKEND_DATABASE_URI:-}" ]]; then database_host=$(grep -oP "^[^:]+://.*@\K(.+?)[:/]" <<<"$SPIFFWORKFLOW_BACKEND_DATABASE_URI" | sed -E 's/[:\/]$//') fi -# this will fix branching conflicts -# poetry run flask db merge heads -m "merging two heads" +# uncomment this line to fix branching conflicts +# poetry run flask db merge heads -m "merging heads" tasks="" if [[ "${1:-}" == "clean" ]]; then diff --git a/spiffworkflow-backend/migrations/versions/844cee572018_.py b/spiffworkflow-backend/migrations/versions/844cee572018_.py new file mode 100644 index 00000000..498d4202 --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/844cee572018_.py @@ -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 ### diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py index fbf3bbcb..01fb9548 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py @@ -88,5 +88,8 @@ from spiffworkflow_backend.models.task_draft_data import ( from spiffworkflow_backend.models.configuration import ( ConfigurationModel, ) # noqa: F401 +from spiffworkflow_backend.models.user_property import ( + UserPropertyModel, +) # noqa: F401 add_listeners() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user_property.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_property.py new file mode 100644 index 00000000..74313f4e --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_property.py @@ -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)) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py index 076dad07..749d59a9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py @@ -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_queue_service import ProcessInstanceIsAlreadyLockedError 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.spec_file_service import SpecFileService from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError @@ -121,17 +122,25 @@ def _run_extension( status_code=400, ) - ui_schema_page_definition = None - if body and "ui_schema_page_definition" in body: - ui_schema_page_definition = body["ui_schema_page_definition"] + ui_schema_action = None + persistence_level = "none" + 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( - status=ProcessInstanceStatus.not_started.value, - process_initiator_id=g.user.id, - process_model_identifier=process_model.id, - process_model_display_name=process_model.display_name, - persistence_level="none", - ) + process_instance = None + if persistence_level == "none": + process_instance = ProcessInstanceModel( + status=ProcessInstanceStatus.not_started.value, + process_initiator_id=g.user.id, + 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 try: @@ -170,10 +179,10 @@ def _run_extension( task_data = processor.get_data() result: dict[str, Any] = {"task_data": task_data} - if ui_schema_page_definition: - if "results_markdown_filename" in ui_schema_page_definition: + if ui_schema_action: + if "results_markdown_filename" in ui_schema_action: 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") form_contents = JinjaService.render_jinja_template(file_contents, task_data=task_data) result["rendered_results_markdown"] = form_contents diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_user_properties.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_user_properties.py new file mode 100644 index 00000000..46266703 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_user_properties.py @@ -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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py index f33b8e4e..777f3ef0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py @@ -31,6 +31,10 @@ class ProcessModelIdentifierMissingError(Exception): pass +class InvalidArgsGivenToScriptError(Exception): + pass + + class Script: """Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/set_user_properties.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/set_user_properties.py new file mode 100644 index 00000000..b6ed4bba --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/set_user_properties.py @@ -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() diff --git a/spiffworkflow-frontend/src/App.tsx b/spiffworkflow-frontend/src/App.tsx index 4fee5ec5..92f140fe 100644 --- a/spiffworkflow-frontend/src/App.tsx +++ b/spiffworkflow-frontend/src/App.tsx @@ -48,11 +48,7 @@ export default function App() { } /> } /> } - /> - } /> diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index 23d7a3e8..4fc02da9 100644 --- a/spiffworkflow-frontend/src/components/NavigationBar.tsx +++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx @@ -24,13 +24,11 @@ import { Can } from '@casl/react'; import logo from '../logo.svg'; import UserService from '../services/UserService'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import { PermissionsToCheck, ProcessModel, ProcessFile } from '../interfaces'; import { - PermissionsToCheck, - ProcessModel, - ProcessFile, ExtensionUiSchema, - UiSchemaNavItem, -} from '../interfaces'; + UiSchemaUxElement, +} from '../extension_ui_schema_interfaces'; import { usePermissionFetcher } from '../hooks/PermissionService'; import HttpService, { UnauthenticatedError } from '../services/HttpService'; import { DOCUMENTATION_URL, SPIFF_ENVIRONMENT } from '../config'; @@ -49,7 +47,7 @@ export default function NavigationBar() { const location = useLocation(); const [activeKey, setActiveKey] = useState(''); const [extensionNavigationItems, setExtensionNavigationItems] = useState< - UiSchemaNavItem[] | null + UiSchemaUxElement[] | null >(null); const { targetUris } = useUriListForPermissions(); @@ -109,7 +107,7 @@ export default function NavigationBar() { } const processExtensionResult = (processModels: ProcessModel[]) => { - const eni: UiSchemaNavItem[] = processModels + const eni: UiSchemaUxElement[] = processModels .map((processModel: ProcessModel) => { const extensionUiSchemaFile = processModel.files.find( (file: ProcessFile) => file.name === 'extension_uischema.json' @@ -119,8 +117,8 @@ export default function NavigationBar() { const extensionUiSchema: ExtensionUiSchema = JSON.parse( extensionUiSchemaFile.file_contents ); - if (extensionUiSchema.navigation_items) { - return extensionUiSchema.navigation_items; + if (extensionUiSchema.ux_elements) { + return extensionUiSchema.ux_elements; } } catch (jsonParseError: any) { console.error( @@ -128,7 +126,7 @@ export default function NavigationBar() { ); } } - return [] as UiSchemaNavItem[]; + return [] as UiSchemaUxElement[]; }) .flat(); if (eni) { @@ -157,6 +155,27 @@ export default function NavigationBar() { const userEmail = UserService.getUserEmail(); 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 {uxElement.label}; + }; + const profileToggletip = (
@@ -177,6 +196,10 @@ export default function NavigationBar() { Documentation + {extensionNavigationElementsForDisplayLocation( + 'user_profile_item', + extensionUserProfileElement + )} {!UserService.authenticationDisabled() ? ( <>
@@ -258,27 +281,21 @@ export default function NavigationBar() { ); }; - const extensionNavigationElements = () => { - if (!extensionNavigationItems) { - return null; + const extensionHeaderMenuItemElement = (uxElement: UiSchemaUxElement) => { + const navItemPage = `/extensions${uxElement.page}`; + const regexp = new RegExp(`^${navItemPage}$`); + if (regexp.test(location.pathname)) { + setActiveKey(navItemPage); } - - return extensionNavigationItems.map((navItem: UiSchemaNavItem) => { - const navItemRoute = `/extensions${navItem.route}`; - const regexp = new RegExp(`^${navItemRoute}`); - if (regexp.test(location.pathname)) { - setActiveKey(navItemRoute); - } - return ( - - {navItem.label} - - ); - }); + return ( + + {uxElement.label} + + ); }; const headerMenuItems = () => { @@ -328,7 +345,10 @@ export default function NavigationBar() { {configurationElement()} - {extensionNavigationElements()} + {extensionNavigationElementsForDisplayLocation( + 'header_menu_item', + extensionHeaderMenuItemElement + )} ); }; diff --git a/spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts b/spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts new file mode 100644 index 00000000..e68cb45c --- /dev/null +++ b/spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts @@ -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; +} diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 30c6bfd3..b4ae0a14 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -435,29 +435,3 @@ export interface DataStore { name: 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; -} diff --git a/spiffworkflow-frontend/src/routes/Extension.tsx b/spiffworkflow-frontend/src/routes/Extension.tsx index bf779560..be9dca51 100644 --- a/spiffworkflow-frontend/src/routes/Extension.tsx +++ b/spiffworkflow-frontend/src/routes/Extension.tsx @@ -1,20 +1,19 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import MDEditor from '@uiw/react-md-editor'; import { useParams } from 'react-router-dom'; import { Editor } from '@monaco-editor/react'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; -import { - ExtensionPostBody, - ExtensionUiSchema, - ProcessFile, - ProcessModel, - UiSchemaPageDefinition, -} from '../interfaces'; +import { ProcessFile, ProcessModel } from '../interfaces'; import HttpService from '../services/HttpService'; import useAPIError from '../hooks/UseApiError'; import { recursivelyChangeNullAndUndefined } from '../helpers'; import CustomForm from '../components/CustomForm'; import { BACKEND_BASE_URL } from '../config'; +import { + ExtensionPostBody, + ExtensionUiSchema, + UiSchemaPageDefinition, +} from '../extension_ui_schema_interfaces'; import ErrorDisplay from '../components/ErrorDisplay'; // eslint-disable-next-line sonarjs/cognitive-complexity @@ -26,7 +25,12 @@ export default function Extension() { const [formData, setFormData] = useState(null); const [formButtonsDisabled, setFormButtonsDisabled] = useState(false); const [processedTaskData, setProcessedTaskData] = useState(null); - const [markdownToRender, setMarkdownToRender] = useState(null); + const [markdownToRenderOnSubmit, setMarkdownToRenderOnSubmit] = useState< + string | null + >(null); + const [markdownToRenderOnLoad, setMarkdownToRenderOnLoad] = useState< + string | null + >(null); const [filesByName] = useState<{ [key: string]: ProcessFile; }>({}); @@ -35,18 +39,15 @@ export default function Extension() { const { addError, removeError } = useAPIError(); - useEffect(() => { - const processExtensionResult = (pm: ProcessModel) => { - setProcessModel(pm); - let extensionUiSchemaFile: ProcessFile | null = null; - pm.files.forEach((file: ProcessFile) => { - filesByName[file.name] = file; - if (file.name === 'extension_uischema.json') { - extensionUiSchemaFile = file; + const setConfigsIfDesiredSchemaFile = useCallback( + (extensionUiSchemaFile: ProcessFile | null, pm: ProcessModel) => { + const processLoadResult = (result: any) => { + setFormData(result.task_data); + if (result.rendered_results_markdown) { + setMarkdownToRenderOnLoad(result.rendered_results_markdown); } - }); + }; - // typescript is really confused by extensionUiSchemaFile so force it since we are properly checking if ( extensionUiSchemaFile && (extensionUiSchemaFile as ProcessFile).file_contents @@ -55,24 +56,61 @@ export default function Extension() { (extensionUiSchemaFile as any).file_contents ); - let routeIdentifier = `/${params.process_model}`; - if (params.extension_route) { - routeIdentifier = `${routeIdentifier}/${params.extension_route}`; + const pageIdentifier = `/${params.page_identifier}`; + if ( + 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({ - path: targetUris.extensionPath, + path: targetUris.extensionListPath, successCallback: processExtensionResult, }); - }, [targetUris.extensionPath, params, filesByName]); + }, [ + filesByName, + params, + setConfigsIfDesiredSchemaFile, + targetUris.extensionListPath, + targetUris.extensionPath, + ]); const processSubmitResult = (result: any) => { setProcessedTaskData(result.task_data); if (result.rendered_results_markdown) { - setMarkdownToRender(result.rendered_results_markdown); + setMarkdownToRenderOnSubmit(result.rendered_results_markdown); } setFormButtonsDisabled(false); }; @@ -111,15 +149,15 @@ export default function Extension() { if (!isValid) { 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; setFormButtonsDisabled(false); } else { const postBody: ExtensionPostBody = { extension_input: dataToSubmit }; let apiPath = targetUris.extensionPath; - if (uiSchemaPageDefinition && uiSchemaPageDefinition.api) { - apiPath = `${targetUris.extensionListPath}/${uiSchemaPageDefinition.api}`; - postBody.ui_schema_page_definition = uiSchemaPageDefinition; + if (uiSchemaPageDefinition && uiSchemaPageDefinition.on_form_submit) { + apiPath = `${targetUris.extensionListPath}/${uiSchemaPageDefinition.on_form_submit.api_path}`; + postBody.ui_schema_action = uiSchemaPageDefinition.on_form_submit; } // 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) { const componentsToDisplay = [

{uiSchemaPageDefinition.header}

]; + const markdownContentsToRender = []; if (uiSchemaPageDefinition.markdown_instruction_filename) { const markdownFile = filesByName[uiSchemaPageDefinition.markdown_instruction_filename]; if (markdownFile.file_contents) { - componentsToDisplay.push( -
- -
- ); + markdownContentsToRender.push(markdownFile.file_contents); } } + if (markdownToRenderOnLoad) { + markdownContentsToRender.push(markdownToRenderOnLoad); + } + + if (markdownContentsToRender.length > 0) { + componentsToDisplay.push( +
+ +
+ ); + } if (uiSchemaPageDefinition.form_schema_filename) { const formSchemaFile = @@ -180,13 +226,13 @@ export default function Extension() { } } if (processedTaskData) { - if (markdownToRender) { + if (markdownToRenderOnSubmit) { componentsToDisplay.push(
);