From 8bede5e569b746302599b408e23714745449d2fb Mon Sep 17 00:00:00 2001 From: Noelia Date: Fri, 3 Nov 2023 16:55:04 +0100 Subject: [PATCH] feat(TransferOwnershipToasts): Created new `ToastsManager.qml` class and new nim backend for special toasts visualization This is a first step to globalize how toasts are treated in the qml layer: - Created `ToastsManager.qml` class to deal with all app toasts generation. - Started moving community transfer ownership related toasts to the new manager class. - Some small cleanup in `AppMain.qml` Nim backend: - Created new api method to deal with extended / action toasts. - Updated needed model / item with new needed roles. Closes of #12175 --- .../main/ephemeral_notification_item.nim | 18 ++ .../main/ephemeral_notification_model.nim | 14 +- src/app/modules/main/io_interface.nim | 4 + src/app/modules/main/module.nim | 20 ++- src/app/modules/main/view.nim | 6 + .../StatusQ/Components/StatusToastMessage.qml | 53 ++++-- .../views/CommunitySettingsView.qml | 30 ---- ui/app/mainui/AppMain.qml | 23 ++- ui/app/mainui/Popups.qml | 4 +- ui/app/mainui/ToastsManager.qml | 160 ++++++++++++++++++ .../shared/stores/CommunityTokensStore.qml | 24 +-- ui/imports/utils/Global.qml | 1 + 12 files changed, 294 insertions(+), 63 deletions(-) create mode 100644 ui/app/mainui/ToastsManager.qml diff --git a/src/app/modules/main/ephemeral_notification_item.nim b/src/app/modules/main/ephemeral_notification_item.nim index 42b07946dc..ec67cc57b1 100644 --- a/src/app/modules/main/ephemeral_notification_item.nim +++ b/src/app/modules/main/ephemeral_notification_item.nim @@ -14,9 +14,12 @@ type durationInMs: int subTitle: string icon: string + iconColor: string loading: bool ephNotifType: EphemeralNotificationType url: string + actionType: int + actionData: string details: NotificationDetails proc initItem*(id: int64, @@ -24,9 +27,12 @@ proc initItem*(id: int64, durationInMs = 0, subTitle = "", icon = "", + iconColor = "", loading = false, ephNotifType = EphemeralNotificationType.Default, url = "", + actionType = 0, # It means, no action enabled + actionData = "", details: NotificationDetails): Item = result = Item() result.id = id @@ -35,9 +41,12 @@ proc initItem*(id: int64, result.title = title result.subTitle = subTitle result.icon = icon + result.iconColor = iconColor result.loading = loading result.ephNotifType = ephNotifType result.url = url + result.actionType = actionType + result.actionData = actionData result.details = details proc id*(self: Item): int64 = @@ -58,6 +67,9 @@ proc subTitle*(self: Item): string = proc icon*(self: Item): string = self.icon +proc iconColor*(self: Item): string = + self.iconColor + proc loading*(self: Item): bool = self.loading @@ -67,5 +79,11 @@ proc ephNotifType*(self: Item): EphemeralNotificationType = proc url*(self: Item): string = self.url +proc actionType*(self: Item): int = + self.actionType + +proc actionData*(self: Item): string = + self.actionData + proc details*(self: Item): NotificationDetails = self.details \ No newline at end of file diff --git a/src/app/modules/main/ephemeral_notification_model.nim b/src/app/modules/main/ephemeral_notification_model.nim index 276e90bc2e..97c70dea3d 100644 --- a/src/app/modules/main/ephemeral_notification_model.nim +++ b/src/app/modules/main/ephemeral_notification_model.nim @@ -9,9 +9,12 @@ type Title SubTitle Icon + IconColor Loading EphNotifType Url + ActionType + ActionData QtObject: type Model* = ref object of QAbstractListModel @@ -39,9 +42,12 @@ QtObject: ModelRole.Title.int:"title", ModelRole.SubTitle.int:"subTitle", ModelRole.Icon.int:"icon", + ModelRole.IconColor.int:"iconColor", ModelRole.Loading.int:"loading", ModelRole.EphNotifType.int:"ephNotifType", - ModelRole.Url.int:"url" + ModelRole.Url.int:"url", + ModelRole.ActionType.int:"actionType", + ModelRole.ActionData.int:"actionData" }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -65,12 +71,18 @@ QtObject: result = newQVariant(item.subTitle) of ModelRole.Icon: result = newQVariant(item.icon) + of ModelRole.IconColor: + result = newQVariant(item.iconColor) of ModelRole.Loading: result = newQVariant(item.loading) of ModelRole.EphNotifType: result = newQVariant(item.ephNotifType.int) of ModelRole.Url: result = newQVariant(item.url) + of ModelRole.ActionType: + result = newQVariant(item.actionType) + of ModelRole.ActionData: + result = newQVariant(item.actionData) proc findIndexById(self: Model, id: int64): int = for i in 0 ..< self.items.len: diff --git a/src/app/modules/main/io_interface.nim b/src/app/modules/main/io_interface.nim index 4f325715bc..4edddc9420 100644 --- a/src/app/modules/main/io_interface.nim +++ b/src/app/modules/main/io_interface.nim @@ -188,6 +188,10 @@ method displayEphemeralNotification*(self: AccessInterface, title: string, subTi ephNotifType: int, url: string, details = NotificationDetails()) {.base.} = raise newException(ValueError, "No implementation available") +method displayEphemeralWithActionNotification*(self: AccessInterface, title: string, subTitle: string, icon: string, iconColor: string, loading: bool, + ephNotifType: int, actionType: int, actionData: string, details = NotificationDetails()) {.base.} = + raise newException(ValueError, "No implementation available") + method removeEphemeralNotification*(self: AccessInterface, id: int64) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 8c9546984b..83915d3a0f 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -1274,9 +1274,25 @@ method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle finalEphNotifType = EphemeralNotificationType.Success elif(ephNotifType == EphemeralNotificationType.Danger.int): finalEphNotifType = EphemeralNotificationType.Danger + + let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon, "", + loading, finalEphNotifType, url, 0, "", details) + self.view.ephemeralNotificationModel().addItem(item) - let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon, - loading, finalEphNotifType, url, details) +# TO UNIFY with the one above. +# Further refactor will be done in a next step +method displayEphemeralWithActionNotification*[T](self: Module[T], title: string, subTitle: string, icon: string, iconColor: string, loading: bool, + ephNotifType: int, actionType: int, actionData: string, details = NotificationDetails()) = + let now = getTime() + let id = now.toUnix * 1000000000 + now.nanosecond + var finalEphNotifType = EphemeralNotificationType.Default + if(ephNotifType == EphemeralNotificationType.Success.int): + finalEphNotifType = EphemeralNotificationType.Success + elif(ephNotifType == EphemeralNotificationType.Danger.int): + finalEphNotifType = EphemeralNotificationType.Danger + + let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon, iconColor, + loading, finalEphNotifType, "", actionType, actionData, details) self.view.ephemeralNotificationModel().addItem(item) method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle: string, details: NotificationDetails) = diff --git a/src/app/modules/main/view.nim b/src/app/modules/main/view.nim index ec842bb79a..258f8d94d8 100644 --- a/src/app/modules/main/view.nim +++ b/src/app/modules/main/view.nim @@ -105,6 +105,12 @@ QtObject: ephNotifType: int, url: string) {.slot.} = self.delegate.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url) + # TO UNIFY with the one above. Now creating a specific method for not introuducing regression. + # Further refactor will be done in a next step + proc displayEphemeralWithActionNotification*(self: View, title: string, subTitle: string, icon: string, iconColor: string, loading: bool, + ephNotifType: int, actionType: int, actionData: string) {.slot.} = + self.delegate.displayEphemeralWithActionNotification(title, subTitle, icon, iconColor, loading, ephNotifType, actionType, actionData) + proc removeEphemeralNotification*(self: View, id: string) {.slot.} = self.delegate.removeEphemeralNotification(id.parseInt) diff --git a/ui/StatusQ/src/StatusQ/Components/StatusToastMessage.qml b/ui/StatusQ/src/StatusQ/Components/StatusToastMessage.qml index 9b893a9b7d..ce23143f7e 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusToastMessage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusToastMessage.qml @@ -101,6 +101,18 @@ Control { Danger } + /*! + \qmlproperty bool StatusToastMessage::actionRequired + This property holds if the specific toast message will enable a specific UI action apart from an external link navigation. + */ + property bool actionRequired: false + + /*! + \qmlproperty bool StatusToastMessage::iconColor + This property holds icon color if it needs a customized color, otherwise, it will depend on toast type. + */ + property string iconColor: "" + /*! \qmlmethod This function is used to open the ToastMessage setting all its properties. @@ -150,13 +162,35 @@ Control { readonly property string openedState: "opened" readonly property string closedState: "closed" - readonly property string iconColor: switch(root.type) { + readonly property string iconColor: { + // If specified: + if(root.iconColor != "") + return root.iconColor + + // If not specified + switch(root.type) { case StatusToastMessage.Type.Success: return Theme.palette.successColor1 case StatusToastMessage.Type.Danger: return Theme.palette.dangerColor1 default: return Theme.palette.primaryColor1 + } + } + readonly property string iconBgColor: { + // If specified: + if(root.iconColor != "") + return Theme.palette.getColor(root.iconColor, 0.1) + + // If not specified + switch(root.type) { + case StatusToastMessage.Type.Success: + return Theme.palette.successColor2 + case StatusToastMessage.Type.Danger: + return Theme.palette.dangerColor3 + default: + return Theme.palette.primaryColor3 + } } } @@ -240,16 +274,7 @@ Control { implicitHeight: 32 Layout.alignment: Qt.AlignVCenter radius: (root.width/2) - color: { - switch(root.type) { - case StatusToastMessage.Type.Success: - return Theme.palette.successColor2 - case StatusToastMessage.Type.Danger: - return Theme.palette.dangerColor3 - default: - return Theme.palette.primaryColor3 - } - } + color: d.iconBgColor visible: loader.sourceComponent != undefined Loader { id: loader @@ -295,7 +320,7 @@ Control { } StatusBaseText { Layout.fillWidth: true - visible: (!root.linkUrl && !!root.secondaryText) + visible: (!linkText.visible && !!root.secondaryText) height: visible ? contentHeight : 0 font.pixelSize: 13 color: Theme.palette.baseColor1 @@ -305,8 +330,10 @@ Control { maximumLineCount: 2 } StatusSelectableText { + id: linkText + Layout.fillWidth: true - visible: (!!root.linkUrl) + visible: (!!root.linkUrl) || root.actionRequired height: visible ? implicitHeight : 0 font.pixelSize: 13 hoveredLinkColor: Theme.palette.primaryColor1 diff --git a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml index 7fe5ae2232..c3e15c84fc 100644 --- a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml @@ -801,36 +801,6 @@ StatusSectionLayout { Global.displayToastMessage(title1, url, "", true, type, url) Global.displayToastMessage(title2, url, "", true, type, url) } - - function onSetSignerStateChanged(communityId, communityName, status, url) { - if (root.community.id !== communityId) - return - - if (status === Constants.ContractTransactionStatus.Completed) { - Global.displayToastMessage(qsTr("%1 smart contract amended").arg(communityName), - qsTr("View on etherscan"), "", false, - Constants.ephemeralNotificationType.success, url) - Global.displayToastMessage(qsTr("Your device is now the control node for %1. You now have full Community admin rights.").arg(communityName), - qsTr("%1 Community admin"), "", false, - Constants.ephemeralNotificationType.success, "" /*TODO internal link*/) - } else if (status === Constants.ContractTransactionStatus.Failed) { - Global.displayToastMessage(qsTr("%1 smart contract update failed").arg(communityName), - qsTr("View on etherscan"), "", false, - Constants.ephemeralNotificationType.normal, url) - } else if (status === Constants.ContractTransactionStatus.InProgress) { - Global.displayToastMessage(qsTr("Updating %1 smart contract").arg(communityName), - qsTr("View on etherscan"), "", true, - Constants.ephemeralNotificationType.normal, url) - } - } - - function onOwnershipLost(communityId, communityName) { - if (root.community.id !== communityId) - return - let type = Constants.ephemeralNotificationType.normal - Global.displayToastMessage(qsTr("Your device is no longer the control node for Doodles. - Your ownership and admin rights for %1 have been transferred to the new owner.").arg(communityName), "", "", false, type, "") - } } Connections { diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 3a22dc5f1f..25c654e9ce 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -70,6 +70,17 @@ Item { // set from main.qml property var sysPalette + // Central UI point for managing app toasts: + ToastsManager { + id: toastsManager + + rootStore: appMain.rootStore + rootChatStore: appMain.rootChatStore + communityTokensStore: appMain.communityTokensStore + + sendModalPopup: sendModal + } + Connections { target: rootStore.mainModuleInst @@ -265,10 +276,6 @@ Item { d.openActivityCenterPopup() } - function onDisplayToastMessage(title: string, subTitle: string, icon: string, loading: bool, ephNotifType: int, url: string) { - appMain.rootStore.mainModuleInst.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url) - } - function onOpenLink(link: string) { // Qt sometimes inserts random HTML tags; and this will break on invalid URL inside QDesktopServices::openUrl(link) link = appMain.rootStore.plainText(link) @@ -1594,15 +1601,22 @@ Item { primaryText: model.title secondaryText: model.subTitle icon.name: model.icon + iconColor: model.iconColor loading: model.loading type: model.ephNotifType linkUrl: model.url + actionRequired: model.actionType !== ToastsManager.ActionType.None duration: model.durationInMs onClicked: { appMain.rootStore.mainModuleInst.ephemeralNotificationClicked(model.timestamp) this.open = false } onLinkActivated: { + if(actionRequired) { + toastsManager.doAction(model.actionType, model.actionData) + return + } + if (link.startsWith("#") && link !== "#") { // internal link to section const sectionArgs = link.substring(1).split("/") const section = sectionArgs[0] @@ -1612,7 +1626,6 @@ Item { else Global.openLink(link) } - onClose: { appMain.rootStore.mainModuleInst.removeEphemeralNotification(model.timestamp) } diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 95e8c1d29e..edfcb0fd2f 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -904,9 +904,9 @@ QtObject { FinaliseOwnershipDeclinePopup { destroyOnClose: true - onDeclineClicked: { + onDeclineClicked: { close() - root.ownershipDeclined() + root.rootStore.communityTokensStore.ownershipDeclined(communityId, communityName) } } } diff --git a/ui/app/mainui/ToastsManager.qml b/ui/app/mainui/ToastsManager.qml new file mode 100644 index 0000000000..c498a70b28 --- /dev/null +++ b/ui/app/mainui/ToastsManager.qml @@ -0,0 +1,160 @@ +import QtQuick 2.15 + +import utils 1.0 + +import AppLayouts.stores 1.0 +import AppLayouts.Chat.stores 1.0 as ChatStores + +import shared.stores 1.0 as SharedStores + +// The purpose of this class is to be the central point for generating toasts in the application. +// It will have as input all needed stores. +// In case the file grows considerably, consider creating different toasts managers per topic / context +// and just instantiate them in here. +QtObject { + id: root + + // Here there are defined some specific actions needed by a toast. + // They are normally specific navigations or open popup action. + enum ActionType { + None = 0, + NavigateToCommunityAdmin = 1, + OpenFinaliseOwnershipPopup = 2, + OpenSendModalPopup = 3 + } + + // Stores: + required property RootStore rootStore + required property ChatStores.RootStore rootChatStore + required property SharedStores.CommunityTokensStore communityTokensStore + + // Properties: + required property var sendModalPopup + + // Utils: + readonly property string viewOptimismExplorerText: qsTr("View on Optimism Explorer") + readonly property string checkmarkCircleAssetName: "checkmark-circle" + readonly property string crownOffAssetName: "crown-off" + + // Community Transfer Ownership related toasts: + readonly property Connections _communityTokensStoreConnections: Connections { + target: root.communityTokensStore + + // Ownership Receiver: + function onOwnerTokenReceived(communityId, communityName) { + let communityColor = root.rootChatStore.getCommunityDetailsAsJson(communityId).color + Global.displayToastWithActionMessage(qsTr("You received the Owner token for %1. To finalize ownership, make your device the control node.").arg(communityName), + qsTr("Finalise ownership"), + "crown", + communityColor, + false, + Constants.ephemeralNotificationType.normal, + ToastsManager.ActionType.OpenFinaliseOwnershipPopup, + communityId) + } + + function onSetSignerStateChanged(communityId, communityName, status, url) { + if (status === Constants.ContractTransactionStatus.Completed) { + Global.displayToastMessage(qsTr("%1 smart contract amended").arg(communityName), + root.viewOptimismExplorerText, + root.checkmarkCircleAssetName, + false, + Constants.ephemeralNotificationType.success, + url) + Global.displayToastWithActionMessage(qsTr("Your device is now the control node for %1. You now have full Community admin rights.").arg(communityName), + qsTr("%1 Community admin").arg(communityName), + root.checkmarkCircleAssetName, + "", + false, + Constants.ephemeralNotificationType.success, + ToastsManager.ActionType.NavigateToCommunityAdmin, + communityId) + } else if (status === Constants.ContractTransactionStatus.Failed) { + Global.displayToastMessage(qsTr("%1 smart contract update failed").arg(communityName), + root.viewOptimismExplorerText, + "warning", + false, + Constants.ephemeralNotificationType.danger, + url) + } else if (status === Constants.ContractTransactionStatus.InProgress) { + Global.displayToastMessage(qsTr("Updating %1 smart contract").arg(communityName), + root.viewOptimismExplorerText, + "", + true, + Constants.ephemeralNotificationType.normal, + url) + } + } + + function onCommunityOwnershipDeclined(communityName) { + Global.displayToastWithActionMessage(qsTr("You declined ownership of %1.").arg(communityName), + qsTr("Return owner token to sender"), + root.crownOffAssetName, + "", + false, + Constants.ephemeralNotificationType.danger, + ToastsManager.ActionType.OpenSendModalPopup, + "") + } + + // Ownership Sender: + function onSendOwnerTokenStateChanged(tokenName, status, url) { + if (status === Constants.ContractTransactionStatus.InProgress) { + Global.displayToastMessage(qsTr("Sending %1 token").arg(tokenName), + root.viewOptimismExplorerText, + "", + true, + Constants.ephemeralNotificationType.normal, url) + } else if (status === Constants.ContractTransactionStatus.Completed) { + Global.displayToastMessage(qsTr("%1 token sent").arg(tokenName), + root.viewOptimismExplorerText, + root.checkmarkCircleAssetName, + false, + Constants.ephemeralNotificationType.success, url) + } + } + + function onOwnershipLost(communityId, communityName) { + Global.displayToastMessage(qsTr("Your device is no longer the control node for %1. + Your ownership and admin rights for %1 have been transferred to the new owner.").arg(communityName), + "", + root.crownOffAssetName, + false, + Constants.ephemeralNotificationType.danger, + "") + } + } + + // Connections to global. These will lead the backend integration: + readonly property Connections _globalConnections: Connections { + target: Global + + function onDisplayToastMessage(title: string, subTitle: string, icon: string, loading: bool, ephNotifType: int, url: string) { + root.rootStore.mainModuleInst.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url) + } + + // TO UNIFY with the one above. + // Further refactor will be done in a next step + function onDisplayToastWithActionMessage(title: string, subTitle: string, icon: string, iconColor: string, loading: bool, ephNotifType: int, actionType: int, actionData: string) { + root.rootStore.mainModuleInst.displayEphemeralWithActionNotification(title, subTitle, icon, iconColor, loading, ephNotifType, actionType, actionData) + } + } + + // It will cover all specific actions (different than open external links) that can be done after clicking toast link text + function doAction(actionType, actionData) { + switch(actionType) { + case ToastsManager.ActionType.NavigateToCommunityAdmin: + root.rootChatStore.setActiveCommunity(actionData) + return + case ToastsManager.ActionType.OpenFinaliseOwnershipPopup: + Global.openFinaliseOwnershipPopup(actionData) + return + case ToastsManager.ActionType.OpenSendModalPopup: + root.sendModalPopup.open() + return + default: + console.warn("ToastsManager: Action type is not defined") + return + } + } +} diff --git a/ui/imports/shared/stores/CommunityTokensStore.qml b/ui/imports/shared/stores/CommunityTokensStore.qml index 28515b60cd..3d6c4c4ec8 100644 --- a/ui/imports/shared/stores/CommunityTokensStore.qml +++ b/ui/imports/shared/stores/CommunityTokensStore.qml @@ -31,6 +31,9 @@ QtObject { signal ownerTokenDeploymentStarted(string communityId, string url) signal setSignerStateChanged(string communityId, string communityName, int status, string url) signal ownershipLost(string communityId, string communityName) + signal communityOwnershipDeclined(string communityName) + signal sendOwnerTokenStateChanged(string tokenName, int status, string url) + signal ownerTokenReceived(string communityId, string communityName) // Minting tokens: function deployCollectible(communityId, collectibleItem) @@ -72,9 +75,10 @@ QtObject { communityTokensModuleInst.setSigner(communityId, chainId, contractAddress, accountAddress) } - function ownershipDeclined(communityId) { + function ownershipDeclined(communityId, communityName) { communityTokensModuleInst.declineOwnership(communityId) - } + root.communityOwnershipDeclined(communityName) + } readonly property Connections connections: Connections { target: communityTokensModuleInst @@ -124,13 +128,7 @@ QtObject { } function onOwnerTokenReceived(communityId, communityName, chainId, communityAddress) { - // TODO clicking url should execute finalise flow - Global.displayToastMessage(qsTr("You received the Owner token for %1. To finalize ownership, make your device the control node.").arg(communityName), - qsTr("Finalise ownership"), - "", - false, - Constants.ephemeralNotificationType.normal, - "") + root.ownerTokenReceived(communityId, communityName) } function onSetSignerStateChanged(communityId, communityName, status, url) { @@ -140,9 +138,15 @@ QtObject { function onOwnershipLost(communityId, communityName) { root.ownershipLost(communityId, communityName) } + + // TODO: BACKEND!!! + function onSendOwnerTokenStateChanged(tokenName, status, url) { + console.warn("TODO: Backend missing! On Send owner token!") + root.onSendOwnerTokenStateChanged(tokenName, status, url) + } } - // Burn: + // Burn: function computeBurnFee(tokenKey, amount, accountAddress, requestId) { console.assert(typeof amount === "string") communityTokensModuleInst.computeBurnFee(tokenKey, amount, accountAddress, requestId) diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index 2675f1e4a2..fa73f6e144 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -25,6 +25,7 @@ QtObject { signal unblockContactRequested(string publicKey, string contactName) signal displayToastMessage(string title, string subTitle, string icon, bool loading, int ephNotifType, string url) + signal displayToastWithActionMessage(string title, string subTitle, string icon, string iconColor, bool loading, int ephNotifType, int actionType, string data) signal openPopupRequested(var popupComponent, var params) signal closePopupRequested()