diff --git a/scripts/dxcomponents/components-manager.js b/scripts/dxcomponents/components-manager.js index 6ccbd7c5..750e4379 100644 --- a/scripts/dxcomponents/components-manager.js +++ b/scripts/dxcomponents/components-manager.js @@ -30,34 +30,21 @@ export class ComponentsManager { /** * Creates a component. + * + * Warning: We cannot init() component here because it may cause infinite loop. + * If we init() child before adding it to parent the following flow may occur: + * 1. Parent creates child (but does not add it to its children list yet) + * 2. Child init() is called + * 3. Parent is updated (because of redux store update) + * 4. Parent creates child again because its children list is empty. + * 5. Child init() is called again - and loops occurs. + * * @param type - type of component to create * @param args - arguments to pass to the component's constructor - * @param init - if true, calls the component's init() method after creation * @returns the created component */ - create(type, args = [], init = true) { + create(type, args = []) { const ComponentClass = getComponentFromMap(type); - const component = new ComponentClass(this, ...args); - if (init) { - component.init(); - } - return component; - } - - /** - * Creates or updates a component. - * @param component - component to update, or null/undefined to create a new one - * @param type - type of component to create - * @param args - arguments to pass to the component's constructor - * @param init - if true, calls the component's init() method after creation - * @returns created or updated component - */ - upsert(component, type, args = [], init = true) { - if (component) { - component.update(...args); - return component; - } else { - return this.create(type, args, init); - } + return new ComponentClass(this, ...args); } } diff --git a/scripts/dxcomponents/components/containers/assignment-card.component.js b/scripts/dxcomponents/components/containers/assignment-card.component.js index 69de3ea6..e0193fac 100644 --- a/scripts/dxcomponents/components/containers/assignment-card.component.js +++ b/scripts/dxcomponents/components/containers/assignment-card.component.js @@ -26,6 +26,7 @@ export class AssignmentCardComponent extends ContainerBaseComponent { this.arSecondaryButtons$, this.actionButtonClick, ]); + this.actionButtonsComponent.init(); this.sendPropsUpdate(); } diff --git a/scripts/dxcomponents/components/containers/assignment.component.js b/scripts/dxcomponents/components/containers/assignment.component.js index 3d2a7a54..f51cb95a 100644 --- a/scripts/dxcomponents/components/containers/assignment.component.js +++ b/scripts/dxcomponents/components/containers/assignment.component.js @@ -127,11 +127,12 @@ export class AssignmentComponent extends BaseComponent { this.arSecondaryButtons$, this.onActionButtonClick, ]; - this.assignmentCardComponent = this.componentsManager.upsert( - this.assignmentCardComponent, - "AssignmentCard", - assignmentCardArgs - ); + if (this.assignmentCardComponent) { + this.assignmentCardComponent.update(...assignmentCardArgs); + } else { + this.assignmentCardComponent = this.componentsManager.create("AssignmentCard", assignmentCardArgs); + this.assignmentCardComponent.init(); + } this.loading = this.newPConn.getLoadingStatus(); this.sendPropsUpdate(); } diff --git a/scripts/dxcomponents/components/containers/container-base.component.js b/scripts/dxcomponents/components/containers/container-base.component.js index 9c3ebd93..9b9213ef 100644 --- a/scripts/dxcomponents/components/containers/container-base.component.js +++ b/scripts/dxcomponents/components/containers/container-base.component.js @@ -48,7 +48,7 @@ export class ContainerBaseComponent extends BaseComponent { oldChildrenComponents.splice(oldChildrenComponents.indexOf(oldComponentToReuse), 1); } else { const newPConn = newChild.getPConnect(); - const newChildComponent = this.componentsManager.create(newPConn.meta.type, [newPConn], false); + const newChildComponent = this.componentsManager.create(newPConn.meta.type, [newPConn]); reconciledComponents.push(newChildComponent); uninitializedComponents.push(newChildComponent); } diff --git a/scripts/dxcomponents/components/containers/flow-container.component.js b/scripts/dxcomponents/components/containers/flow-container.component.js index a8ea1fea..ca3b58d0 100644 --- a/scripts/dxcomponents/components/containers/flow-container.component.js +++ b/scripts/dxcomponents/components/containers/flow-container.component.js @@ -134,9 +134,11 @@ export class FlowContainerComponent extends BaseComponent { ] : []; - this.alertBannerComponents = banners.map((b) => - this.componentsManager.create("AlertBanner", [b.variant, b.messages], true) - ); + this.alertBannerComponents = banners.map((b) => { + const component = this.componentsManager.create("AlertBanner", [b.variant, b.messages]); + component.init(); + return component; + }); } #destroyBanners() { @@ -171,6 +173,7 @@ export class FlowContainerComponent extends BaseComponent { this.childrenPConns, this.containerContextKey, ]); + this.assignmentComponent.init(); } #updateSelf() { diff --git a/scripts/dxcomponents/components/containers/modal-view-container.component.js b/scripts/dxcomponents/components/containers/modal-view-container.component.js index bde27830..65083580 100644 --- a/scripts/dxcomponents/components/containers/modal-view-container.component.js +++ b/scripts/dxcomponents/components/containers/modal-view-container.component.js @@ -256,9 +256,11 @@ export class ModalViewContainerComponent extends ContainerBaseComponent { ] : []; - this.alertBannerComponents = banners.map((b) => - this.componentsManager.create("AlertBanner", [b.variant, b.messages], true) - ); + this.alertBannerComponents = banners.map((b) => { + const component = this.componentsManager.create("AlertBanner", [b.variant, b.messages]); + component.init(); + return component; + }); } #getBannerMessages() { diff --git a/scripts/dxcomponents/components/containers/root-container.component.js b/scripts/dxcomponents/components/containers/root-container.component.js index 4640f388..2ceab429 100644 --- a/scripts/dxcomponents/components/containers/root-container.component.js +++ b/scripts/dxcomponents/components/containers/root-container.component.js @@ -103,11 +103,12 @@ export class RootContainerComponent extends BaseComponent { options, }; const viewContainerPConn = PCore.createPConnect(viewContConfig).getPConnect(); - this.#viewContainerComponent = this.componentsManager.upsert( - this.#viewContainerComponent, - viewContainerPConn.meta.type, - [viewContainerPConn] - ); + if (this.#viewContainerComponent) { + this.#viewContainerComponent.update(viewContainerPConn); + } else { + this.#viewContainerComponent = this.componentsManager.create(viewContainerPConn.meta.type, [viewContainerPConn]); + this.#viewContainerComponent.init(); + } } #configureModalContainer() { @@ -122,11 +123,12 @@ export class RootContainerComponent extends BaseComponent { }); const modalViewContainerPConn = configObjModal.getPConnect(); - this.#modalViewContainerComponent = this.componentsManager.upsert( - this.#modalViewContainerComponent, - modalViewContainerPConn.meta.type, - [modalViewContainerPConn] - ); + if (this.#modalViewContainerComponent) { + this.#modalViewContainerComponent.update(modalViewContainerPConn); + } else { + this.#modalViewContainerComponent = this.componentsManager.create(modalViewContainerPConn.meta.type, [modalViewContainerPConn]); + this.#modalViewContainerComponent.init(); + } if (this.compId !== "1") { console.error(TAG, "RootComponent id must be '1' to match root container on consumer side"); return; diff --git a/scripts/dxcomponents/components/containers/templates/field-group-template.component.js b/scripts/dxcomponents/components/containers/templates/field-group-template.component.js index 00912a33..60599fc3 100644 --- a/scripts/dxcomponents/components/containers/templates/field-group-template.component.js +++ b/scripts/dxcomponents/components/containers/templates/field-group-template.component.js @@ -99,7 +99,14 @@ export class FieldGroupTemplateComponent extends BaseComponent { lookForChildInConfig, evaluateAllowRowAction(allowRowEdit, item) ).getPConnect(); - const newComponent = this.componentsManager.upsert(oldComponent, newPConn.meta.type, [newPConn]); + let newComponent; + if (oldComponent) { + oldComponent.update(newPConn); + newComponent = oldComponent; + } else { + newComponent = this.componentsManager.create(newPConn.meta.type, [newPConn]); + newComponent.init(); + } updatedItems.push({ id: index, name: diff --git a/scripts/dxcomponents/components/containers/templates/simple-table-manual.component.js b/scripts/dxcomponents/components/containers/templates/simple-table-manual.component.js index 4d20e89b..cd6248cd 100644 --- a/scripts/dxcomponents/components/containers/templates/simple-table-manual.component.js +++ b/scripts/dxcomponents/components/containers/templates/simple-table-manual.component.js @@ -244,7 +244,14 @@ export class SimpleTableManualComponent extends BaseComponent { }; const cellPConn = PCore.createPConnect(config).getPConnect(); const oldComponent = editableRow?.cells?.[cellIndex]?.component; - const newComponent = this.componentsManager.upsert(oldComponent, cellPConn.meta.type, [cellPConn]); + let newComponent; + if (oldComponent) { + oldComponent.update(cellPConn); + newComponent = oldComponent; + } else { + newComponent = this.componentsManager.create(cellPConn.meta.type, [cellPConn]); + newComponent.init(); + } newEditableCells.push({ component: newComponent }) } }); diff --git a/scripts/dxcomponents/components/containers/templates/simple-table.component.js b/scripts/dxcomponents/components/containers/templates/simple-table.component.js index f9b342f1..d8f7253a 100644 --- a/scripts/dxcomponents/components/containers/templates/simple-table.component.js +++ b/scripts/dxcomponents/components/containers/templates/simple-table.component.js @@ -69,15 +69,22 @@ export class SimpleTableComponent extends BaseComponent { } if (multiRecordDisplayAs === "fieldGroup") { const fieldGroupProps = { ...configProps, contextClass }; - this.childComponent = this.componentsManager.upsert(this.childComponent, "FieldGroupTemplate", [ - this.pConn, - fieldGroupProps, - ]); + if (this.childComponent) { + this.childComponent.update(this.pConn, fieldGroupProps); + } else { + this.childComponent = this.componentsManager.create("FieldGroupTemplate", [this.pConn, fieldGroupProps]); + this.childComponent.init(); + } this.#sendPropsUpdate(); } else if (fieldMetadata && fieldMetadata.type === 'Page List' && fieldMetadata.dataRetrievalType === 'refer') { console.warn(TAG, 'Displaying ListView in SimpleTable is not supported yet.'); } else { - this.childComponent = this.componentsManager.upsert(this.childComponent, "SimpleTableManual", [this.pConn]); + if (this.childComponent) { + this.childComponent.update(this.pConn); + } else { + this.childComponent = this.componentsManager.create("SimpleTableManual", [this.pConn]); + this.childComponent.init(); + } this.#sendPropsUpdate(); } } diff --git a/scripts/dxcomponents/components/containers/view-container.component.js b/scripts/dxcomponents/components/containers/view-container.component.js index 5c9314a4..a38aefc5 100644 --- a/scripts/dxcomponents/components/containers/view-container.component.js +++ b/scripts/dxcomponents/components/containers/view-container.component.js @@ -94,6 +94,7 @@ export class ViewContainerComponent extends BaseComponent { const viewPConn = ReferenceComponent.normalizePConn(newCompPConn); this.childComponent?.destroy?.(); this.childComponent = this.componentsManager.create(viewPConn.meta.type, [viewPConn]); + this.childComponent.init(); this.props.children = [this.childComponent.compId]; this.componentsManager.onComponentPropsUpdate(this); } diff --git a/scripts/dxcomponents/components/containers/view.component.js b/scripts/dxcomponents/components/containers/view.component.js index ee400c6f..0c7a19fe 100644 --- a/scripts/dxcomponents/components/containers/view.component.js +++ b/scripts/dxcomponents/components/containers/view.component.js @@ -97,7 +97,7 @@ export class ViewComponent extends ContainerBaseComponent { * component is able to handle it. */ if (!configProps.visibility && this.pConn.getPageReference().length > "caseInfo.content".length) { - this.props.visible = this.#evaluateVisibility(this.pConn, configProps.referenceContext); + this.props.visible = this.#evaluateVisibility(this.pConn, configProps); } if (this.SUPPORTED_TEMPLATES.includes(template)) { @@ -116,14 +116,31 @@ export class ViewComponent extends ContainerBaseComponent { this.destroyChildren(); } - this.childrenComponents[0] = this.componentsManager.upsert(this.childrenComponents[0], template, [this.pConn]); + if (this.childrenComponents[0]) { + this.childrenComponents[0].update(this.pConn); + } else { + this.childrenComponents[0] = this.componentsManager.create(template, [this.pConn]); + this.childrenComponents[0].init(); + } } - #evaluateVisibility(pConn, referenceContext) { + #evaluateVisibility(pConn, configProps) { const visibilityExpression = pConn.meta.config.visibility; if (!visibilityExpression || visibilityExpression.length === 0) return true; + const contextName = pConn.getContextName(); + if (visibilityExpression.startsWith("@E ")) { + return this.#evaluateVisibilityExpression(contextName, configProps.referenceContext, visibilityExpression); + } else if (visibilityExpression.startsWith("@W ")) { + // when rules does not need special handling + return configProps.visibility; + } else { + console.warn(TAG, `Unsupported visibility expression: ${visibilityExpression}. Defaulting to visible.`); + return true; + } + } - let dataPage = this.#getDataPage(pConn.getContextName(), referenceContext); + #evaluateVisibilityExpression(contextName, referenceContext, visibilityExpression) { + let dataPage = this.#getDataPage(contextName, referenceContext); if (!dataPage) return false; const visibilityConditions = visibilityExpression.replace("@E ", ""); diff --git a/scripts/init/initial-render.js b/scripts/init/initial-render.js index 1836f514..9c0d4985 100644 --- a/scripts/init/initial-render.js +++ b/scripts/init/initial-render.js @@ -16,5 +16,7 @@ export function initialRender(renderObj) { (component) => bridge.updateNativeComponent(component) ); - return componentsManager.create("RootContainer", [pconn]); + const root = componentsManager.create("RootContainer", [pconn]); + root.init(); + return root; } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/EditableTable.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/EditableTable.kt index 1fce97f0..284870e5 100644 --- a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/EditableTable.kt +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/EditableTable.kt @@ -51,6 +51,8 @@ fun EditableTable( ) { Column { Heading(label, Modifier.padding(vertical = 8.dp), fontSize = 16.sp) + + if (columns.isEmpty()) return val focusManager = LocalFocusManager.current val haveEditColumn = rows.any { it.onEditButtonClick != null } val haveDeleteColumn = rows.any { it.onDeleteButtonClick != null } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt index 20c064e1..46f7449b 100644 --- a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt @@ -1,11 +1,14 @@ package com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import be.digitalia.compose.htmlconverter.htmlToAnnotatedString -import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.internal.Input +import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.internal.HelperText @Composable fun RichText( @@ -22,17 +25,35 @@ fun RichText( // Read-only RichText renders on web as display-only RichTextFieldValue(label, value) } else { - Input( - value = TextFieldValue(rememberAnnotated(value)), - label = label, - modifier = modifier, - helperText = helperText, - validateMessage = validateMessage, - required = required, - disabled = disabled, - readOnly = true, // not allowing editing in RichText for now - lines = 3 - ) + Column(modifier = modifier) { + // not allowing editing in RichText for now + OutlinedTextField( + value = TextFieldValue(rememberAnnotated(value)), + onValueChange = {}, + modifier = Modifier.fillMaxWidth(), + label = { + Label( + label = label, + hideLabel = false, + required = required, + disabled = disabled, + readOnly = true + ) + }, + placeholder = { }, + enabled = !disabled, + readOnly = true, + singleLine = false, + minLines = 3, + maxLines = 3, + ) + HelperText( + text = helperText, + validateMessage = validateMessage, + disabled = disabled, + readOnly = true + ) + } } } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt index 0046e79a..c5227311 100644 --- a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.text.input.TextFieldValue import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.Label @Composable @@ -32,52 +31,11 @@ internal fun Input( leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } -) { - Input( - value = TextFieldValue(value), - label = label, - modifier = modifier, - helperText = helperText, - validateMessage = validateMessage, - hideLabel = hideLabel, - placeholder = placeholder, - required = required, - disabled = disabled, - readOnly = readOnly, - onValueChange = onValueChange, - onFocusChange = onFocusChange, - lines = lines, - keyboardOptions = keyboardOptions, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - interactionSource = interactionSource - ) -} - -@Composable -internal fun Input( - value: TextFieldValue, - label: String, - modifier: Modifier = Modifier, - helperText: String = "", - validateMessage: String = "", - hideLabel: Boolean = false, - placeholder: String = "", - required: Boolean = false, - disabled: Boolean = false, - readOnly: Boolean = false, - onValueChange: (String) -> Unit = {}, - onFocusChange: (Boolean) -> Unit = {}, - lines: Int = 1, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { Column(modifier = modifier) { OutlinedTextField( value = value, - onValueChange = { onValueChange(it.text) }, + onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() .onFocusChanged { onFocusChange(it.isFocused) },