From 9d7015eae3f92711f9932cdf5ac2f2e5cd6e54eb Mon Sep 17 00:00:00 2001 From: Eric Wu <95886809+Eric-B-Wu@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:33:52 -0400 Subject: [PATCH] feat(designer): Custom Code (#4376) * temp * first look * custom code bugfixes * filtering out files to be deleted that have no data * update to segment property * feat(designer): Resizable side panel (#4134) * Initial implementation * Specify for resizable custom sidepanels * Implement Math.max * adding resize support * reseting custom code state on save * fix comment * update based on comments --------- Co-authored-by: Carlos Emiliano Castro Trejo <102700317+ccastrotrejo@users.noreply.github.com> Co-authored-by: Travis Harris --- CHANGELOG.md | 101 ++++--- Localize/lang/strings.json | 4 + .../src/components/DevApiTester.tsx | 34 ++- .../src/components/DevSerializationTester.tsx | 47 +++- .../DesignerCommandBar.tsx | 34 ++- .../Services/WorkflowAndArtifacts.tsx | 100 ++++++- .../app/AzureLogicAppsDesigner/laDesigner.tsx | 87 ++++-- .../src/app/LocalDesigner/localDesigner.tsx | 21 +- .../app/LocalDesigner/pseudoCommandBar.tsx | 4 +- .../src/lib/components/codeView/CodeView.tsx | 34 ++- .../propertiesPane/tabs/CodeTab.tsx | 8 +- .../components/testMapPanel/TestMapPanel.tsx | 11 +- .../util/serializecollapsedarray.ts | 21 +- .../src/lib/authentication/util.ts | 52 ++-- .../src/lib/card/addActionCard/index.tsx | 4 +- libs/designer-ui/src/lib/card/index.tsx | 4 +- .../src/lib/card/noActionCard/index.tsx | 4 +- libs/designer-ui/src/lib/code/codeeditor.less | 18 +- libs/designer-ui/src/lib/code/index.tsx | 92 ++++++- libs/designer-ui/src/lib/code/util.ts | 41 ++- libs/designer-ui/src/lib/combobox/index.tsx | 31 +-- libs/designer-ui/src/lib/constants.ts | 28 ++ .../plugins/CollapsedDictionaryValidation.tsx | 5 +- libs/designer-ui/src/lib/dropdown/index.tsx | 12 +- .../lib/editor/base/utils/editorToSegment.ts | 11 +- .../src/lib/editor/base/utils/helper.ts | 24 +- .../src/lib/editor/base/utils/keyvalueitem.ts | 14 +- .../src/lib/editor/monaco/index.tsx | 11 +- .../src/lib/expressioneditor/index.tsx | 4 +- .../floatingactionmenuinputs/helper.ts | 19 +- libs/designer-ui/src/lib/index.ts | 5 +- libs/designer-ui/src/lib/panel/index.ts | 5 + .../src/lib/panel/panelResizer.tsx | 69 +++++ libs/designer-ui/src/lib/panel/panelUtil.ts | 1 + .../src/lib/panel/panelcontainer.tsx | 9 + .../src/lib/panel/panelheader/panelheader.tsx | 3 + .../panel/panelheader/panelheadertitle.tsx | 6 + .../operationSearchCard/index.tsx | 7 +- libs/designer-ui/src/lib/peek/index.tsx | 3 +- .../src/lib/picker/filepickereditor.tsx | 37 ++- .../src/lib/querybuilder/GroupDropdown.tsx | 9 +- .../lib/querybuilder/HybridQueryBuilder.tsx | 7 +- libs/designer-ui/src/lib/querybuilder/Row.tsx | 15 +- .../src/lib/querybuilder/RowDropdown.tsx | 9 +- .../lib/querybuilder/SimpleQueryBuilder.tsx | 21 +- .../src/lib/querybuilder/index.tsx | 7 +- libs/designer-ui/src/lib/recurrence/index.tsx | 5 +- .../src/lib/schemaeditor/index.tsx | 9 +- .../src/lib/settings/settingsection/index.tsx | 7 + .../settingsection/settingTokenField.tsx | 260 +++++++++--------- .../settingsection/settingdictionary.tsx | 2 +- .../settingsection/settingdropdown.tsx | 2 +- .../settingexpressioneditor.tsx | 2 +- .../settingsection/settingmultiselect.tsx | 2 +- .../settingsection/settingreactiveinput.tsx | 2 +- .../settings/settingsection/settingslider.tsx | 2 +- .../settingsection/settingtextfield.tsx | 2 +- .../settings/settingsection/settingtoggle.tsx | 7 +- libs/designer-ui/src/lib/staticResult/util.ts | 6 +- libs/designer-ui/src/lib/table/index.tsx | 7 +- .../src/lib/tokenpicker/tokenpicker.less | 15 + .../tokenpickersection/tokenpickeroption.tsx | 16 +- libs/designer-ui/src/lib/utils/utils.ts | 4 - libs/designer/src/lib/common/constants.ts | 10 + .../src/lib/common/models/customcode.ts | 12 + .../src/lib/core/BJSWorkflowProvider.tsx | 7 +- .../lib/core/actions/bjsworkflow/delete.ts | 18 +- .../core/actions/bjsworkflow/initialize.ts | 37 ++- .../bjsworkflow/operationdeserializer.ts | 14 +- libs/designer/src/lib/core/index.ts | 64 ++++- .../src/lib/core/parsers/ParseReduxAction.ts | 7 +- .../state/customcode/customcodeInterfaces.ts | 26 ++ .../core/state/customcode/customcodeSlice.ts | 86 ++++++ .../designerOptionsInterfaces.ts | 2 + .../designerOptions/designerOptionsSlice.ts | 3 + .../state/operation/operationMetadataSlice.ts | 95 ++++++- libs/designer/src/lib/core/store.ts | 2 + libs/designer/src/lib/core/utils/loops.ts | 3 + .../src/lib/core/utils/parameters/helper.ts | 49 +++- libs/designer/src/lib/index.tsx | 1 + libs/designer/src/lib/ui/Designer.tsx | 2 +- .../src/lib/ui/connections/dropzone.tsx | 39 ++- .../nodeDetailsPanel/nodeDetailsPanel.tsx | 72 ++++- .../tabs/parametersTab/index.tsx | 61 ++-- libs/designer/src/lib/ui/panel/panelRoot.tsx | 13 +- .../src/lib/ui/settings/settingsection.tsx | 3 +- .../src/designer-client-services/index.ts | 1 + .../__mocks__/builtInOperationResponse.ts | 42 +++ .../lib/base/manifests/inlinecode.ts | 130 +++++++++ .../lib/base/operationmanifest.ts | 17 +- .../lib/customcode.ts | 50 ++++ .../lib/httpClient.ts | 2 +- .../lib/standard/customcode.ts | 142 ++++++++++ .../lib/standard/index.ts | 1 + .../src/intl/compiled-lang/strings.en-XA.json | 28 ++ .../src/intl/compiled-lang/strings.json | 12 + .../src/utils/src/lib/helpers/color.ts | 13 + .../src/utils/src/lib/helpers/customcode.ts | 37 +++ .../src/utils/src/lib/helpers/index.ts | 9 +- .../utils/src/lib/helpers/stringFunctions.ts | 9 + 100 files changed, 2032 insertions(+), 530 deletions(-) create mode 100644 libs/designer-ui/src/lib/panel/index.ts create mode 100644 libs/designer-ui/src/lib/panel/panelResizer.tsx create mode 100644 libs/designer/src/lib/common/models/customcode.ts create mode 100644 libs/designer/src/lib/core/state/customcode/customcodeInterfaces.ts create mode 100644 libs/designer/src/lib/core/state/customcode/customcodeSlice.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/base/manifests/inlinecode.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/customcode.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/standard/customcode.ts create mode 100644 libs/logic-apps-shared/src/utils/src/lib/helpers/customcode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 997e13f55ee..e9cccd8b962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,108 +9,99 @@ ## [2.128.0](https://github.com/Azure/LogicAppsUX/compare/v2.127.0...v2.128.0) (2024-03-22) +## [2.128.0](https://github.com/Azure/LogicAppsUX/compare/v2.127.0...v2.128.0) (2024-03-22) ### Bug Fixes -* **Designer:** Allow WorkflowParameters and AppSettings to be used within each other in dynamic data ([#4410](https://github.com/Azure/LogicAppsUX/issues/4410)) ([6f9b652](https://github.com/Azure/LogicAppsUX/commit/6f9b6520107b2daa5a1d5314155f486c01bbb554)) +- **Designer:** Allow WorkflowParameters and AppSettings to be used within each other in dynamic data ([#4410](https://github.com/Azure/LogicAppsUX/issues/4410)) ([6f9b652](https://github.com/Azure/LogicAppsUX/commit/6f9b6520107b2daa5a1d5314155f486c01bbb554)) ## [2.127.0](https://github.com/Azure/LogicAppsUX/compare/v2.126.0...v2.127.0) (2024-03-21) - ### Features -* **Designer:** Allow user to zoom out 10x more than before ([#4372](https://github.com/Azure/LogicAppsUX/issues/4372)) ([31bffcc](https://github.com/Azure/LogicAppsUX/commit/31bffcca97ff0e6a5a1ea5e195c2c235b49ec32e)) -* **designer:** make adjustments to http request trigger parameters to make chosen method clearer ([#4398](https://github.com/Azure/LogicAppsUX/issues/4398)) ([261511d](https://github.com/Azure/LogicAppsUX/commit/261511da6e7120898beb78d0a4fcac0f999a219e)) - +- **Designer:** Allow user to zoom out 10x more than before ([#4372](https://github.com/Azure/LogicAppsUX/issues/4372)) ([31bffcc](https://github.com/Azure/LogicAppsUX/commit/31bffcca97ff0e6a5a1ea5e195c2c235b49ec32e)) +- **designer:** make adjustments to http request trigger parameters to make chosen method clearer ([#4398](https://github.com/Azure/LogicAppsUX/issues/4398)) ([261511d](https://github.com/Azure/LogicAppsUX/commit/261511da6e7120898beb78d0a4fcac0f999a219e)) ### Bug Fixes -* **Consumption:** Changing parent ID for subgraph nodes ([#4395](https://github.com/Azure/LogicAppsUX/issues/4395)) ([650f736](https://github.com/Azure/LogicAppsUX/commit/650f73681dd41797587cf8e55b18cfb2fe487b0f)) -* **consumption:** Deleting idReplamenent when deleting node ([#4369](https://github.com/Azure/LogicAppsUX/issues/4369)) ([69f6a87](https://github.com/Azure/LogicAppsUX/commit/69f6a87f2c80e63acdfb584be27f4e86748da962)) -* **Data Mapper:** Deserialize source edge for custom function with dash in its name ([#4384](https://github.com/Azure/LogicAppsUX/issues/4384)) ([9b44f43](https://github.com/Azure/LogicAppsUX/commit/9b44f43d5cd1e3cf5ee46b7171422ca28e5b0cf6)) -* **data mapper:** Fix issue where custom functions were being labels 'Collection' ([2c03d24](https://github.com/Azure/LogicAppsUX/commit/2c03d24f01072fdd29de2228bda6fffd0dea0e19)) -* **Data Mapper:** Fixes custom functions category branding ([#4382](https://github.com/Azure/LogicAppsUX/issues/4382)) ([1327f8d](https://github.com/Azure/LogicAppsUX/commit/1327f8d33a41e36a07344bf9f26818ecbcdfa373)) -* **designer-ui:** Convert tokenpickeroptions to ul ([#4396](https://github.com/Azure/LogicAppsUX/issues/4396)) ([45b56ce](https://github.com/Azure/LogicAppsUX/commit/45b56cec7450a2575d787c6c8f1f27c2b550b556)) -* **designer:** Fixes an issue where titles were sometimes not updating input tokens from dynamic data ([#4377](https://github.com/Azure/LogicAppsUX/issues/4377)) ([4d0b914](https://github.com/Azure/LogicAppsUX/commit/4d0b9140bf3bd931fcd8bd66e12962f7b15691ee)) -* **designer:** load monaco as part of build instead of CDN ([#4401](https://github.com/Azure/LogicAppsUX/issues/4401)) ([3c7e360](https://github.com/Azure/LogicAppsUX/commit/3c7e360e860bc6ff708718d4fe17e196c80f9901)) -* **Designer:** Override default Paste Behavior on FireFox ([#4378](https://github.com/Azure/LogicAppsUX/issues/4378)) ([6a2e972](https://github.com/Azure/LogicAppsUX/commit/6a2e9726883a52c5c05f4ad9e89d6828b76198cc)) -* **Designer:** Secure string workflow parameters now pass validation ([#4408](https://github.com/Azure/LogicAppsUX/issues/4408)) ([ba6327a](https://github.com/Azure/LogicAppsUX/commit/ba6327aeadafe3944799e4f0eafc9b28eb70f12a)) -* **designer:** Title changes not updating downstream inputs when serializing ([#4407](https://github.com/Azure/LogicAppsUX/issues/4407)) ([25cf75b](https://github.com/Azure/LogicAppsUX/commit/25cf75bc822e58e7bb2934ad0b9e7a56fbecfead)) +- **Consumption:** Changing parent ID for subgraph nodes ([#4395](https://github.com/Azure/LogicAppsUX/issues/4395)) ([650f736](https://github.com/Azure/LogicAppsUX/commit/650f73681dd41797587cf8e55b18cfb2fe487b0f)) +- **consumption:** Deleting idReplamenent when deleting node ([#4369](https://github.com/Azure/LogicAppsUX/issues/4369)) ([69f6a87](https://github.com/Azure/LogicAppsUX/commit/69f6a87f2c80e63acdfb584be27f4e86748da962)) +- **Data Mapper:** Deserialize source edge for custom function with dash in its name ([#4384](https://github.com/Azure/LogicAppsUX/issues/4384)) ([9b44f43](https://github.com/Azure/LogicAppsUX/commit/9b44f43d5cd1e3cf5ee46b7171422ca28e5b0cf6)) +- **data mapper:** Fix issue where custom functions were being labels 'Collection' ([2c03d24](https://github.com/Azure/LogicAppsUX/commit/2c03d24f01072fdd29de2228bda6fffd0dea0e19)) +- **Data Mapper:** Fixes custom functions category branding ([#4382](https://github.com/Azure/LogicAppsUX/issues/4382)) ([1327f8d](https://github.com/Azure/LogicAppsUX/commit/1327f8d33a41e36a07344bf9f26818ecbcdfa373)) +- **designer-ui:** Convert tokenpickeroptions to ul ([#4396](https://github.com/Azure/LogicAppsUX/issues/4396)) ([45b56ce](https://github.com/Azure/LogicAppsUX/commit/45b56cec7450a2575d787c6c8f1f27c2b550b556)) +- **designer:** Fixes an issue where titles were sometimes not updating input tokens from dynamic data ([#4377](https://github.com/Azure/LogicAppsUX/issues/4377)) ([4d0b914](https://github.com/Azure/LogicAppsUX/commit/4d0b9140bf3bd931fcd8bd66e12962f7b15691ee)) +- **designer:** load monaco as part of build instead of CDN ([#4401](https://github.com/Azure/LogicAppsUX/issues/4401)) ([3c7e360](https://github.com/Azure/LogicAppsUX/commit/3c7e360e860bc6ff708718d4fe17e196c80f9901)) +- **Designer:** Override default Paste Behavior on FireFox ([#4378](https://github.com/Azure/LogicAppsUX/issues/4378)) ([6a2e972](https://github.com/Azure/LogicAppsUX/commit/6a2e9726883a52c5c05f4ad9e89d6828b76198cc)) +- **Designer:** Secure string workflow parameters now pass validation ([#4408](https://github.com/Azure/LogicAppsUX/issues/4408)) ([ba6327a](https://github.com/Azure/LogicAppsUX/commit/ba6327aeadafe3944799e4f0eafc9b28eb70f12a)) +- **designer:** Title changes not updating downstream inputs when serializing ([#4407](https://github.com/Azure/LogicAppsUX/issues/4407)) ([25cf75b](https://github.com/Azure/LogicAppsUX/commit/25cf75bc822e58e7bb2934ad0b9e7a56fbecfead)) ## [2.126.0](https://github.com/Azure/LogicAppsUX/compare/v2.125.0...v2.126.0) (2024-03-14) - ### Bug Fixes -* **Consumption:** Adding check for valid 'false' value ([#4360](https://github.com/Azure/LogicAppsUX/issues/4360)) ([4791743](https://github.com/Azure/LogicAppsUX/commit/4791743f840bd68d6789801490fb25f492f41005)) -* **Consumption:** Changing 'undefined' check for value ([#4361](https://github.com/Azure/LogicAppsUX/issues/4361)) ([b4e0212](https://github.com/Azure/LogicAppsUX/commit/b4e0212690c16190629e0d0b352908965a30d02f)) -* **Designer:** Only resolve app settings if they are used in resource ids ([#4354](https://github.com/Azure/LogicAppsUX/issues/4354)) ([04b6f29](https://github.com/Azure/LogicAppsUX/commit/04b6f29e694ef9dc2e87971162bd5275f2dbd66b)) -* **Designer:** Reverted app settings resolve change ([#4353](https://github.com/Azure/LogicAppsUX/issues/4353)) ([38e3959](https://github.com/Azure/LogicAppsUX/commit/38e3959adf6189633fd1f5b133ebe55d704d0cf9)) +- **Consumption:** Adding check for valid 'false' value ([#4360](https://github.com/Azure/LogicAppsUX/issues/4360)) ([4791743](https://github.com/Azure/LogicAppsUX/commit/4791743f840bd68d6789801490fb25f492f41005)) +- **Consumption:** Changing 'undefined' check for value ([#4361](https://github.com/Azure/LogicAppsUX/issues/4361)) ([b4e0212](https://github.com/Azure/LogicAppsUX/commit/b4e0212690c16190629e0d0b352908965a30d02f)) +- **Designer:** Only resolve app settings if they are used in resource ids ([#4354](https://github.com/Azure/LogicAppsUX/issues/4354)) ([04b6f29](https://github.com/Azure/LogicAppsUX/commit/04b6f29e694ef9dc2e87971162bd5275f2dbd66b)) +- **Designer:** Reverted app settings resolve change ([#4353](https://github.com/Azure/LogicAppsUX/issues/4353)) ([38e3959](https://github.com/Azure/LogicAppsUX/commit/38e3959adf6189633fd1f5b133ebe55d704d0cf9)) ## [2.125.0](https://github.com/Azure/LogicAppsUX/compare/v2.124.0...v2.125.0) (2024-03-11) - ### Bug Fixes -* **Consumption:** Multiple minor changes to scope and subgraph nodes ([#4331](https://github.com/Azure/LogicAppsUX/issues/4331)) ([fd62aaa](https://github.com/Azure/LogicAppsUX/commit/fd62aaab26b6251171f5b76c2bfc34520a190ed3)) -* **Designer:** Fixed issue where some service provider operations would not appear in search / browse ([#4345](https://github.com/Azure/LogicAppsUX/issues/4345)) ([96d2cf5](https://github.com/Azure/LogicAppsUX/commit/96d2cf52c346d9de1a82499842e0c506c6afdbac)) -* **Designer:** Showing secure workflow parameters in consumption ([#4325](https://github.com/Azure/LogicAppsUX/issues/4325)) ([ab07b14](https://github.com/Azure/LogicAppsUX/commit/ab07b14d24af81eb9a2d1380697c016dbfdc8f28)) -* **Designer:** Stuck on loading after making Manage Identity Issue Action ([#4332](https://github.com/Azure/LogicAppsUX/issues/4332)) ([1a366aa](https://github.com/Azure/LogicAppsUX/commit/1a366aadd9c0e7f3c8a12c2f1fa7670cf93ab1c7)) -* **desinger:** Remove Outputs if includeRootOutputs is not set ([#4333](https://github.com/Azure/LogicAppsUX/issues/4333)) ([2ae577d](https://github.com/Azure/LogicAppsUX/commit/2ae577d1f43b61ac9312aae43f1574189c2afd76)) -* **vscode:** Add validation for extension bundle workflows folder ([#4326](https://github.com/Azure/LogicAppsUX/issues/4326)) ([fe7dc9c](https://github.com/Azure/LogicAppsUX/commit/fe7dc9cd2c4daabbada0112b4abeea956b35f006)) +- **Consumption:** Multiple minor changes to scope and subgraph nodes ([#4331](https://github.com/Azure/LogicAppsUX/issues/4331)) ([fd62aaa](https://github.com/Azure/LogicAppsUX/commit/fd62aaab26b6251171f5b76c2bfc34520a190ed3)) +- **Designer:** Fixed issue where some service provider operations would not appear in search / browse ([#4345](https://github.com/Azure/LogicAppsUX/issues/4345)) ([96d2cf5](https://github.com/Azure/LogicAppsUX/commit/96d2cf52c346d9de1a82499842e0c506c6afdbac)) +- **Designer:** Showing secure workflow parameters in consumption ([#4325](https://github.com/Azure/LogicAppsUX/issues/4325)) ([ab07b14](https://github.com/Azure/LogicAppsUX/commit/ab07b14d24af81eb9a2d1380697c016dbfdc8f28)) +- **Designer:** Stuck on loading after making Manage Identity Issue Action ([#4332](https://github.com/Azure/LogicAppsUX/issues/4332)) ([1a366aa](https://github.com/Azure/LogicAppsUX/commit/1a366aadd9c0e7f3c8a12c2f1fa7670cf93ab1c7)) +- **desinger:** Remove Outputs if includeRootOutputs is not set ([#4333](https://github.com/Azure/LogicAppsUX/issues/4333)) ([2ae577d](https://github.com/Azure/LogicAppsUX/commit/2ae577d1f43b61ac9312aae43f1574189c2afd76)) +- **vscode:** Add validation for extension bundle workflows folder ([#4326](https://github.com/Azure/LogicAppsUX/issues/4326)) ([fe7dc9c](https://github.com/Azure/LogicAppsUX/commit/fe7dc9cd2c4daabbada0112b4abeea956b35f006)) ## [2.124.0](https://github.com/Azure/LogicAppsUX/compare/v2.123.0...v2.124.0) (2024-03-07) ## [2.123.0](https://github.com/Azure/LogicAppsUX/compare/v2.122.0...v2.123.0) (2024-03-06) - ### Features -* **designer:** Add verbose telemetry for a number of scenarios ([#4307](https://github.com/Azure/LogicAppsUX/issues/4307)) ([4c93053](https://github.com/Azure/LogicAppsUX/commit/4c9305382f915e07de7cb609a3b5817e99be0ace)) -* **designer:** Allow host to define conditions for built-in and custom connectors ([#4299](https://github.com/Azure/LogicAppsUX/issues/4299)) ([d6f81de](https://github.com/Azure/LogicAppsUX/commit/d6f81deeae7c667e31ffb8f80fad677b29a4544e)) -* **vscode:** Improve status step indicator for export experience ([#4305](https://github.com/Azure/LogicAppsUX/issues/4305)) ([19473ee](https://github.com/Azure/LogicAppsUX/commit/19473ee32c84f161b0914a24600d0942c19814ea)) - +- **designer:** Add verbose telemetry for a number of scenarios ([#4307](https://github.com/Azure/LogicAppsUX/issues/4307)) ([4c93053](https://github.com/Azure/LogicAppsUX/commit/4c9305382f915e07de7cb609a3b5817e99be0ace)) +- **designer:** Allow host to define conditions for built-in and custom connectors ([#4299](https://github.com/Azure/LogicAppsUX/issues/4299)) ([d6f81de](https://github.com/Azure/LogicAppsUX/commit/d6f81deeae7c667e31ffb8f80fad677b29a4544e)) +- **vscode:** Improve status step indicator for export experience ([#4305](https://github.com/Azure/LogicAppsUX/issues/4305)) ([19473ee](https://github.com/Azure/LogicAppsUX/commit/19473ee32c84f161b0914a24600d0942c19814ea)) ### Bug Fixes -* **consumption:** Adding 'Invalid Parameter' message to Subgraph nodes ([#4314](https://github.com/Azure/LogicAppsUX/issues/4314)) ([97e26d7](https://github.com/Azure/LogicAppsUX/commit/97e26d7c2c4efebc7243eca69803ffcfee9652fe)) -* **designer:** Fix linter errors happening on PRs from library consolodation ([#4315](https://github.com/Azure/LogicAppsUX/issues/4315)) ([96e0fff](https://github.com/Azure/LogicAppsUX/commit/96e0fff70a04b5a0797c571951685b259bf09c1d)) +- **consumption:** Adding 'Invalid Parameter' message to Subgraph nodes ([#4314](https://github.com/Azure/LogicAppsUX/issues/4314)) ([97e26d7](https://github.com/Azure/LogicAppsUX/commit/97e26d7c2c4efebc7243eca69803ffcfee9652fe)) +- **designer:** Fix linter errors happening on PRs from library consolodation ([#4315](https://github.com/Azure/LogicAppsUX/issues/4315)) ([96e0fff](https://github.com/Azure/LogicAppsUX/commit/96e0fff70a04b5a0797c571951685b259bf09c1d)) ## [2.122.0](https://github.com/Azure/LogicAppsUX/compare/v2.121.0...v2.122.0) (2024-03-01) - ### Features -* **designer:** Adding hidden parameter field in ConnectionCreationIn… ([#4275](https://github.com/Azure/LogicAppsUX/issues/4275)) ([49b2c9c](https://github.com/Azure/LogicAppsUX/commit/49b2c9c0098a97e8832d3f0b682c47c7b7d7c025)) -* **vscode:** Download extension bundle in extension activation instead of project initialization ([#4287](https://github.com/Azure/LogicAppsUX/issues/4287)) ([a663771](https://github.com/Azure/LogicAppsUX/commit/a6637712e9b46eb87f24bcfae44ea1237abc9899)) - +- **designer:** Adding hidden parameter field in ConnectionCreationIn… ([#4275](https://github.com/Azure/LogicAppsUX/issues/4275)) ([49b2c9c](https://github.com/Azure/LogicAppsUX/commit/49b2c9c0098a97e8832d3f0b682c47c7b7d7c025)) +- **vscode:** Download extension bundle in extension activation instead of project initialization ([#4287](https://github.com/Azure/LogicAppsUX/issues/4287)) ([a663771](https://github.com/Azure/LogicAppsUX/commit/a6637712e9b46eb87f24bcfae44ea1237abc9899)) ### Bug Fixes -* **Consumption:** Adding node name to props to account for change ([#4286](https://github.com/Azure/LogicAppsUX/issues/4286)) ([e61a878](https://github.com/Azure/LogicAppsUX/commit/e61a8788070cdf5bc64e8622a3a10cdd49195688)) -* **Designer:** Fixed issue where connection references would sometimes overlap ([#4290](https://github.com/Azure/LogicAppsUX/issues/4290)) ([01e8768](https://github.com/Azure/LogicAppsUX/commit/01e876871cdf421ee3c9653d66107b8cc35e089e)) -* **designer:** Revert - Update to make connections name case-insensitive ([#4283](https://github.com/Azure/LogicAppsUX/issues/4283)) ([7fc19f7](https://github.com/Azure/LogicAppsUX/commit/7fc19f72329c00decf1064a112a9f40626a7cbc3)), closes [#4279](https://github.com/Azure/LogicAppsUX/issues/4279) +- **Consumption:** Adding node name to props to account for change ([#4286](https://github.com/Azure/LogicAppsUX/issues/4286)) ([e61a878](https://github.com/Azure/LogicAppsUX/commit/e61a8788070cdf5bc64e8622a3a10cdd49195688)) +- **Designer:** Fixed issue where connection references would sometimes overlap ([#4290](https://github.com/Azure/LogicAppsUX/issues/4290)) ([01e8768](https://github.com/Azure/LogicAppsUX/commit/01e876871cdf421ee3c9653d66107b8cc35e089e)) +- **designer:** Revert - Update to make connections name case-insensitive ([#4283](https://github.com/Azure/LogicAppsUX/issues/4283)) ([7fc19f7](https://github.com/Azure/LogicAppsUX/commit/7fc19f72329c00decf1064a112a9f40626a7cbc3)), closes [#4279](https://github.com/Azure/LogicAppsUX/issues/4279) ## [2.121.0](https://github.com/Azure/LogicAppsUX/compare/v2.120.0...v2.121.0) (2024-02-29) - ### Features -* **designer:** Expose receiver URI on AS2 encode output ([#4247](https://github.com/Azure/LogicAppsUX/issues/4247)) ([495973f](https://github.com/Azure/LogicAppsUX/commit/495973f72c200b8fd08638a189108c013c7b0daf)) -* **Designer:** Hybrid preload / active search ([#4233](https://github.com/Azure/LogicAppsUX/issues/4233)) ([94b168f](https://github.com/Azure/LogicAppsUX/commit/94b168f66bfee831b1778082418ef48957d37cfe)) -* **designer:** moved intl ([#4245](https://github.com/Azure/LogicAppsUX/issues/4245)) ([d343bb9](https://github.com/Azure/LogicAppsUX/commit/d343bb96e11fcb2ac49060a6b5cdc2b90f07fa94)) - +- **designer:** Expose receiver URI on AS2 encode output ([#4247](https://github.com/Azure/LogicAppsUX/issues/4247)) ([495973f](https://github.com/Azure/LogicAppsUX/commit/495973f72c200b8fd08638a189108c013c7b0daf)) +- **Designer:** Hybrid preload / active search ([#4233](https://github.com/Azure/LogicAppsUX/issues/4233)) ([94b168f](https://github.com/Azure/LogicAppsUX/commit/94b168f66bfee831b1778082418ef48957d37cfe)) +- **designer:** moved intl ([#4245](https://github.com/Azure/LogicAppsUX/issues/4245)) ([d343bb9](https://github.com/Azure/LogicAppsUX/commit/d343bb96e11fcb2ac49060a6b5cdc2b90f07fa94)) ### Bug Fixes -* **Designer:** Fixed connection reference bug for MI connections ([#4262](https://github.com/Azure/LogicAppsUX/issues/4262)) ([eefe252](https://github.com/Azure/LogicAppsUX/commit/eefe25226a2c6da6753e316cc2e7d71f3689503b)) -* **Designer:** fixed some scripts related to moving libs ([#4268](https://github.com/Azure/LogicAppsUX/issues/4268)) ([48c61bf](https://github.com/Azure/LogicAppsUX/commit/48c61bfd93f54c8d2e8735d5788aec84672a0846)) -* **designer:** Revert - Adding hidden parameter field in ConnectionCreationInfo to pass selected credential id ([#4193](https://github.com/Azure/LogicAppsUX/issues/4193)) ([#4265](https://github.com/Azure/LogicAppsUX/issues/4265)) ([2d57d57](https://github.com/Azure/LogicAppsUX/commit/2d57d57ac35e11e2554e5b34a1723873f1907d81)) -* **designer:** Update to make connections name case-insensitive ([#4279](https://github.com/Azure/LogicAppsUX/issues/4279)) ([498fef9](https://github.com/Azure/LogicAppsUX/commit/498fef92d9d7c8e7abddf7c6b4c4af93bf92ce15)) -* **vscode:** Add conditional clause for already initialized projects ([#4280](https://github.com/Azure/LogicAppsUX/issues/4280)) ([b464f64](https://github.com/Azure/LogicAppsUX/commit/b464f6486c6085e21b14f781a7ff69da9c0351dc)) -* **vscode:** Add padding to overview page ([#4253](https://github.com/Azure/LogicAppsUX/issues/4253)) ([5105754](https://github.com/Azure/LogicAppsUX/commit/51057543e423563cf1a1b63fd97ae02716dd7771)) -* **vscode:** Fix useEffect on workflows success data - export tool ([#4249](https://github.com/Azure/LogicAppsUX/issues/4249)) ([75bed99](https://github.com/Azure/LogicAppsUX/commit/75bed9978e73d0b8aa59d266732a97c17318fab7)) -* **vscode:** Initialize vscode project correctly when project is created outside of vscode ([#4267](https://github.com/Azure/LogicAppsUX/issues/4267)) ([2a3ee91](https://github.com/Azure/LogicAppsUX/commit/2a3ee9159463ed14c5c8a87cb9d991ad52c08a4c)) +- **Designer:** Fixed connection reference bug for MI connections ([#4262](https://github.com/Azure/LogicAppsUX/issues/4262)) ([eefe252](https://github.com/Azure/LogicAppsUX/commit/eefe25226a2c6da6753e316cc2e7d71f3689503b)) +- **Designer:** fixed some scripts related to moving libs ([#4268](https://github.com/Azure/LogicAppsUX/issues/4268)) ([48c61bf](https://github.com/Azure/LogicAppsUX/commit/48c61bfd93f54c8d2e8735d5788aec84672a0846)) +- **designer:** Revert - Adding hidden parameter field in ConnectionCreationInfo to pass selected credential id ([#4193](https://github.com/Azure/LogicAppsUX/issues/4193)) ([#4265](https://github.com/Azure/LogicAppsUX/issues/4265)) ([2d57d57](https://github.com/Azure/LogicAppsUX/commit/2d57d57ac35e11e2554e5b34a1723873f1907d81)) +- **designer:** Update to make connections name case-insensitive ([#4279](https://github.com/Azure/LogicAppsUX/issues/4279)) ([498fef9](https://github.com/Azure/LogicAppsUX/commit/498fef92d9d7c8e7abddf7c6b4c4af93bf92ce15)) +- **vscode:** Add conditional clause for already initialized projects ([#4280](https://github.com/Azure/LogicAppsUX/issues/4280)) ([b464f64](https://github.com/Azure/LogicAppsUX/commit/b464f6486c6085e21b14f781a7ff69da9c0351dc)) +- **vscode:** Add padding to overview page ([#4253](https://github.com/Azure/LogicAppsUX/issues/4253)) ([5105754](https://github.com/Azure/LogicAppsUX/commit/51057543e423563cf1a1b63fd97ae02716dd7771)) +- **vscode:** Fix useEffect on workflows success data - export tool ([#4249](https://github.com/Azure/LogicAppsUX/issues/4249)) ([75bed99](https://github.com/Azure/LogicAppsUX/commit/75bed9978e73d0b8aa59d266732a97c17318fab7)) +- **vscode:** Initialize vscode project correctly when project is created outside of vscode ([#4267](https://github.com/Azure/LogicAppsUX/issues/4267)) ([2a3ee91](https://github.com/Azure/LogicAppsUX/commit/2a3ee9159463ed14c5c8a87cb9d991ad52c08a4c)) ## [2.120.0](https://github.com/Azure/LogicAppsUX/compare/v2.119.0...v2.120.0) (2024-02-22) diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index 0cd843b391d..c8976cc60d4 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -569,6 +569,7 @@ "Mb+Eaq": "Bool", "Mb/Vp8": "Next failed", "Mc6ITJ": "Search", + "Mcvr0B": "To use modules or dependecies, please add at Custom Code Dependenncies in Portal TOC", "MfAdfx": "Edit in advanced mode", "MirIsS": "Show code", "MmBfD1": "Unexpected error", @@ -731,6 +732,7 @@ "TgcgXE": "Tags", "Tiqnir": "Custom", "TjMkDP": "(UTC-06:00) Easter Island", + "TjkOzp": "Close", "TlX98E": "(UTC+02:00) Kaliningrad", "Tla33B": "Required. The value for which to find the index.", "Tmr/9e": "Invalid parameters", @@ -1474,6 +1476,7 @@ "_Mb+Eaq.comment": "This is an option in a dropdown where users can select type Boolean for their parameter.", "_Mb/Vp8.comment": "Button indicating to go to the next page with failed options", "_Mc6ITJ.comment": "Placeholder text to search token picker", + "_Mcvr0B.comment": "This is a message to inform the user to add dependencies to use this action", "_MfAdfx.comment": "Button Label when clicked to swith to advanced editor", "_MirIsS.comment": "Button to display the code view", "_MmBfD1.comment": "This is the default message shown in case of an error. It can be shown in multiple contexts but generally would be a notification", @@ -1636,6 +1639,7 @@ "_TgcgXE.comment": "Label For Tags in About Panel", "_Tiqnir.comment": "Option text for table column type in table editor", "_TjMkDP.comment": "Time zone value ", + "_TjkOzp.comment": "This is the aria label for the close button in the message bar", "_TlX98E.comment": "Time zone value ", "_Tla33B.comment": "Required. The text parameter for which to find the index with the 'indexOf' function.", "_Tmr/9e.comment": "Text to explain that there are invalid parameters for this node", diff --git a/apps/data-mapper-standalone/src/components/DevApiTester.tsx b/apps/data-mapper-standalone/src/components/DevApiTester.tsx index 2c364fddf19..82308addaad 100644 --- a/apps/data-mapper-standalone/src/components/DevApiTester.tsx +++ b/apps/data-mapper-standalone/src/components/DevApiTester.tsx @@ -13,8 +13,9 @@ import { tokens, } from '@fluentui/react-components'; import type { MonacoProps } from '@microsoft/designer-ui'; -import { EditorLanguage, MonacoEditor } from '@microsoft/designer-ui'; +import { MonacoEditor } from '@microsoft/designer-ui'; import { generateDataMapXslt, getFunctions, getSelectedSchema, testDataMap } from '@microsoft/logic-apps-data-mapper'; +import { EditorLanguage } from '@microsoft/logic-apps-shared'; import { useState } from 'react'; const RequestTab = { @@ -138,7 +139,14 @@ export const DevApiTester = () => { onChange={(_e, newValue) => setXsltFilename(newValue ?? '')} /> - + Input schema value { {selectedTab === RequestTab.GenerateXslt && ( - Map definition + + Map definition + { - Response + + Response + { {selectedTab === SerializationTab.Deserialization && ( <> - Map definition + + Map definition + { - Connections + + Connections + { {selectedTab === SerializationTab.Serialization && ( <> - Connections + + Connections + { - Map definition + + Map definition + unknown; - saveWorkflow: (workflow: Workflow) => Promise; + saveWorkflow: (workflow: Workflow, customCodeData: CustomCodeFileNameMapping | undefined) => Promise; isDarkMode: boolean; isConsumption?: boolean; showConnectionsPanel?: boolean; @@ -70,7 +72,14 @@ export const DesignerCommandBar = ({ return parameterGroup.parameters.some((parameter) => { const validationErrors = validateParameter(parameter, parameter.value); if (validationErrors.length > 0) { - dispatch(updateParameterValidation({ nodeId: id, groupId: parameterGroup.id, parameterId: parameter.id, validationErrors })); + dispatch( + updateParameterValidation({ + nodeId: id, + groupId: parameterGroup.id, + parameterId: parameter.id, + validationErrors, + }) + ); } return validationErrors.length; }); @@ -80,9 +89,12 @@ export const DesignerCommandBar = ({ const hasParametersErrors = !isNullOrEmpty(validationErrorsList); + const customCodeFilesWithData = getCustomCodeFilesWithData(designerState.customCode); + if (!hasParametersErrors) { - await saveWorkflow(serializedWorkflow); + await saveWorkflow(serializedWorkflow, customCodeFilesWithData); updateCallbackUrl(designerState, DesignerStore.dispatch); + DesignerStore.dispatch(resetCustomCode()); } }); @@ -167,7 +179,11 @@ export const DesignerCommandBar = ({ disabled: !haveErrors, iconProps: { iconName: haveErrors ? 'StatusErrorFull' : 'ErrorBadge', - style: haveErrors ? { color: RUN_AFTER_COLORS[isDarkMode ? 'dark' : 'light']['FAILED'] } : undefined, + style: haveErrors + ? { + color: RUN_AFTER_COLORS[isDarkMode ? 'dark' : 'light']['FAILED'], + } + : undefined, }, onClick: () => !!dispatch(openPanel({ panelMode: 'Error' })), }, @@ -227,7 +243,13 @@ export const DesignerCommandBar = ({ ); }; -const CustomCommandBarButton = ({ text, showError }: { text: string; showError?: boolean }) => ( +const CustomCommandBarButton = ({ + text, + showError, +}: { + text: string; + showError?: boolean; +}) => (
{text} {showError && } diff --git a/apps/designer-standalone/src/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx b/apps/designer-standalone/src/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx index 2271de903f2..f7a36105cf6 100644 --- a/apps/designer-standalone/src/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx +++ b/apps/designer-standalone/src/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx @@ -3,7 +3,9 @@ import type { CallbackInfo, ConnectionsData, ParametersData, Workflow } from '.. import { Artifact } from '../Models/Workflow'; import { validateResourceId } from '../Utilities/resourceUtilities'; import { convertDesignerWorkflowToConsumptionWorkflow } from './ConsumptionSerializationHelpers'; -import type { LogicAppsV2 } from '@microsoft/logic-apps-shared'; +import type { CustomCodeFileNameMapping } from '@microsoft/logic-apps-designer'; +import { CustomCodeService, LogEntryLevel, LoggerService } from '@microsoft/logic-apps-shared'; +import type { LogicAppsV2, VFSObject } from '@microsoft/logic-apps-shared'; import axios from 'axios'; import jwt_decode from 'jwt-decode'; import { useQuery } from 'react-query'; @@ -34,6 +36,53 @@ export const useWorkflowAndArtifactsStandard = (workflowId: string) => { ); }; +export const useAllCustomCodeFiles = (appId?: string, workflowName?: string) => { + return useQuery(['workflowCustomCode', appId, workflowName], async () => await getAllCustomCodeFiles(appId, workflowName), { + enabled: !!appId && !!workflowName, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }); +}; + +const getAllCustomCodeFiles = async (appId?: string, workflowName?: string) => { + const customCodeFiles: Record = {}; + const uri = `${baseUrl}${appId}/hostruntime/admin/vfs/${workflowName}`; + const vfsObjects: VFSObject[] = ( + await axios.get(uri, { + headers: { + Authorization: `Bearer ${environment.armToken}`, + }, + params: { + relativePath: 1, + 'api-version': '2018-11-01', + }, + }) + ).data.filter((file) => file.name !== Artifact.WorkflowFile); + + const filesData = await Promise.all( + vfsObjects.map(async (file) => { + const response = await axios.get(`${uri}/${file.name}`, { + headers: { + Authorization: `Bearer ${environment.armToken}`, + 'If-Match': ['*'], + }, + params: { + relativePath: 1, + 'api-version': '2018-11-01', + }, + }); + return { name: file.name, data: response.data }; + }) + ); + + filesData.forEach((file) => { + customCodeFiles[file.name] = file.data; + }); + + return customCodeFiles; +}; + export const useWorkflowAndArtifactsConsumption = (workflowId: string) => { return useQuery(['workflowArtifactsConsumption', workflowId], () => getWorkflowAndArtifactsConsumption(workflowId), { refetchOnMount: false, @@ -213,13 +262,57 @@ export const getConnectionConsumption = async (connectionId: string) => { return response.data; }; +export const saveCustomCodeStandard = async (customCode?: CustomCodeFileNameMapping): Promise => { + if (!customCode) return; + try { + const existingFiles = (await CustomCodeService().getAllCustomCodeFiles()).map((file) => file.name); + // to prevent 404's we first check which custom code files are already present before deleting + Object.entries(customCode).forEach(([fileName, customCodeData]) => { + const { fileExtension, isModified, isDeleted, fileData } = customCodeData; + if (isDeleted) { + if (existingFiles.includes(fileName)) { + CustomCodeService().deleteCustomCode(fileName); + LoggerService().log({ + level: LogEntryLevel.Verbose, + area: 'serializeCustomcode', + message: `Deleting custom code file: ${fileName}`, + }); + } + } else if (isModified) { + LoggerService().log({ + level: LogEntryLevel.Verbose, + area: 'serializeCustomcode', + message: `Uploading/Updating custom code file: ${fileName}`, + }); + + CustomCodeService().uploadCustomCode({ + fileData, + fileName, + fileExtension, + }); + } + }); + return; + } catch (error) { + const errorMessage = `Failed to save custom code: ${error}`; + LoggerService().log({ + level: LogEntryLevel.Error, + area: 'serializeCustomcode', + message: errorMessage, + error: error instanceof Error ? error : undefined, + }); + return; + } +}; + export const saveWorkflowStandard = async ( siteResourceId: string, workflowName: string, workflow: any, connectionsData: ConnectionsData | undefined, parametersData: ParametersData | undefined, - settings: Record | undefined + settings: Record | undefined, + customCodeData: CustomCodeFileNameMapping | undefined ): Promise => { const data: any = { files: { @@ -248,6 +341,9 @@ export const saveWorkflowStandard = async ( } } + // saving custom code must happen synchronously with deploying the workflow artifacts as they both cause + // the host to go soft restart. We may need to look into if there's a race case where this may still happen + saveCustomCodeStandard(customCodeData); await axios.post(`${baseUrl}${siteResourceId}/deployWorkflowArtifacts?api-version=${standardApiVersion}`, data, { headers: { 'If-Match': '*', diff --git a/apps/designer-standalone/src/app/AzureLogicAppsDesigner/laDesigner.tsx b/apps/designer-standalone/src/app/AzureLogicAppsDesigner/laDesigner.tsx index a49d348983a..fc99406bf4c 100644 --- a/apps/designer-standalone/src/app/AzureLogicAppsDesigner/laDesigner.tsx +++ b/apps/designer-standalone/src/app/AzureLogicAppsDesigner/laDesigner.tsx @@ -14,6 +14,7 @@ import { getConnectionStandard, listCallbackUrl, saveWorkflowStandard, + useAllCustomCodeFiles, useAppSettings, useCurrentObjectId, useCurrentTenantId, @@ -32,6 +33,7 @@ import { BaseGatewayService, StandardConnectionService, StandardConnectorService, + StandardCustomCodeService, StandardOperationManifestService, StandardRunService, StandardSearchService, @@ -42,7 +44,7 @@ import { optional, } from '@microsoft/logic-apps-shared'; import type { ContentType, IWorkflowService, LogicAppsV2 } from '@microsoft/logic-apps-shared'; -import type { Workflow } from '@microsoft/logic-apps-designer'; +import type { CustomCodeFileNameMapping, Workflow } from '@microsoft/logic-apps-designer'; import { DesignerProvider, BJSWorkflowProvider, @@ -73,6 +75,7 @@ const DesignerEditor = () => { const workflowName = workflowId.split('/').splice(-1)[0]; const siteResourceId = new ArmParser(workflowId).topmostResourceId; + const { data: customCodeData, isLoading: customCodeLoading } = useAllCustomCodeFiles(appId, workflowName); const { data, isLoading, isError, error } = useWorkflowAndArtifactsStandard(workflowId); const { data: settingsData, isLoading: settingsLoading, isError: settingsIsError, error: settingsError } = useAppSettings(siteResourceId); const { data: workflowAppData, isLoading: appLoading } = useWorkflowApp(siteResourceId); @@ -175,26 +178,33 @@ const DesignerEditor = () => { setWorkflow(data?.properties.files[Artifact.WorkflowFile]); }, [data?.properties.files]); - if (isLoading || appLoading || settingsLoading) { + if (isLoading || appLoading || settingsLoading || customCodeLoading) { // eslint-disable-next-line react/jsx-no-useless-fragment return <>; } - const originalSettings: Record = { ...(settingsData?.properties ?? {}) }; + const originalSettings: Record = { + ...(settingsData?.properties ?? {}), + }; const originalParametersData: ParametersData = clone(parameters ?? {}); if (isError || settingsIsError) { throw error ?? settingsError; } - const saveWorkflowFromDesigner = async (workflowFromDesigner: Workflow): Promise => { + const saveWorkflowFromDesigner = async ( + workflowFromDesigner: Workflow, + customCode: CustomCodeFileNameMapping | undefined + ): Promise => { const { definition, connectionReferences, parameters } = workflowFromDesigner; const workflowToSave = { ...workflow, definition, }; - const newManagedApiConnections = { ...(connectionsData?.managedApiConnections ?? {}) }; + const newManagedApiConnections = { + ...(connectionsData?.managedApiConnections ?? {}), + }; const newServiceProviderConnections: Record = {}; const referenceKeys = Object.keys(connectionReferences ?? {}); @@ -243,7 +253,15 @@ const DesignerEditor = () => { const parametersToUpdate = !isEqual(originalParametersData, parameters) ? (parameters as ParametersData) : undefined; const settingsToUpdate = !isEqual(settingsData?.properties, originalSettings) ? settingsData?.properties : undefined; - return saveWorkflowStandard(siteResourceId, workflowName, workflowToSave, connectionsToUpdate, parametersToUpdate, settingsToUpdate); + return saveWorkflowStandard( + siteResourceId, + workflowName, + workflowToSave, + connectionsToUpdate, + parametersToUpdate, + settingsToUpdate, + customCode + ); }; const getUpdatedWorkflow = async (): Promise => { @@ -283,7 +301,13 @@ const DesignerEditor = () => { > {workflow?.definition ? ( @@ -337,10 +361,15 @@ const getDesignerServices = ( const baseUrl = `${armUrl}${siteResourceId}/hostruntime/runtime/webhooks/workflow/api/management`; const workflowName = workflowId.split('/').splice(-1)[0]; const workflowIdWithHostRuntime = `${siteResourceId}/hostruntime/runtime/webhooks/workflow/api/management/workflows/${workflowName}`; + const appName = siteResourceId.split('/').splice(-1)[0]; const { subscriptionId, resourceGroup } = new ArmParser(workflowId); const defaultServiceParams = { baseUrl, httpClient, apiVersion }; - const armServiceParams = { ...defaultServiceParams, baseUrl: armUrl, siteResourceId }; + const armServiceParams = { + ...defaultServiceParams, + baseUrl: armUrl, + siteResourceId, + }; const connectionService = new StandardConnectionService({ ...defaultServiceParams, @@ -353,7 +382,7 @@ const getDesignerServices = ( tenantId, httpClient, }, - workflowAppDetails: { appName: siteResourceId.split('/').splice(-1)[0], identity: workflowApp?.identity as any }, + workflowAppDetails: { appName, identity: workflowApp?.identity as any }, readConnections: () => Promise.resolve(connectionsData), writeConnection: addConnection as any, connectionCreationClients: { @@ -361,7 +390,7 @@ const getDesignerServices = ( baseUrl: armUrl, subscriptionId, resourceGroup, - appName: siteResourceId.split('/').splice(-1)[0], + appName, apiVersion: '2022-03-01', httpClient, }), @@ -374,13 +403,24 @@ const getDesignerServices = ( httpClient, queryClient, }); - const childWorkflowService = new ChildWorkflowService({ apiVersion, baseUrl: armUrl, siteResourceId, httpClient, workflowName }); + const childWorkflowService = new ChildWorkflowService({ + apiVersion, + baseUrl: armUrl, + siteResourceId, + httpClient, + workflowName, + }); const artifactService = new ArtifactService({ ...armServiceParams, siteResourceId, integrationAccountCallbackUrl: undefined, }); - const appService = new BaseAppServiceService({ baseUrl: armUrl, apiVersion, subscriptionId, httpClient }); + const appService = new BaseAppServiceService({ + baseUrl: armUrl, + apiVersion, + subscriptionId, + httpClient, + }); const connectorService = new StandardConnectorService({ ...defaultServiceParams, clientSupportedOperations: [ @@ -456,7 +496,11 @@ const getDesignerServices = ( const operationManifestService = new StandardOperationManifestService(defaultServiceParams); const searchService = new StandardSearchService({ ...defaultServiceParams, - apiHubServiceDetails: { apiVersion: '2018-07-01-preview', subscriptionId, location }, + apiHubServiceDetails: { + apiVersion: '2018-07-01-preview', + subscriptionId, + location, + }, showStatefulOperations: isStateful, isDev: false, }); @@ -522,12 +566,20 @@ const getDesignerServices = ( }); const chatbotService = new BaseChatbotService({ - // temporarily having brazilus as the baseUrl until deployment finishes in prod - baseUrl: 'https://brazilus.management.azure.com', + baseUrl: armUrl, apiVersion: '2022-09-01-preview', subscriptionId, - // temporarily hardcoding location until we have deployed to all regions - location: 'westcentralus', + location, + }); + + const customCodeService = new StandardCustomCodeService({ + apiVersion: '2018-11-01', + baseUrl: armUrl, + subscriptionId, + resourceGroup, + appName, + workflowName, + httpClient, }); return { @@ -545,6 +597,7 @@ const getDesignerServices = ( runService, hostService, chatbotService, + customCodeService, }; }; diff --git a/apps/designer-standalone/src/app/LocalDesigner/localDesigner.tsx b/apps/designer-standalone/src/app/LocalDesigner/localDesigner.tsx index d288d3537ea..3df8b103509 100644 --- a/apps/designer-standalone/src/app/LocalDesigner/localDesigner.tsx +++ b/apps/designer-standalone/src/app/LocalDesigner/localDesigner.tsx @@ -15,6 +15,7 @@ import { StandardRunService, ConsumptionOperationManifestService, ConsumptionConnectionService, + StandardCustomCodeService, ResourceIdentityType, } from '@microsoft/logic-apps-shared'; import type { ContentType } from '@microsoft/logic-apps-shared'; @@ -34,7 +35,10 @@ const connectionServiceStandard = new StandardConnectionService({ location: '', httpClient, }, - workflowAppDetails: { appName: 'app', identity: { type: ResourceIdentityType.SYSTEM_ASSIGNED } }, + workflowAppDetails: { + appName: 'app', + identity: { type: ResourceIdentityType.SYSTEM_ASSIGNED }, + }, readConnections: () => Promise.resolve({}), }); @@ -124,7 +128,19 @@ const runService = new StandardRunService({ isDev: true, }); -const workflowService = { getCallbackUrl: () => Promise.resolve({ method: 'POST', value: 'Dummy url' }) }; +const customCodeService = new StandardCustomCodeService({ + apiVersion: '2018-11-01', + baseUrl: '/url', + subscriptionId: 'test', + resourceGroup: 'test', + appName: 'app', + workflowName: 'workflow', + httpClient, +}); + +const workflowService = { + getCallbackUrl: () => Promise.resolve({ method: 'POST', value: 'Dummy url' }), +}; const hostService = { fetchAndDisplayContent: (title: string, url: string, type: ContentType) => console.log(title, url, type), @@ -166,6 +182,7 @@ export const LocalDesigner = () => { runService, editorService, connectionParameterEditorService, + customCodeService, }, readOnly: isReadOnly, isMonitoringView, diff --git a/apps/designer-standalone/src/app/LocalDesigner/pseudoCommandBar.tsx b/apps/designer-standalone/src/app/LocalDesigner/pseudoCommandBar.tsx index c6ca3316b0d..45624ec74c3 100644 --- a/apps/designer-standalone/src/app/LocalDesigner/pseudoCommandBar.tsx +++ b/apps/designer-standalone/src/app/LocalDesigner/pseudoCommandBar.tsx @@ -2,7 +2,7 @@ import { useShowConnectionsPanel } from '../../state/workflowLoadingSelectors'; import './pseudoCommandBar.less'; import type { IModalStyles } from '@fluentui/react'; import { ActionButton, Modal } from '@fluentui/react'; -import { MonacoEditor, EditorLanguage } from '@microsoft/designer-ui'; +import { MonacoEditor } from '@microsoft/designer-ui'; import type { Workflow, AppDispatch, RootState } from '@microsoft/logic-apps-designer'; import { useIsDesignerDirty, @@ -13,7 +13,7 @@ import { useWorkflowParameterValidationErrors, openPanel, } from '@microsoft/logic-apps-designer'; -import { RUN_AFTER_COLORS } from '@microsoft/logic-apps-shared'; +import { EditorLanguage, RUN_AFTER_COLORS } from '@microsoft/logic-apps-shared'; import { useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; diff --git a/libs/data-mapper/src/lib/components/codeView/CodeView.tsx b/libs/data-mapper/src/lib/components/codeView/CodeView.tsx index 10d86931723..91bac7b56c0 100644 --- a/libs/data-mapper/src/lib/components/codeView/CodeView.tsx +++ b/libs/data-mapper/src/lib/components/codeView/CodeView.tsx @@ -2,7 +2,8 @@ import { commonCodeEditorProps } from '../testMapPanel/TestMapPanel'; import { Stack } from '@fluentui/react'; import { Button, makeStyles, shorthands, Text, tokens, typographyStyles } from '@fluentui/react-components'; import { Code20Regular, Dismiss20Regular } from '@fluentui/react-icons'; -import { EditorLanguage, MonacoEditor } from '@microsoft/designer-ui'; +import { MonacoEditor } from '@microsoft/designer-ui'; +import { EditorLanguage } from '@microsoft/logic-apps-shared'; import { useState } from 'react'; import { useIntl } from 'react-intl'; @@ -113,9 +114,20 @@ export const CodeView = ({ }); return ( - +
@@ -143,7 +160,14 @@ export const CodeView = ({ /> -
+
{ return srcSchemaNode.key; } else { // Get target schema node's map definition chunk - const reducedConnectionDictionary: ConnectionDictionary = { ...connectionDictionary }; + const reducedConnectionDictionary: ConnectionDictionary = { + ...connectionDictionary, + }; Object.keys(reducedConnectionDictionary).forEach((conKey) => { if (conKey.includes(targetPrefix) && !conKey.includes(currentNode.key)) { delete reducedConnectionDictionary[conKey]; diff --git a/libs/data-mapper/src/lib/components/testMapPanel/TestMapPanel.tsx b/libs/data-mapper/src/lib/components/testMapPanel/TestMapPanel.tsx index 4c096446a01..d0b47caa61e 100644 --- a/libs/data-mapper/src/lib/components/testMapPanel/TestMapPanel.tsx +++ b/libs/data-mapper/src/lib/components/testMapPanel/TestMapPanel.tsx @@ -5,8 +5,8 @@ import { LogCategory, LogService } from '../../utils/Logging.Utils'; import { ChoiceGroup, DefaultButton, Panel, PanelType, Pivot, PivotItem, PrimaryButton, Stack, StackItem, Text } from '@fluentui/react'; import { makeStyles, shorthands, tokens } from '@fluentui/react-components'; import type { MonacoProps } from '@microsoft/designer-ui'; -import { EditorLanguage, MonacoEditor } from '@microsoft/designer-ui'; -import { guid, isNullOrEmpty, SchemaFileFormat } from '@microsoft/logic-apps-shared'; +import { MonacoEditor } from '@microsoft/designer-ui'; +import { EditorLanguage, guid, isNullOrEmpty, SchemaFileFormat } from '@microsoft/logic-apps-shared'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; @@ -229,7 +229,12 @@ export const TestMapPanel = ({ mapDefinition, isOpen, onClose }: TestMapPanelPro {noXsltLoc} ) : isMismatchedXslt ? ( - + {mismatchedXsltLoc} ) : ( diff --git a/libs/designer-ui/src/lib/arrayeditor/util/serializecollapsedarray.ts b/libs/designer-ui/src/lib/arrayeditor/util/serializecollapsedarray.ts index 60cb3570fb3..359e84d0bae 100644 --- a/libs/designer-ui/src/lib/arrayeditor/util/serializecollapsedarray.ts +++ b/libs/designer-ui/src/lib/arrayeditor/util/serializecollapsedarray.ts @@ -1,16 +1,15 @@ import type { ArrayItemSchema, ComplexArrayItems, SimpleArrayItem } from '..'; import type { ValueSegment } from '../../editor'; -import { ValueSegmentType } from '../../editor'; import type { CastHandler } from '../../editor/base'; import { convertStringToSegments } from '../../editor/base/utils/editorToSegment'; -import { getChildrenNodes, insertQutationForStringType } from '../../editor/base/utils/helper'; +import { createLiteralValueSegment, getChildrenNodes, insertQutationForStringType } from '../../editor/base/utils/helper'; import { convertSegmentsToString } from '../../editor/base/utils/parsesegments'; import { convertComplexItemsToArray, validationAndSerializeComplexArray, validationAndSerializeSimpleArray } from './util'; -import { guid, prettifyJsonString } from '@microsoft/logic-apps-shared'; +import { prettifyJsonString } from '@microsoft/logic-apps-shared'; import type { LexicalEditor } from 'lexical'; import { $getRoot } from 'lexical'; -const emptyArrayValue = [{ id: guid(), type: ValueSegmentType.LITERAL, value: '[]' }]; +const emptyArrayValue = [createLiteralValueSegment('[]')]; export const serializeSimpleArray = ( editor: LexicalEditor, @@ -49,23 +48,23 @@ export const parseSimpleItems = ( const { type, format } = itemSchema; const castedArraySegments: ValueSegment[] = []; const uncastedArraySegments: ValueSegment[] = []; - castedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '[\n ' }); - uncastedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '[\n ' }); + castedArraySegments.push(createLiteralValueSegment('[\n ')); + uncastedArraySegments.push(createLiteralValueSegment('[\n ')); items.forEach((item, index) => { const { value } = item; if (value?.length === 0) { - castedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '""' }); - uncastedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '""' }); + castedArraySegments.push(createLiteralValueSegment('""')); + uncastedArraySegments.push(createLiteralValueSegment('""')); } else { insertQutationForStringType(castedArraySegments, type); insertQutationForStringType(uncastedArraySegments, type); - castedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: castParameter(value, type, format) }); + castedArraySegments.push(createLiteralValueSegment(castParameter(value, type, format))); uncastedArraySegments.push(...value); insertQutationForStringType(castedArraySegments, type); insertQutationForStringType(uncastedArraySegments, type); } - castedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: index < items.length - 1 ? ',\n ' : '\n]' }); - uncastedArraySegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: index < items.length - 1 ? ',\n ' : '\n]' }); + castedArraySegments.push(createLiteralValueSegment(index < items.length - 1 ? ',\n ' : '\n]')); + uncastedArraySegments.push(createLiteralValueSegment(index < items.length - 1 ? ',\n ' : '\n]')); }); // Beautify ValueSegment diff --git a/libs/designer-ui/src/lib/authentication/util.ts b/libs/designer-ui/src/lib/authentication/util.ts index 5508187ecac..96d29c3575e 100644 --- a/libs/designer-ui/src/lib/authentication/util.ts +++ b/libs/designer-ui/src/lib/authentication/util.ts @@ -2,12 +2,12 @@ import type { AuthProps } from '.'; import { AuthenticationType } from '.'; import constants from '../constants'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import { convertStringToSegments } from '../editor/base/utils/editorToSegment'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import { convertKeyValueItemToSegments } from '../editor/base/utils/keyvalueitem'; import { AuthenticationOAuthType } from './AADOAuth/AADOAuth'; -import { getIntl, guid, equals, ResourceIdentityType } from '@microsoft/logic-apps-shared'; import type { ManagedIdentity } from '@microsoft/logic-apps-shared'; +import { ResourceIdentityType, equals, getIntl, guid } from '@microsoft/logic-apps-shared'; export interface AuthProperty { displayName: string; @@ -346,9 +346,7 @@ export function parseAuthEditor(authType: AuthenticationType, items: AuthProps): break; case AuthenticationType.MSI: if (items.msi?.msiIdentity) { - updateValues(values, AUTHENTICATION_PROPERTIES.MSI_IDENTITY, [ - { id: guid(), type: ValueSegmentType.LITERAL, value: items.msi.msiIdentity }, - ]); + updateValues(values, AUTHENTICATION_PROPERTIES.MSI_IDENTITY, [createLiteralValueSegment(items.msi.msiIdentity)]); } updateValues(values, AUTHENTICATION_PROPERTIES.MSI_AUDIENCE, items.msi?.msiAudience); @@ -368,8 +366,8 @@ export function parseAuthEditor(authType: AuthenticationType, items: AuthProps): } const currentItems: CollapsedAuthEditorItems[] = [ { - key: [{ type: ValueSegmentType.LITERAL, id: guid(), value: AUTHENTICATION_PROPERTIES.TYPE.name }], - value: [{ type: ValueSegmentType.LITERAL, id: guid(), value: authType }], + key: [createLiteralValueSegment(AUTHENTICATION_PROPERTIES.TYPE.name)], + value: [createLiteralValueSegment(authType)], id: guid(), }, ...values, @@ -381,8 +379,8 @@ export function parseAuthEditor(authType: AuthenticationType, items: AuthProps): const updateValues = (values: CollapsedAuthEditorItems[], property: AuthProperty, val?: ValueSegment[]) => { if (property.isRequired || (val && val.length > 0)) { values.push({ - key: [{ type: ValueSegmentType.LITERAL, id: guid(), value: property.name }], - value: val ?? [{ type: ValueSegmentType.LITERAL, id: guid(), value: '' }], + key: [createLiteralValueSegment(property.name)], + value: val ?? [createLiteralValueSegment('')], id: guid(), }); } @@ -404,32 +402,48 @@ export const serializeAuthentication = ( switch (jsonEditor.type) { case AuthenticationType.BASIC: returnItems.basic = { - basicUsername: convertStringToSegments(jsonEditor.username, nodeMap, { tokensEnabled: true }), - basicPassword: convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true }), + basicUsername: convertStringToSegments(jsonEditor.username, nodeMap, { + tokensEnabled: true, + }), + basicPassword: convertStringToSegments(jsonEditor.password, nodeMap, { + tokensEnabled: true, + }), }; break; case AuthenticationType.CERTIFICATE: returnItems.clientCertificate = { - clientCertificatePfx: convertStringToSegments(jsonEditor.pfx, nodeMap, { tokensEnabled: true }), + clientCertificatePfx: convertStringToSegments(jsonEditor.pfx, nodeMap, { + tokensEnabled: true, + }), clientCertificatePassword: convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true }), }; break; case AuthenticationType.RAW: returnItems.raw = { - rawValue: convertStringToSegments(jsonEditor.value, nodeMap, { tokensEnabled: true }), + rawValue: convertStringToSegments(jsonEditor.value, nodeMap, { + tokensEnabled: true, + }), }; break; case AuthenticationType.MSI: returnItems.msi = { msiIdentity: jsonEditor.identity, - msiAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { tokensEnabled: true }), + msiAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { + tokensEnabled: true, + }), }; break; case AuthenticationType.OAUTH: returnItems.aadOAuth = { - oauthTenant: convertStringToSegments(jsonEditor.tenant, nodeMap, { tokensEnabled: true }), - oauthAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { tokensEnabled: true }), - oauthClientId: convertStringToSegments(jsonEditor.clientId, nodeMap, { tokensEnabled: true }), + oauthTenant: convertStringToSegments(jsonEditor.tenant, nodeMap, { + tokensEnabled: true, + }), + oauthAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { + tokensEnabled: true, + }), + oauthClientId: convertStringToSegments(jsonEditor.clientId, nodeMap, { + tokensEnabled: true, + }), }; if (jsonEditor.authority) { returnItems.aadOAuth.oauthAuthority = convertStringToSegments(jsonEditor.authority, nodeMap, { tokensEnabled: true }); @@ -441,7 +455,9 @@ export const serializeAuthentication = ( if (jsonEditor.pfx && jsonEditor.password) { returnItems.aadOAuth.oauthType = AuthenticationOAuthType.CERTIFICATE; returnItems.aadOAuth.oauthTypeCertificatePfx = convertStringToSegments(jsonEditor.pfx, nodeMap, { tokensEnabled: true }); - returnItems.aadOAuth.oauthTypeCertificatePassword = convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true }); + returnItems.aadOAuth.oauthTypeCertificatePassword = convertStringToSegments(jsonEditor.password, nodeMap, { + tokensEnabled: true, + }); } break; default: diff --git a/libs/designer-ui/src/lib/card/addActionCard/index.tsx b/libs/designer-ui/src/lib/card/addActionCard/index.tsx index d1766ceb4c9..657ddcbccfc 100644 --- a/libs/designer-ui/src/lib/card/addActionCard/index.tsx +++ b/libs/designer-ui/src/lib/card/addActionCard/index.tsx @@ -1,8 +1,8 @@ -import { convertUIElementNameToAutomationId } from '../../utils'; import { useCardKeyboardInteraction } from '../hooks'; import { getCardStyle } from '../utils'; import AddNodeIcon from './addNodeIcon.svg'; import { TooltipHost, DirectionalHint, css } from '@fluentui/react'; +import { replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; import { useIntl } from 'react-intl'; export const ADD_CARD_TYPE = { @@ -108,7 +108,7 @@ export const AddActionCard: React.FC = ({ addCardType, onCli className={css('msla-panel-card-container', selected && 'msla-panel-card-container-selected')} style={getCardStyle(brandColor)} data-testid={`card-${title}`} - data-automation-id={`card-${convertUIElementNameToAutomationId(title)}`} + data-automation-id={`card-${replaceWhiteSpaceWithUnderscore(title)}`} onClick={handleClick} onKeyDown={keyboardInteraction.keyDown} onKeyUp={keyboardInteraction.keyUp} diff --git a/libs/designer-ui/src/lib/card/index.tsx b/libs/designer-ui/src/lib/card/index.tsx index 3dade95f849..340c743eed9 100644 --- a/libs/designer-ui/src/lib/card/index.tsx +++ b/libs/designer-ui/src/lib/card/index.tsx @@ -1,5 +1,4 @@ import { StatusPill } from '../monitoring'; -import { convertUIElementNameToAutomationId } from '../utils'; import { CardContextMenu } from './cardcontextmenu'; import { CardFooter } from './cardfooter'; import { ErrorBanner } from './errorbanner'; @@ -11,6 +10,7 @@ import type { ISpinnerStyles, MessageBarType } from '@fluentui/react'; import { Icon, css } from '@fluentui/react'; import { Spinner } from '@fluentui/react-components'; import type { LogicAppsV2 } from '@microsoft/logic-apps-shared'; +import { replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; import { useEffect, useMemo, useRef } from 'react'; import type { ConnectDragPreview, ConnectDragSource } from 'react-dnd'; import { useIntl } from 'react-intl'; @@ -180,7 +180,7 @@ export const Card: React.FC = ({ )} style={getCardStyle(brandColor)} data-testid={`card-${title}`} - data-automation-id={`card-${convertUIElementNameToAutomationId(title)}`} + data-automation-id={`card-${replaceWhiteSpaceWithUnderscore(title)}`} onClick={handleClick} onContextMenu={contextMenu.handle} onKeyDown={keyboardInteraction.keyDown} diff --git a/libs/designer-ui/src/lib/card/noActionCard/index.tsx b/libs/designer-ui/src/lib/card/noActionCard/index.tsx index 4da4b9d7942..145513c920a 100644 --- a/libs/designer-ui/src/lib/card/noActionCard/index.tsx +++ b/libs/designer-ui/src/lib/card/noActionCard/index.tsx @@ -1,4 +1,4 @@ -import { convertUIElementNameToAutomationId } from '../../utils'; +import { replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; import { useIntl } from 'react-intl'; export const NoActionCard: React.FC = () => { @@ -16,7 +16,7 @@ export const NoActionCard: React.FC = () => { aria-label={triggerTitle} className="msla-panel-card-container--no-action" data-testid={`card-${triggerTitle}`} - data-automation-id={`card-${convertUIElementNameToAutomationId(triggerTitle)}`} + data-automation-id={`card-${replaceWhiteSpaceWithUnderscore(triggerTitle)}`} tabIndex={0} >
diff --git a/libs/designer-ui/src/lib/code/codeeditor.less b/libs/designer-ui/src/lib/code/codeeditor.less index 2be6577d34e..3e80940673b 100644 --- a/libs/designer-ui/src/lib/code/codeeditor.less +++ b/libs/designer-ui/src/lib/code/codeeditor.less @@ -1,6 +1,7 @@ @import (reference) '../variables.less'; -.msla-code-editor-body { +.msla-code-editor-body, +.msla-custom-code-editor-body { position: relative; .msla-monaco { border: 1px solid @defaultBorderColor; @@ -17,3 +18,18 @@ margin-left: 0px; } } + +.msla-custom-code-editor-body { + .msla-custom-code-editor-file { + display: flex; + align-items: center; + margin-bottom: 15px; + .msla-custom-code-editor-fileName { + color: #0078d4; + font-size: 14px; + } + } + .msla-custom-code-editor-message-bar { + margin-top: 30px; + } +} diff --git a/libs/designer-ui/src/lib/code/index.tsx b/libs/designer-ui/src/lib/code/index.tsx index 0d5a7053d27..dc5d00fb6e0 100644 --- a/libs/designer-ui/src/lib/code/index.tsx +++ b/libs/designer-ui/src/lib/code/index.tsx @@ -1,18 +1,32 @@ +import constants from '../constants'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import type { BaseEditorProps } from '../editor/base'; import TokenPickerButtonLegacy from '../editor/base/plugins/TokenPickerButtonLegacy'; -import type { EditorContentChangedEventArgs, EditorLanguage } from '../editor/monaco'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; +import type { EditorContentChangedEventArgs } from '../editor/monaco'; import { MonacoEditor } from '../editor/monaco'; import { useId } from '../useId'; -import { buildInlineCodeTextFromToken, getEditorHeight, getInitialValue } from './util'; +import { buildInlineCodeTextFromToken, getCodeEditorHeight, getInitialValue } from './util'; +import { Icon, MessageBar, MessageBarType } from '@fluentui/react'; +import type { EditorLanguage } from '@microsoft/logic-apps-shared'; +import { getFileExtensionName } from '@microsoft/logic-apps-shared'; import { useFunctionalState } from '@react-hookz/web'; import type { editor, IRange } from 'monaco-editor'; -import { useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; -interface CodeEditorProps extends BaseEditorProps { +const customCodeIconStyle = { + root: { + fontSize: 20, + padding: '8px', + color: constants.PANEL_HIGHLIGHT_COLOR, + }, +}; + +export interface CodeEditorProps extends BaseEditorProps { language: EditorLanguage; + isCustomCode?: boolean; + nodeTitle?: string; } export function CodeEditor({ @@ -23,20 +37,32 @@ export function CodeEditor({ onFocus, getTokenPicker, label, + nodeTitle, + isCustomCode, }: CodeEditorProps): JSX.Element { const intl = useIntl(); const codeEditorRef = useRef(null); const editorId = useId('msla-tokenpicker-callout-location'); const callOutLabelId = useId('msla-tokenpicker-callout-label'); const [getCurrentValue, setCurrentValue] = useFunctionalState(getInitialValue(initialValue)); - const [editorHeight, setEditorHeight] = useState(getEditorHeight(getInitialValue(initialValue))); + const [editorHeight, setEditorHeight] = useState(getCodeEditorHeight(getInitialValue(initialValue))); const [showTokenPickerButton, setShowTokenPickerButton] = useState(false); const [getInTokenPicker, setInTokenPicker] = useFunctionalState(false); + const [showMessageBar, setShowMessageBar] = useState(true); + const [getFileName, setFileName] = useFunctionalState(''); + + const fileExtensionName = useMemo(() => { + return getFileExtensionName(language); + }, [language]); + + useEffect(() => { + setFileName(nodeTitle + fileExtensionName); + }, [nodeTitle, fileExtensionName, setFileName]); const handleContentChanged = (e: EditorContentChangedEventArgs): void => { if (e.value !== undefined) { setCurrentValue(e.value); - setEditorHeight(getEditorHeight(e.value)); + setEditorHeight(getCodeEditorHeight(e.value)); } }; @@ -44,7 +70,20 @@ export function CodeEditor({ if (!getInTokenPicker()) { setShowTokenPickerButton(false); } - onChange?.({ value: [{ id: 'key', type: ValueSegmentType.LITERAL, value: getCurrentValue() }] }); + if (isCustomCode) { + onChange?.({ + value: [createLiteralValueSegment(getFileName())], + viewModel: { + customCodeData: { + fileData: getCurrentValue(), + fileExtension: getFileExtensionName(language), + fileName: getFileName(), + }, + }, + }); + } else { + onChange?.({ value: [createLiteralValueSegment(getCurrentValue())] }); + } }; const handleFocus = (): void => { @@ -60,7 +99,12 @@ export function CodeEditor({ const tokenClicked = (valueSegment: ValueSegment) => { if (codeEditorRef.current && valueSegment.token) { const newText = buildInlineCodeTextFromToken(valueSegment.token, language); - codeEditorRef.current.executeEdits(null, [{ range: codeEditorRef.current.getSelection() as IRange, text: newText }]); + codeEditorRef.current.executeEdits(null, [ + { + range: codeEditorRef.current.getSelection() as IRange, + text: newText, + }, + ]); const currSelection = codeEditorRef.current.getSelection(); if (currSelection) { setTimeout(() => { @@ -83,8 +127,26 @@ export function CodeEditor({ ); }; + const messageBarText = intl.formatMessage({ + defaultMessage: 'To use modules or dependecies, please add at Custom Code Dependenncies in Portal TOC', + id: 'Mcvr0B', + description: 'This is a message to inform the user to add dependencies to use this action', + }); + + const closeButtonAriaLabel = intl.formatMessage({ + defaultMessage: 'Close', + id: 'TjkOzp', + description: 'This is the aria label for the close button in the message bar', + }); + return ( -
+
+ {isCustomCode ? ( +
+ +
{getFileName()}
+
+ ) : null} setShowMessageBar(false)} + > +
{messageBarText}
+ + ) : null}
); } diff --git a/libs/designer-ui/src/lib/code/util.ts b/libs/designer-ui/src/lib/code/util.ts index 7a29ea8ecf3..39eecc12ac4 100644 --- a/libs/designer-ui/src/lib/code/util.ts +++ b/libs/designer-ui/src/lib/code/util.ts @@ -10,6 +10,7 @@ import { equals, prettifyJsonString, UnsupportedException, + capitalizeFirstLetter, } from '@microsoft/logic-apps-shared'; const OperationCategory = { @@ -68,11 +69,18 @@ export function buildInlineCodeTextFromToken(inputToken: Token, language: string } else { property = decodePropertySegment(inputToken.name); } + const segmentedProperty = getSegmentedPropertyValue(property); switch (language) { - case constants.SWAGGER.FORMAT.JAVASCRIPT: { + case constants.PARAMETER.EDITOR_OPTIONS.LANGUAGE.JAVASCRIPT: { return formatForJavascript(property, actionName, source); } + case constants.PARAMETER.EDITOR_OPTIONS.LANGUAGE.POWERSHELL: { + return formatForPowershell(segmentedProperty, actionName, source); + } + case constants.PARAMETER.EDITOR_OPTIONS.LANGUAGE.CSHARP: { + return formatForCSharp(segmentedProperty, actionName, source); + } default: { throw new ArgumentException( @@ -107,6 +115,32 @@ function formatForJavascript(property: string, actionName?: string, source?: str return result; } +function formatForPowershell(property: string, actionName?: string, source?: string): string { + const result = `(get-WorkflowActionOutputs -actionName ${actionName ?? capitalizeFirstLetter(OperationCategory.Trigger)})${ + source ? `["${source}"]` : '' + }${property}`; + + return result; +} + +function formatForCSharp(property: string, actionName?: string, source?: string): string { + const result = `await context.GetActionResult("${actionName ?? capitalizeFirstLetter(OperationCategory.Trigger)}")${ + source ? `["${source}"]` : '' + }${property}`; + return result; +} + +const getSegmentedPropertyValue = (property: string): string => { + const splitProperty = property.split('.'); + let updatedProperty = ''; + splitProperty.forEach((segment) => { + if (segment) { + updatedProperty += `["${segment}"]`; + } + }); + return updatedProperty; +}; + function matchesOutputKey(tokenName: string): boolean { return ( equals(tokenName, OutputKeys.Queries) || @@ -140,3 +174,8 @@ export const formatValue = (input: string): string => { export const getEditorHeight = (input = ''): string => { return Math.min(Math.max(input?.split('\n').length * 20, 120), 380) + 'px'; }; + +// CodeEditor Height should be at least 12 rows high (19*12 px) but no more than 24 rows high (19*24 px). +export const getCodeEditorHeight = (input = ''): string => { + return Math.min(Math.max(input?.split('\n').length * 20, 228), 456) + 'px'; +}; diff --git a/libs/designer-ui/src/lib/combobox/index.tsx b/libs/designer-ui/src/lib/combobox/index.tsx index dd4a93f7e9b..6eafd5fda32 100644 --- a/libs/designer-ui/src/lib/combobox/index.tsx +++ b/libs/designer-ui/src/lib/combobox/index.tsx @@ -3,11 +3,12 @@ import { ValueSegmentType } from '../editor'; import type { BaseEditorProps, CallbackHandler, ChangeHandler } from '../editor/base'; import { EditorWrapper } from '../editor/base/EditorWrapper'; import { EditorChangePlugin } from '../editor/base/plugins/EditorChange'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import type { IComboBox, IComboBoxOption, IComboBoxOptionStyles, IComboBoxStyles } from '@fluentui/react'; import { SelectableOptionMenuItemType, ComboBox } from '@fluentui/react'; import { Button, Spinner, Tooltip } from '@fluentui/react-components'; import { bundleIcon, Dismiss24Filled, Dismiss24Regular } from '@fluentui/react-icons'; -import { getIntl, guid } from '@microsoft/logic-apps-shared'; +import { getIntl } from '@microsoft/logic-apps-shared'; import { useRef, useState, useCallback, useMemo, useEffect } from 'react'; import type { FormEvent } from 'react'; import { useIntl } from 'react-intl'; @@ -208,7 +209,7 @@ export const Combobox = ({ const handleOptionSelect = (_event: FormEvent, option?: IComboBoxOption): void => { if (option?.data === 'customrender') { - setValue([{ id: guid(), type: ValueSegmentType.LITERAL, value: option.key === 'customValue' ? '' : option.key.toString() }]); + setValue([createLiteralValueSegment(option.key === 'customValue' ? '' : option.key.toString())]); setMode(Mode.Custom); setCanAutoFocus(true); } else { @@ -217,13 +218,7 @@ export const Combobox = ({ setSelectedKey(currSelectedKey); setMode(Mode.Default); onChange?.({ - value: [ - { - id: guid(), - type: ValueSegmentType.LITERAL, - value: currSelectedKey ? getSelectedValue(options, currSelectedKey).toString() : '', - }, - ], + value: [createLiteralValueSegment(currSelectedKey ? getSelectedValue(options, currSelectedKey).toString() : '')], }); } } @@ -231,7 +226,7 @@ export const Combobox = ({ const handleOptionMultiSelect = (_event: FormEvent, option?: IComboBoxOption): void => { if (option?.data === 'customrender') { - setValue([{ id: guid(), type: ValueSegmentType.LITERAL, value: option.key === 'customValue' ? '' : option.key.toString() }]); + setValue([createLiteralValueSegment(option.key === 'customValue' ? '' : option.key.toString())]); setMode(Mode.Custom); setCanAutoFocus(true); } else { @@ -244,11 +239,9 @@ export const Combobox = ({ const selectedValues = newKeys.map((key) => getSelectedValue(options, key)); onChange?.({ value: [ - { - id: guid(), - value: serialization?.valueType === 'array' ? JSON.stringify(selectedValues) : selectedValues.join(serialization?.separator), - type: ValueSegmentType.LITERAL, - }, + createLiteralValueSegment( + serialization?.valueType === 'array' ? JSON.stringify(selectedValues) : selectedValues.join(serialization?.separator) + ), ], }); } @@ -268,13 +261,7 @@ export const Combobox = ({ comboBoxRef.current?.focus(true); setMode(Mode.Default); onChange?.({ - value: [ - { - id: guid(), - type: ValueSegmentType.LITERAL, - value: '', - }, - ], + value: [createLiteralValueSegment('')], }); }; diff --git a/libs/designer-ui/src/lib/constants.ts b/libs/designer-ui/src/lib/constants.ts index 1e3c84624c0..a947be19f33 100644 --- a/libs/designer-ui/src/lib/constants.ts +++ b/libs/designer-ui/src/lib/constants.ts @@ -433,6 +433,34 @@ export default { VALUE_OFFSET: -7, }, DROPDOWN_CALLOUT_MAX_HEIGHT: 380, + + PARAMETER: { + EDITOR: { + ARRAY: 'array', + AUTHENTICATION: 'authentication', + CODE: 'code', + COMBOBOX: 'combobox', + COPYABLE: 'copyable', + CONDITION: 'condition', + DICTIONARY: 'dictionary', + DROPDOWN: 'dropdown', + FLOATINGACTIONMENU: 'floatingactionmenu', + FILEPICKER: 'filepicker', + HTML: 'html', + RECURRENCE: 'recurrence', + SCHEMA: 'schema', + STRING: 'string', + TABLE: 'table', + }, + EDITOR_OPTIONS: { + LANGUAGE: { + CSHARP: 'csharp', + JAVASCRIPT: 'javascript', + JSON: 'json', + POWERSHELL: 'powershell', + }, + }, + }, }; export const FxBrandColor = '#AD008C'; diff --git a/libs/designer-ui/src/lib/dictionary/plugins/CollapsedDictionaryValidation.tsx b/libs/designer-ui/src/lib/dictionary/plugins/CollapsedDictionaryValidation.tsx index c2fe49d6e89..5edda5c4bda 100644 --- a/libs/designer-ui/src/lib/dictionary/plugins/CollapsedDictionaryValidation.tsx +++ b/libs/designer-ui/src/lib/dictionary/plugins/CollapsedDictionaryValidation.tsx @@ -1,8 +1,7 @@ import type { DictionaryEditorItemProps } from '..'; import type { ValueSegment } from '../../editor'; -import { ValueSegmentType } from '../../editor'; import { serializeEditorState } from '../../editor/base/utils/editorToSegment'; -import { getChildrenNodes } from '../../editor/base/utils/helper'; +import { createLiteralValueSegment, getChildrenNodes } from '../../editor/base/utils/helper'; import { serializeDictionary } from '../util/serializecollapseddictionary'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; @@ -46,7 +45,7 @@ export const CollapsedDictionaryValidation = ({ if (!editorString.trim().length || editorString === '{}') { setIsValid(true); setItems([{ key: [], value: [], id: guid() }]); - setCollapsedValue([{ id: guid(), type: ValueSegmentType.LITERAL, value: editorString }]); + setCollapsedValue([createLiteralValueSegment(editorString)]); } else { serializeDictionary(editor, setItems, setIsValid, keyType, valueType); setCollapsedValue(serializeEditorState(editorState)); diff --git a/libs/designer-ui/src/lib/dropdown/index.tsx b/libs/designer-ui/src/lib/dropdown/index.tsx index ba0149d17ec..cf8bbf38e61 100644 --- a/libs/designer-ui/src/lib/dropdown/index.tsx +++ b/libs/designer-ui/src/lib/dropdown/index.tsx @@ -1,9 +1,9 @@ import type { ValueSegment } from '../editor'; import { ValueSegmentType } from '../editor'; import type { ChangeHandler } from '../editor/base'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import type { IDropdownOption, IDropdownStyles } from '@fluentui/react'; import { SelectableOptionMenuItemType, Dropdown } from '@fluentui/react'; -import { guid } from '@microsoft/logic-apps-shared'; import type { FormEvent } from 'react'; import { useMemo, useState } from 'react'; @@ -75,7 +75,7 @@ export const DropdownEditor = ({ const handleOptionSelect = (_event: FormEvent, option?: IDropdownOption): void => { if (option) { setSelectedKey(option.key as string); - onChange?.({ value: [{ id: guid(), value: getSelectedValue(options, option.key as string), type: ValueSegmentType.LITERAL }] }); + onChange?.({ value: [createLiteralValueSegment(getSelectedValue(options, option.key as string))] }); } }; @@ -87,11 +87,9 @@ export const DropdownEditor = ({ const selectedValues = newKeys.map((key) => getSelectedValue(options, key)); onChange?.({ value: [ - { - id: guid(), - value: serialization?.valueType === 'array' ? JSON.stringify(selectedValues) : selectedValues.join(serialization?.separator), - type: ValueSegmentType.LITERAL, - }, + createLiteralValueSegment( + serialization?.valueType === 'array' ? JSON.stringify(selectedValues) : selectedValues.join(serialization?.separator) + ), ], }); } diff --git a/libs/designer-ui/src/lib/editor/base/utils/editorToSegment.ts b/libs/designer-ui/src/lib/editor/base/utils/editorToSegment.ts index 625f1d61247..f9a025ee5cd 100644 --- a/libs/designer-ui/src/lib/editor/base/utils/editorToSegment.ts +++ b/libs/designer-ui/src/lib/editor/base/utils/editorToSegment.ts @@ -1,6 +1,7 @@ import type { ValueSegment } from '../../models/parameter'; import { ValueSegmentType } from '../../models/parameter'; import { $isTokenNode } from '../nodes/tokenNode'; +import { createLiteralValueSegment } from './helper'; import type { SegmentParserOptions } from './parsesegments'; import { guid } from '@microsoft/logic-apps-shared'; import type { EditorState, ElementNode } from 'lexical'; @@ -19,16 +20,16 @@ const getChildrenNodesToSegments = (node: ElementNode, segments: ValueSegment[], const childNode = $getNodeByKey(child.getKey()); if (childNode && $isElementNode(childNode)) { if (!trimLiteral && /* ignore first paragraph node */ index > 0) { - segments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '\n' }); + segments.push(createLiteralValueSegment('\n')); } return getChildrenNodesToSegments(childNode, segments, trimLiteral); } if ($isTextNode(childNode)) { - segments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: trimLiteral ? childNode.__text.trim() : childNode.__text }); + segments.push(createLiteralValueSegment(trimLiteral ? childNode.__text.trim() : childNode.__text)); } else if ($isTokenNode(childNode)) { segments.push(childNode.__data); } else if ($isLineBreakNode(childNode)) { - segments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '\n' }); + segments.push(createLiteralValueSegment('\n')); } }); }; @@ -43,7 +44,7 @@ export const convertStringToSegments = ( const { tokensEnabled } = options ?? {}; if (typeof value !== 'string' || !tokensEnabled) { - return [{ id: guid(), type: ValueSegmentType.LITERAL, value }]; + return [createLiteralValueSegment(value)]; } const returnSegments: ValueSegment[] = []; @@ -91,7 +92,7 @@ export const convertStringToSegments = ( if (segmentSoFar) { // Treat anything remaining as `ValueSegmentType.LITERAL`, even if `currSegmentType` is not; this is to // ensure that if we opened a token with `@{`, but it has no end, we just treat the remaining text as a literal. - returnSegments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: segmentSoFar }); + returnSegments.push(createLiteralValueSegment(segmentSoFar)); } collapseLiteralSegments(returnSegments); diff --git a/libs/designer-ui/src/lib/editor/base/utils/helper.ts b/libs/designer-ui/src/lib/editor/base/utils/helper.ts index da36d488870..cc8bba091b3 100644 --- a/libs/designer-ui/src/lib/editor/base/utils/helper.ts +++ b/libs/designer-ui/src/lib/editor/base/utils/helper.ts @@ -6,6 +6,28 @@ import { guid } from '@microsoft/logic-apps-shared'; import type { ElementNode } from 'lexical'; import { $getNodeByKey, $isElementNode, $isLineBreakNode, $isTextNode } from 'lexical'; +/** + * Creates a literal value segment. + * @arg {string} value - The literal value. + * @arg {string} [segmentId] - The segment id. + * @return {ValueSegment} + */ +export function createLiteralValueSegment(value: string, segmentId?: string): ValueSegment { + return { + id: segmentId ? segmentId : guid(), + type: ValueSegmentType.LITERAL, + value, + }; +} + +export function createEmptyLiteralValueSegment(): ValueSegment { + return { + id: guid(), + type: ValueSegmentType.LITERAL, + value: '', + }; +} + export const removeFirstAndLast = (segments: ValueSegment[], removeFirst?: string, removeLast?: string): ValueSegment[] => { const n = segments.length - 1; segments.forEach((segment, i) => { @@ -145,7 +167,7 @@ export const insertQutationForStringType = (segments: ValueSegment[], type?: str }; const addStringLiteralSegment = (segments: ValueSegment[]): void => { - segments.push({ id: guid(), type: ValueSegmentType.LITERAL, value: `"` }); + segments.push(createLiteralValueSegment(`"`)); }; export const removeQuotes = (s: string): string => { diff --git a/libs/designer-ui/src/lib/editor/base/utils/keyvalueitem.ts b/libs/designer-ui/src/lib/editor/base/utils/keyvalueitem.ts index 6a0135e15df..54f77b0612c 100644 --- a/libs/designer-ui/src/lib/editor/base/utils/keyvalueitem.ts +++ b/libs/designer-ui/src/lib/editor/base/utils/keyvalueitem.ts @@ -1,9 +1,9 @@ import constants from '../../../constants'; import { isEmpty } from '../../../dictionary/expandeddictionary'; -import { ValueSegmentType, type ValueSegment } from '../../models/parameter'; -import { insertQutationForStringType } from './helper'; +import type { ValueSegment } from '../../models/parameter'; +import { createLiteralValueSegment, insertQutationForStringType } from './helper'; import { convertSegmentsToString } from './parsesegments'; -import { isNumber, guid, isBoolean } from '@microsoft/logic-apps-shared'; +import { isNumber, isBoolean } from '@microsoft/logic-apps-shared'; export interface KeyValueItem { id: string; @@ -17,10 +17,10 @@ export const convertKeyValueItemToSegments = (items: KeyValueItem[], keyType?: s }); if (itemsToConvert.length === 0) { - return [{ id: guid(), type: ValueSegmentType.LITERAL, value: '' }]; + return [createLiteralValueSegment('')]; } const parsedItems: ValueSegment[] = []; - parsedItems.push({ id: guid(), type: ValueSegmentType.LITERAL, value: '{\n ' }); + parsedItems.push(createLiteralValueSegment('{\n ')); for (let index = 0; index < itemsToConvert.length; index++) { const { key, value } = itemsToConvert[index]; @@ -44,11 +44,11 @@ export const convertKeyValueItemToSegments = (items: KeyValueItem[], keyType?: s insertQutationForStringType(parsedItems, convertedKeyType); parsedItems.push(...updatedKey); insertQutationForStringType(parsedItems, convertedKeyType); - parsedItems.push({ id: guid(), type: ValueSegmentType.LITERAL, value: ' : ' }); + parsedItems.push(createLiteralValueSegment(' : ')); insertQutationForStringType(parsedItems, convertedValueType); parsedItems.push(...updatedValue); insertQutationForStringType(parsedItems, convertedValueType); - parsedItems.push({ id: guid(), type: ValueSegmentType.LITERAL, value: index < itemsToConvert.length - 1 ? ',\n ' : '\n}' }); + parsedItems.push(createLiteralValueSegment(index < itemsToConvert.length - 1 ? ',\n ' : '\n}')); } return parsedItems; diff --git a/libs/designer-ui/src/lib/editor/monaco/index.tsx b/libs/designer-ui/src/lib/editor/monaco/index.tsx index 47efdd05434..6ff01012728 100644 --- a/libs/designer-ui/src/lib/editor/monaco/index.tsx +++ b/libs/designer-ui/src/lib/editor/monaco/index.tsx @@ -1,6 +1,7 @@ import Constants from '../../constants'; import { registerWorkflowLanguageProviders } from '../../workflow/languageservice/workflowlanguageservice'; import { useTheme } from '@fluentui/react'; +import { EditorLanguage } from '@microsoft/logic-apps-shared'; import Editor, { loader } from '@monaco-editor/react'; import * as monaco from 'monaco-editor'; import type { IScrollEvent, editor } from 'monaco-editor'; @@ -11,15 +12,6 @@ loader.config({ monaco }); export interface EditorContentChangedEventArgs extends editor.IModelContentChangedEvent { value?: string; } -// TODO: Add more languages -export const EditorLanguage = { - javascript: 'javascript', - json: 'json', - xml: 'xml', - templateExpressionLanguage: 'TemplateExpressionLanguage', - yaml: 'yaml', -} as const; -export type EditorLanguage = (typeof EditorLanguage)[keyof typeof EditorLanguage]; export interface MonacoProps extends MonacoOptions { className?: string; @@ -271,6 +263,7 @@ export const MonacoEditor = forwardRef void; +} + +export const PanelResizer = (props: PanelResizerProps): JSX.Element => { + const { updatePanelWidth } = props; + const styles = useStyles(); + const [isResizing, setIsResizing] = useState(false); + const startResizing = useCallback(() => setIsResizing(true), []); + const stopResizing = useCallback(() => setIsResizing(false), []); + const animationFrame = useRef(0); + + const resize = useCallback( + ({ clientX }: MouseEvent) => { + animationFrame.current = requestAnimationFrame(() => { + if (isResizing) { + const newWidth = Math.max(window.innerWidth - clientX, 400); + updatePanelWidth(newWidth.toString() + 'px'); + } + }); + }, + [isResizing, updatePanelWidth] + ); + + useEffect(() => { + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResizing); + + return () => { + cancelAnimationFrame(animationFrame.current); + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + }; + }, [resize, stopResizing]); + return ( +
{ + startResizing(); + }} + /> + ); +}; diff --git a/libs/designer-ui/src/lib/panel/panelUtil.ts b/libs/designer-ui/src/lib/panel/panelUtil.ts index 85b498bf840..fe5bb82b816 100644 --- a/libs/designer-ui/src/lib/panel/panelUtil.ts +++ b/libs/designer-ui/src/lib/panel/panelUtil.ts @@ -42,4 +42,5 @@ export interface CommonPanelProps { width: string; layerProps?: any; panelLocation: PanelLocation; + isResizeable?: boolean; } diff --git a/libs/designer-ui/src/lib/panel/panelcontainer.tsx b/libs/designer-ui/src/lib/panel/panelcontainer.tsx index 137c7747a71..28604629a16 100644 --- a/libs/designer-ui/src/lib/panel/panelcontainer.tsx +++ b/libs/designer-ui/src/lib/panel/panelcontainer.tsx @@ -1,5 +1,6 @@ import { EmptyContent } from '../card/emptycontent'; import type { PageActionTelemetryData } from '../telemetry/models'; +import { PanelResizer } from './panelResizer'; import type { CommonPanelProps, PanelTab } from './panelUtil'; import { PanelScope, PanelLocation } from './panelUtil'; import { PanelContent } from './panelcontent'; @@ -53,6 +54,8 @@ export type PanelContainerProps = { onCommentChange: (panelCommentChangeEvent?: string) => void; renderHeader?: (props?: IPanelProps, defaultrender?: IPanelHeaderRenderer, headerTextId?: string) => JSX.Element; onTitleChange: TitleChangeHandler; + onTitleBlur?: (prevTitle: string) => void; + setCurrWidth: (width: string) => void; } & CommonPanelProps; export const PanelContainer = ({ @@ -82,6 +85,9 @@ export const PanelContainer = ({ renderHeader, onCommentChange, onTitleChange, + onTitleBlur, + setCurrWidth, + isResizeable, }: PanelContainerProps) => { const intl = useIntl(); @@ -109,6 +115,7 @@ export const PanelContainer = ({ commentChange={onCommentChange} toggleCollapse={toggleCollapse} onTitleChange={onTitleChange} + onTitleBlur={onTitleBlur} /> ); }, @@ -131,6 +138,7 @@ export const PanelContainer = ({ onCommentChange, toggleCollapse, onTitleChange, + onTitleBlur, ] ); @@ -175,6 +183,7 @@ export const PanelContainer = ({ ) : ( )} + {isResizeable ? : null} )} diff --git a/libs/designer-ui/src/lib/panel/panelheader/panelheader.tsx b/libs/designer-ui/src/lib/panel/panelheader/panelheader.tsx index e3431028664..98aa7d638c2 100644 --- a/libs/designer-ui/src/lib/panel/panelheader/panelheader.tsx +++ b/libs/designer-ui/src/lib/panel/panelheader/panelheader.tsx @@ -44,6 +44,7 @@ export interface PanelHeaderProps { onRenderWarningMessage?(): JSX.Element; toggleCollapse: () => void; onTitleChange: TitleChangeHandler; + onTitleBlur?: (prevtitle: string) => void; } const DismissIcon = bundleIcon(ChevronRight24Filled, ChevronRight24Regular); @@ -72,6 +73,7 @@ export const PanelHeader = ({ onRenderWarningMessage, toggleCollapse, onTitleChange, + onTitleBlur, }: PanelHeaderProps): JSX.Element => { const intl = useIntl(); @@ -180,6 +182,7 @@ export const PanelHeader = ({ renameTitleDisabled={renameTitleDisabled} titleValue={title} onChange={onTitleChange} + onBlur={onTitleBlur} />
diff --git a/libs/designer-ui/src/lib/panel/panelheader/panelheadertitle.tsx b/libs/designer-ui/src/lib/panel/panelheader/panelheadertitle.tsx index 7556139e7cb..0e4828a50c4 100644 --- a/libs/designer-ui/src/lib/panel/panelheader/panelheadertitle.tsx +++ b/libs/designer-ui/src/lib/panel/panelheader/panelheadertitle.tsx @@ -25,6 +25,7 @@ export interface PanelHeaderTitleProps { titleValue?: string; titleId?: string; onChange: TitleChangeHandler; + onBlur?: (prevTitle: string) => void; } export const PanelHeaderTitle = ({ @@ -33,6 +34,7 @@ export const PanelHeaderTitle = ({ readOnlyMode, renameTitleDisabled, onChange, + onBlur, }: PanelHeaderTitleProps): JSX.Element => { const intl = useIntl(); @@ -40,6 +42,7 @@ export const PanelHeaderTitle = ({ const [newTitleValue, setNewTitleValue] = useState(titleValue); const [validValue, setValidValue] = useState(titleValue); + const [titleBeforeBlur, setTitleBeforeBlur] = useState(titleValue ?? ''); const [errorMessage, setErrorMessage] = useState(''); const onTitleChange = (_: React.FormEvent, newValue?: string): void => { @@ -65,6 +68,9 @@ export const PanelHeaderTitle = ({ onChange(validValue || ''); setNewTitleValue(validValue); setErrorMessage(''); + } else { + onBlur?.(titleBeforeBlur); + setTitleBeforeBlur(newTitleValue ?? ''); } }; diff --git a/libs/designer-ui/src/lib/panel/recommendationpanel/operationSearchCard/index.tsx b/libs/designer-ui/src/lib/panel/recommendationpanel/operationSearchCard/index.tsx index be60f9b4783..4fa55eb4070 100644 --- a/libs/designer-ui/src/lib/panel/recommendationpanel/operationSearchCard/index.tsx +++ b/libs/designer-ui/src/lib/panel/recommendationpanel/operationSearchCard/index.tsx @@ -1,16 +1,17 @@ import { InfoDot } from '../../../infoDot'; -import { convertUIElementNameToAutomationId, getPreviewTag } from '../../../utils'; +import { getPreviewTag } from '../../../utils'; import type { OperationActionData } from '../interfaces'; import { Text, Image } from '@fluentui/react'; import { Badge } from '@fluentui/react-components'; +import { replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; import { useIntl } from 'react-intl'; export type OperationSearchCardProps = { operationActionData: OperationActionData; - onClick: (operationId: string, apiId?: string) => void; displayRuntimeInfo: boolean; showImage?: boolean; style?: any; + onClick: (operationId: string, apiId?: string) => void; } & CommonCardProps; export interface CommonCardProps { @@ -40,7 +41,7 @@ export const OperationSearchCard = (props: OperationSearchCardProps) => { className="msla-op-search-card-container" onClick={() => onCardClick()} style={style} - data-automation-id={`msla-op-search-result-${convertUIElementNameToAutomationId(operationActionData.id)}`} + data-automation-id={`msla-op-search-result-${replaceWhiteSpaceWithUnderscore(operationActionData.id)}`} aria-label={`${title} ${description}`} >
diff --git a/libs/designer-ui/src/lib/peek/index.tsx b/libs/designer-ui/src/lib/peek/index.tsx index 9d5baffcda0..43d448edfc0 100644 --- a/libs/designer-ui/src/lib/peek/index.tsx +++ b/libs/designer-ui/src/lib/peek/index.tsx @@ -1,5 +1,6 @@ -import { MonacoEditor, EditorLanguage } from '../editor/monaco'; +import { MonacoEditor } from '../editor/monaco'; import { PrimaryButton } from '@fluentui/react/lib/Button'; +import { EditorLanguage } from '@microsoft/logic-apps-shared'; import { useIntl } from 'react-intl'; export interface PeekProps { diff --git a/libs/designer-ui/src/lib/picker/filepickereditor.tsx b/libs/designer-ui/src/lib/picker/filepickereditor.tsx index 33a6dc00d43..df5bdf69c9a 100644 --- a/libs/designer-ui/src/lib/picker/filepickereditor.tsx +++ b/libs/designer-ui/src/lib/picker/filepickereditor.tsx @@ -1,9 +1,8 @@ import type { BaseEditorProps, ChangeHandler } from '../editor/base'; import { EditorWrapper } from '../editor/base/EditorWrapper'; import { TokenPickerButtonLocation } from '../editor/base/plugins/tokenpickerbutton'; -import { notEqual } from '../editor/base/utils/helper'; +import { createLiteralValueSegment, notEqual } from '../editor/base/utils/helper'; import type { ValueSegment } from '../editor/models/parameter'; -import { ValueSegmentType } from '../editor/models/parameter'; import { Picker } from './picker'; import { PickerItemType } from './pickerItem'; import { EditorValueChange } from './plugins/EditorValueChange'; @@ -12,7 +11,7 @@ import type { IBreadcrumbItem, IIconProps, ITooltipHostStyles } from '@fluentui/ import { TooltipHost, IconButton } from '@fluentui/react'; import { useId } from '@fluentui/react-hooks'; import type { TreeDynamicValue } from '@microsoft/logic-apps-shared'; -import { equals, guid } from '@microsoft/logic-apps-shared'; +import { equals } from '@microsoft/logic-apps-shared'; import { useState } from 'react'; import { useIntl } from 'react-intl'; @@ -35,7 +34,9 @@ export interface FilePickerEditorProps extends BaseEditorProps { } const folderIcon: IIconProps = { iconName: 'FolderOpen' }; -const hostStyles: Partial = { root: { display: 'inline-block' } }; +const hostStyles: Partial = { + root: { display: 'inline-block' }, +}; const calloutProps = { gapSpace: 0 }; export const FilePickerEditor = ({ @@ -53,7 +54,7 @@ export const FilePickerEditor = ({ const pickerIconId = useId(); const intl = useIntl(); const [selectedItem, setSelectedItem] = useState(); - const initialDisplayValue = displayValue ? [{ id: guid(), value: displayValue, type: ValueSegmentType.LITERAL }] : initialValue; + const initialDisplayValue = displayValue ? [createLiteralValueSegment(displayValue)] : initialValue; const [editorDisplayValue, setEditorDisplayValue] = useState(initialDisplayValue); const [pickerDisplayValue, setPickerDisplayValue] = useState(initialDisplayValue); const [showPicker, setShowPicker] = useState(false); @@ -79,7 +80,14 @@ export const FilePickerEditor = ({ const onFolderNavigated = (selectedItem: TreeDynamicValue) => { onFolderNavigation(selectedItem.value); const displayValue = selectedItem.displayName; - setTitleSegments([...titleSegments, { text: displayValue, key: displayValue, onClick: () => onFolderNavigated(selectedItem) }]); + setTitleSegments([ + ...titleSegments, + { + text: displayValue, + key: displayValue, + onClick: () => onFolderNavigated(selectedItem), + }, + ]); }; const onFileFolderSelected = (selectedItem: TreeDynamicValue) => { @@ -88,20 +96,21 @@ export const FilePickerEditor = ({ } if (showPicker) { setSelectedItem(selectedItem.value); - setPickerDisplayValue([{ id: guid(), value: getDisplayValueFromSelectedItem(selectedItem.value), type: ValueSegmentType.LITERAL }]); + setPickerDisplayValue([createLiteralValueSegment(getDisplayValueFromSelectedItem(selectedItem.value))]); setShowPicker(false); } }; const handleBlur = () => { if (selectedItem) { - const valueSegmentValue: ValueSegment[] = [ - { id: guid(), type: ValueSegmentType.LITERAL, value: getValueFromSelectedItem(selectedItem) }, - ]; + const valueSegmentValue: ValueSegment[] = [createLiteralValueSegment(getValueFromSelectedItem(selectedItem))]; editorBlur?.({ value: valueSegmentValue, - viewModel: { displayValue: pickerDisplayValue[0]?.value, selectedItem: selectedItem }, + viewModel: { + displayValue: pickerDisplayValue[0]?.value, + selectedItem: selectedItem, + }, }); } else if (notEqual(editorDisplayValue, pickerDisplayValue)) { editorBlur?.({ @@ -116,7 +125,11 @@ export const FilePickerEditor = ({ setPickerDisplayValue([]); }; - const openFolderLabel = intl.formatMessage({ defaultMessage: 'Open folder', id: 's+4LEa', description: 'Open folder label' }); + const openFolderLabel = intl.formatMessage({ + defaultMessage: 'Open folder', + id: 's+4LEa', + description: 'Open folder label', + }); return (
{ const containerRef = useRef(null); const [heights, setHeights] = useState([]); @@ -17,7 +14,7 @@ export const HybridQueryBuilderEditor = ({ getTokenPicker, groupProps, readonly, const [getRootProp, setRootProp] = useFunctionalState(groupProps); useUpdateEffect(() => { - onChange?.({ value: emptyValue, viewModel: JSON.parse(JSON.stringify({ items: getRootProp() })) }); + onChange?.({ value: [createEmptyLiteralValueSegment()], viewModel: JSON.parse(JSON.stringify({ items: getRootProp() })) }); setHeights(checkHeights(getRootProp(), [], 0)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [getRootProp()]); diff --git a/libs/designer-ui/src/lib/querybuilder/Row.tsx b/libs/designer-ui/src/lib/querybuilder/Row.tsx index 216943ff9fb..7ec7b865544 100644 --- a/libs/designer-ui/src/lib/querybuilder/Row.tsx +++ b/libs/designer-ui/src/lib/querybuilder/Row.tsx @@ -3,17 +3,14 @@ import { GroupType } from '.'; import { Checkbox } from '../checkbox'; import constants from '../constants'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import type { ChangeState, GetTokenPickerHandler } from '../editor/base'; import { TokenPickerButtonLocation } from '../editor/base/plugins/tokenpickerbutton'; -import { notEqual } from '../editor/base/utils/helper'; +import { createEmptyLiteralValueSegment, notEqual } from '../editor/base/utils/helper'; import { StringEditor } from '../editor/string'; -// import type { MoveOption } from './Group'; import { RowDropdown, RowDropdownOptions } from './RowDropdown'; import { operandNotEmpty } from './helper'; import type { ICalloutProps, IIconProps, IOverflowSetItemProps, IOverflowSetStyles } from '@fluentui/react'; import { css, IconButton, DirectionalHint, TooltipHost, OverflowSet } from '@fluentui/react'; -import { guid } from '@microsoft/logic-apps-shared'; import { useState } from 'react'; import { useIntl } from 'react-intl'; @@ -28,8 +25,6 @@ const menuIconProps: IIconProps = { iconName: 'More', }; -const emptyValueSegmentArray: ValueSegment[] = [{ type: ValueSegmentType.LITERAL, value: '', id: guid() }]; - type RowProps = { checked?: boolean; operand1?: ValueSegment[]; @@ -177,7 +172,7 @@ export const Row = ({ checked: checked, operand1: newState.value, operator: operator ?? 'equals', - operand2: operandNotEmpty(operand2) ? operand2 : emptyValueSegmentArray, + operand2: operandNotEmpty(operand2) ? operand2 : [createEmptyLiteralValueSegment()], }, index ); @@ -189,9 +184,9 @@ export const Row = ({ { type: GroupType.ROW, checked: checked, - operand1: operandNotEmpty(operand1) ? operand1 : emptyValueSegmentArray, + operand1: operandNotEmpty(operand1) ? operand1 : [createEmptyLiteralValueSegment()], operator: newState.value[0].value, - operand2: operandNotEmpty(operand2) ? operand2 : emptyValueSegmentArray, + operand2: operandNotEmpty(operand2) ? operand2 : [createEmptyLiteralValueSegment()], }, index ); @@ -203,7 +198,7 @@ export const Row = ({ { type: GroupType.ROW, checked: checked, - operand1: operandNotEmpty(operand1) ? operand1 : emptyValueSegmentArray, + operand1: operandNotEmpty(operand1) ? operand1 : [createEmptyLiteralValueSegment()], operator: operator ?? 'equals', operand2: newState.value, }, diff --git a/libs/designer-ui/src/lib/querybuilder/RowDropdown.tsx b/libs/designer-ui/src/lib/querybuilder/RowDropdown.tsx index 1105c427f44..f7f9a2003b8 100644 --- a/libs/designer-ui/src/lib/querybuilder/RowDropdown.tsx +++ b/libs/designer-ui/src/lib/querybuilder/RowDropdown.tsx @@ -1,8 +1,7 @@ import type { DropdownItem } from '../dropdown'; import { DropdownEditor } from '../dropdown'; -import { ValueSegmentType } from '../editor'; import type { ChangeHandler } from '../editor/base'; -import { guid } from '@microsoft/logic-apps-shared'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; interface RowDropdownProps { condition?: string; @@ -46,11 +45,7 @@ export const RowDropdown = ({ condition, disabled, onChange }: RowDropdownProps)
{ const { operator, operand1, operand2 } = rootProps; const negatory = operator.includes('not'); - const op1: ValueSegment = getOperationValue(operand1[0]) ?? { id: guid(), type: ValueSegmentType.LITERAL, value: '' }; - const separatorLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `,` }; - const op2: ValueSegment = getOperationValue(operand2[0]) ?? { id: guid(), type: ValueSegmentType.LITERAL, value: '' }; + const op1: ValueSegment = getOperationValue(operand1[0]) ?? createLiteralValueSegment(''); + const separatorLiteral: ValueSegment = createLiteralValueSegment(`,`); + const op2: ValueSegment = getOperationValue(operand2[0]) ?? createLiteralValueSegment(''); if (negatory) { const newOperator = operator.replace('not', ''); - const negatoryOperatorLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `@not(${newOperator}(` }; - const endingLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `))` }; + const negatoryOperatorLiteral: ValueSegment = createLiteralValueSegment(`@not(${newOperator}(`); + const endingLiteral: ValueSegment = createLiteralValueSegment(`))`); return [negatoryOperatorLiteral, op1, separatorLiteral, op2, endingLiteral]; } else { - const operatorLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `@${operator}(` }; - const endingLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `)` }; + const operatorLiteral: ValueSegment = createLiteralValueSegment(`@${operator}(`); + const endingLiteral: ValueSegment = createLiteralValueSegment(`)`); return [operatorLiteral, op1, separatorLiteral, op2, endingLiteral]; } }; @@ -188,8 +189,8 @@ const convertAdvancedValueToRootProp = (value: ValueSegment[]): RowItemProps | u const operandSubstring = stringValue.substring(stringValue.indexOf('(') + 1, nthLastIndexOf(stringValue, ')', negatory ? 2 : 1)); const operand1String = removeQuotes(operandSubstring.substring(0, getOuterMostCommaIndex(operandSubstring)).trim()); const operand2String = removeQuotes(operandSubstring.substring(getOuterMostCommaIndex(operandSubstring) + 1).trim()); - operand1 = [nodeMap.get(operand1String) ?? { id: guid(), type: ValueSegmentType.LITERAL, value: operand1String }]; - operand2 = [nodeMap.get(operand2String) ?? { id: guid(), type: ValueSegmentType.LITERAL, value: operand2String }]; + operand1 = [nodeMap.get(operand1String) ?? createLiteralValueSegment(operand1String)]; + operand2 = [nodeMap.get(operand2String) ?? createLiteralValueSegment(operand2String)]; } catch { return undefined; } diff --git a/libs/designer-ui/src/lib/querybuilder/index.tsx b/libs/designer-ui/src/lib/querybuilder/index.tsx index bf256472c88..53df2b77515 100644 --- a/libs/designer-ui/src/lib/querybuilder/index.tsx +++ b/libs/designer-ui/src/lib/querybuilder/index.tsx @@ -1,11 +1,10 @@ import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import type { ChangeHandler, GetTokenPickerHandler } from '../editor/base'; +import { createEmptyLiteralValueSegment } from '../editor/base/utils/helper'; import { Group } from './Group'; import { GroupDropdownOptions } from './GroupDropdown'; import { RowDropdownOptions } from './RowDropdown'; import { checkHeights, getGroupedItems } from './helper'; -import { guid } from '@microsoft/logic-apps-shared'; import { useFunctionalState, useUpdateEffect } from '@react-hookz/web'; import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -49,8 +48,6 @@ export interface QueryBuilderProps { showDescription?: boolean; } -const emptyValue = [{ id: guid(), type: ValueSegmentType.LITERAL, value: '' }]; - export const QueryBuilderEditor = ({ getTokenPicker, groupProps, @@ -68,7 +65,7 @@ export const QueryBuilderEditor = ({ const [getRootProp, setRootProp] = useFunctionalState(groupProps); useUpdateEffect(() => { - onChange?.({ value: emptyValue, viewModel: JSON.parse(JSON.stringify({ items: getRootProp() })) }); + onChange?.({ value: [createEmptyLiteralValueSegment()], viewModel: JSON.parse(JSON.stringify({ items: getRootProp() })) }); setHeights(checkHeights(getRootProp(), [], 0)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [getRootProp()]); diff --git a/libs/designer-ui/src/lib/recurrence/index.tsx b/libs/designer-ui/src/lib/recurrence/index.tsx index 155d609207a..2b2149d2c10 100644 --- a/libs/designer-ui/src/lib/recurrence/index.tsx +++ b/libs/designer-ui/src/lib/recurrence/index.tsx @@ -1,7 +1,7 @@ import constants from '../constants'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import type { ChangeHandler } from '../editor/base'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import { DropdownControl, DropdownType } from './dropdownControl'; import { Preview } from './preview'; import { MinuteTextInput, TextInput } from './textInput'; @@ -13,7 +13,6 @@ import { getScheduleDayValues, getScheduleHourValues, getTimezoneValues, - guid, RecurrenceType, } from '@microsoft/logic-apps-shared'; import { useState } from 'react'; @@ -51,7 +50,7 @@ export const ScheduleEditor = ({ const updateRecurrence = (newRecurrence: Recurrence) => { setRecurrence(newRecurrence); - onChange?.({ value: [{ id: guid(), type: ValueSegmentType.LITERAL, value: JSON.stringify(newRecurrence) }] }); + onChange?.({ value: [createLiteralValueSegment(JSON.stringify(newRecurrence))] }); }; const renderScheduleSection = (): JSX.Element | null => { diff --git a/libs/designer-ui/src/lib/schemaeditor/index.tsx b/libs/designer-ui/src/lib/schemaeditor/index.tsx index 08ab695f635..ca05798471d 100644 --- a/libs/designer-ui/src/lib/schemaeditor/index.tsx +++ b/libs/designer-ui/src/lib/schemaeditor/index.tsx @@ -1,15 +1,16 @@ import { formatValue, getEditorHeight, getInitialValue } from '../code/util'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; import type { ChangeHandler } from '../editor/base'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import type { EditorContentChangedEventArgs } from '../editor/monaco'; -import { MonacoEditor, EditorLanguage } from '../editor/monaco'; +import { MonacoEditor } from '../editor/monaco'; import { ModalDialog } from '../modaldialog'; import { generateSchemaFromJsonString } from '../workflow/schema/generator'; import type { IDialogStyles, IStyle } from '@fluentui/react'; import type { IButtonStyles } from '@fluentui/react/lib/Button'; import { ActionButton } from '@fluentui/react/lib/Button'; import { FontSizes } from '@fluentui/theme'; +import { EditorLanguage } from '@microsoft/logic-apps-shared'; import { useFunctionalState } from '@react-hookz/web'; import type { editor } from 'monaco-editor'; import { useRef, useState } from 'react'; @@ -81,7 +82,7 @@ export function SchemaEditor({ readonly, label, initialValue, onChange, onFocus }; const handleBlur = (): void => { - onChange?.({ value: [{ id: 'key', type: ValueSegmentType.LITERAL, value: getCurrentValue() }] }); + onChange?.({ value: [createLiteralValueSegment(getCurrentValue())] }); }; const handleFocus = (): void => { @@ -101,7 +102,7 @@ export function SchemaEditor({ readonly, label, initialValue, onChange, onFocus const stringifiedJsonSchema = formatValue(JSON.stringify(jsonSchema, null, 4)); setCurrentValue(stringifiedJsonSchema); setEditorHeight(getEditorHeight(stringifiedJsonSchema)); - onChange?.({ value: [{ id: 'key', type: ValueSegmentType.LITERAL, value: stringifiedJsonSchema }] }); + onChange?.({ value: [createLiteralValueSegment(stringifiedJsonSchema)] }); } catch (ex) { const error = intl.formatMessage({ defaultMessage: 'Unable to generate schema', diff --git a/libs/designer-ui/src/lib/settings/settingsection/index.tsx b/libs/designer-ui/src/lib/settings/settingsection/index.tsx index 6935db48039..4c11c5907d0 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/index.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/index.tsx @@ -24,3 +24,10 @@ export type { SettingTokenFieldProps as SettingTokenTextFieldProps } from './set export { SettingDropdown } from './settingdropdown'; export type { SettingDropdownProps, DropdownSelectionChangeHandler } from './settingdropdown'; export { toCustomEditorAndOptions, isCustomEditor } from './customTokenField'; + +export interface SettingProps { + readOnly?: boolean; + ariaLabel?: string; + customLabel?: JSX.Element; + nodeTitle?: string; +} diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingTokenField.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingTokenField.tsx index 3a11735e197..2ae56df85d5 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingTokenField.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingTokenField.tsx @@ -2,13 +2,14 @@ import { ArrayEditor } from '../../arrayeditor'; import { AuthenticationEditor } from '../../authentication'; import { CodeEditor } from '../../code'; import { Combobox } from '../../combobox'; +import constants from '../../constants'; import { CopyInputControl } from '../../copyinputcontrol'; import { DictionaryEditor } from '../../dictionary'; import { DropdownEditor } from '../../dropdown'; import type { ValueSegment } from '../../editor'; import type { CallbackHandler, CastHandler, ChangeHandler, GetTokenPickerHandler } from '../../editor/base'; import type { TokenPickerButtonEditorProps } from '../../editor/base/plugins/tokenpickerbutton'; -import { EditorLanguage } from '../../editor/monaco'; +import { createLiteralValueSegment } from '../../editor/base/utils/helper'; import { StringEditor } from '../../editor/string'; import { FloatingActionMenuKind } from '../../floatingactionmenu/constants'; import { FloatingActionMenuInputs } from '../../floatingactionmenu/floatingactionmenuinputs'; @@ -24,11 +25,10 @@ import { SchemaEditor } from '../../schemaeditor'; import { TableEditor } from '../../table'; import type { TokenGroup } from '../../tokenpicker/models/token'; import { useId } from '../../useId'; -import { convertUIElementNameToAutomationId } from '../../utils'; +import type { SettingProps } from './'; import { CustomTokenField, isCustomEditor } from './customTokenField'; -import type { SettingProps } from './settingtoggle'; import { Label } from '@fluentui/react'; -import { equals, getPropertyValue } from '@microsoft/logic-apps-shared'; +import { EditorLanguage, equals, getPropertyValue, replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; export interface SettingTokenFieldProps extends SettingProps { id?: string; @@ -83,6 +83,7 @@ export const SettingTokenField = ({ ...props }: SettingTokenFieldProps) => { export type TokenFieldProps = SettingTokenFieldProps & { labelId: string }; export const TokenField = ({ + nodeTitle, editor, editorOptions, editorViewModel, @@ -106,105 +107,10 @@ export const TokenField = ({ suppressCastingForSerialize, }: TokenFieldProps) => { const dropdownOptions = editorOptions?.options?.value ?? editorOptions?.options ?? []; - const labelForAutomationId = convertUIElementNameToAutomationId(label); + const labelForAutomationId = replaceWhiteSpaceWithUnderscore(label); switch (editor?.toLowerCase()) { - case 'copyable': - return ; - - case 'dropdown': - return ( - ({ key: index.toString(), ...option }))} - multiSelect={!!getPropertyValue(editorOptions, 'multiSelect')} - serialization={editorOptions?.serialization} - onChange={onValueChange} - dataAutomationId={`msla-setting-token-editor-dropdowneditor-${labelForAutomationId}`} - /> - ); - - case 'code': - return ( - - ); - - case 'combobox': - return ( - ({ key: index.toString(), ...option }))} - useOption={true} - isLoading={isLoading} - errorDetails={errorDetails} - getTokenPicker={getTokenPicker} - onChange={onValueChange} - onMenuOpen={onComboboxMenuOpen} - multiSelect={getPropertyValue(editorOptions, 'multiSelect')} - serialization={editorOptions?.serialization} - dataAutomationId={`msla-setting-token-editor-combobox-${labelForAutomationId}`} - tokenMapping={tokenMapping} - loadParameterValueFromString={loadParameterValueFromString} - /> - ); - - case 'schema': - return ; - - case 'dictionary': - return ( - - ); - - case 'table': - return ( - - ); - - case 'array': + case constants.PARAMETER.EDITOR.ARRAY: return ( ); - case 'authentication': + case constants.PARAMETER.EDITOR.AUTHENTICATION: return ( ); + case constants.PARAMETER.EDITOR.CODE: + return (() => { + const isCustomCode = editorOptions?.language !== constants.PARAMETER.EDITOR_OPTIONS.LANGUAGE.JAVASCRIPT; + const initialValue = + editorOptions?.language && isCustomCode ? [createLiteralValueSegment(editorViewModel?.customCodeData?.fileData)] : value; + const language = editorOptions.language ?? EditorLanguage.javascript; + + return ( + + ); + })(); + + case constants.PARAMETER.EDITOR.COMBOBOX: + return ( + ({ key: index.toString(), ...option }))} + useOption={true} + isLoading={isLoading} + errorDetails={errorDetails} + getTokenPicker={getTokenPicker} + onChange={onValueChange} + onMenuOpen={onComboboxMenuOpen} + multiSelect={getPropertyValue(editorOptions, 'multiSelect')} + serialization={editorOptions?.serialization} + dataAutomationId={`msla-setting-token-editor-combobox-${labelForAutomationId}`} + tokenMapping={tokenMapping} + loadParameterValueFromString={loadParameterValueFromString} + /> + ); + + case constants.PARAMETER.EDITOR.COPYABLE: + return ; - case 'condition': + case constants.PARAMETER.EDITOR.CONDITION: return editorViewModel.isOldFormat ? ( ); + case constants.PARAMETER.EDITOR.DICTIONARY: + return ( + + ); - case 'recurrence': + case constants.PARAMETER.EDITOR.DROPDOWN: return ( - ({ key: index.toString(), ...option }))} + multiSelect={!!getPropertyValue(editorOptions, 'multiSelect')} + serialization={editorOptions?.serialization} + onChange={onValueChange} + dataAutomationId={`msla-setting-token-editor-dropdowneditor-${labelForAutomationId}`} + /> + ); + + case constants.PARAMETER.EDITOR.FLOATINGACTIONMENU: { + return editorOptions?.menuKind === FloatingActionMenuKind.outputs ? ( + + ) : ( + ); + } - case 'filepicker': + case constants.PARAMETER.EDITOR.FILEPICKER: return ( ); - case 'html': + + case constants.PARAMETER.EDITOR.HTML: return ( ); - case 'floatingactionmenu': { - return editorOptions?.menuKind === FloatingActionMenuKind.outputs ? ( - - ) : ( - ; + + case constants.PARAMETER.EDITOR.TABLE: + return ( + ); - } default: return ( diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingdictionary.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingdictionary.tsx index 62b26407939..0e4fdbfcf77 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingdictionary.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingdictionary.tsx @@ -1,6 +1,6 @@ import type { EventHandler } from '../..'; +import type { SettingProps } from './'; import { SimpleDictionary } from './dictionary/simpledictionary'; -import type { SettingProps } from './settingtoggle'; import { TextField } from '@fluentui/react'; import type { ITextFieldStyles } from '@fluentui/react'; import { isObject } from '@microsoft/logic-apps-shared'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingdropdown.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingdropdown.tsx index fd4bbf7a49b..799844ac5e6 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingdropdown.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingdropdown.tsx @@ -1,4 +1,4 @@ -import type { SettingProps } from './settingtoggle'; +import type { SettingProps } from './'; import { Dropdown } from '@fluentui/react'; import type { IDropdownOption } from '@fluentui/react'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingexpressioneditor.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingexpressioneditor.tsx index 81bc5f82975..fb945ffad49 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingexpressioneditor.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingexpressioneditor.tsx @@ -1,4 +1,4 @@ -import type { SettingProps } from './settingtoggle'; +import type { SettingProps } from './'; import { ActionButton, IconButton } from '@fluentui/react/lib/Button'; import type { IIconProps } from '@fluentui/react/lib/Icon'; import { TextField } from '@fluentui/react/lib/TextField'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingmultiselect.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingmultiselect.tsx index 6feedecd85a..89500d20da3 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingmultiselect.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingmultiselect.tsx @@ -1,4 +1,4 @@ -import type { SettingProps } from './settingtoggle'; +import type { SettingProps } from './'; import { Checkbox } from '@fluentui/react'; import React, { useState } from 'react'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingreactiveinput.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingreactiveinput.tsx index e5296e26e20..f4cddf5cf1a 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingreactiveinput.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingreactiveinput.tsx @@ -1,7 +1,7 @@ +import type { SettingProps, ToggleChangeHandler } from './'; import { SettingTextField } from './settingtextfield'; import type { TextInputChangeHandler } from './settingtextfield'; import { SettingToggle } from './settingtoggle'; -import type { SettingProps, ToggleChangeHandler } from './settingtoggle'; import { useState } from 'react'; export interface ReactiveToggleProps extends SettingProps { diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingslider.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingslider.tsx index 1b5ef51fef7..322f9afb265 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingslider.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingslider.tsx @@ -1,4 +1,4 @@ -import type { SettingProps } from './settingtoggle'; +import type { SettingProps } from './'; import { Slider, TextField } from '@fluentui/react'; import { useCallback, useState } from 'react'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingtextfield.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingtextfield.tsx index 05aa20acb45..57ea8be166c 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingtextfield.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingtextfield.tsx @@ -1,4 +1,4 @@ -import type { SettingProps } from './settingtoggle'; +import type { SettingProps } from './'; import { TextField } from '@fluentui/react'; import React, { useState } from 'react'; import type { FormEvent } from 'react'; diff --git a/libs/designer-ui/src/lib/settings/settingsection/settingtoggle.tsx b/libs/designer-ui/src/lib/settings/settingsection/settingtoggle.tsx index adda4dc822c..9665370a66c 100644 --- a/libs/designer-ui/src/lib/settings/settingsection/settingtoggle.tsx +++ b/libs/designer-ui/src/lib/settings/settingsection/settingtoggle.tsx @@ -1,15 +1,10 @@ +import type { SettingProps } from './'; import type { IToggleProps } from '@fluentui/react'; import { Toggle } from '@fluentui/react'; import { useIntl } from 'react-intl'; export type ToggleChangeHandler = (e: React.MouseEvent, checked?: boolean) => void; -export interface SettingProps { - readOnly?: boolean; - ariaLabel?: string; - customLabel?: JSX.Element; -} - export interface SettingToggleProps extends IToggleProps, SettingProps { onToggleInputChange?: ToggleChangeHandler; } diff --git a/libs/designer-ui/src/lib/staticResult/util.ts b/libs/designer-ui/src/lib/staticResult/util.ts index 9d922ed211d..c9fde861352 100644 --- a/libs/designer-ui/src/lib/staticResult/util.ts +++ b/libs/designer-ui/src/lib/staticResult/util.ts @@ -2,10 +2,10 @@ import type { StaticResultRootSchemaType } from '.'; import constants from '../constants'; import type { DropdownItem } from '../dropdown'; import type { ValueSegment } from '../editor'; -import { ValueSegmentType } from '../editor'; +import { createLiteralValueSegment } from '../editor/base/utils/helper'; import { SchemaPropertyValueType } from './propertyEditor/PropertyEditorItem'; import type { OpenAPIV2 } from '@microsoft/logic-apps-shared'; -import { capitalizeFirstLetter, guid } from '@microsoft/logic-apps-shared'; +import { capitalizeFirstLetter } from '@microsoft/logic-apps-shared'; export const parseStaticResultSchema = (staticResultSchema: OpenAPIV2.SchemaObject) => { const { additionalProperties, properties, required, type } = staticResultSchema; @@ -131,7 +131,7 @@ export const initializeShownProperties = ( export const formatShownProperties = (propertiesSchema: Record): ValueSegment[] => { if (!propertiesSchema) return []; const filteredProperties: Record = Object.fromEntries(Object.entries(propertiesSchema).filter(([, value]) => value)); - return [{ id: guid(), type: ValueSegmentType.LITERAL, value: Object.keys(filteredProperties).toString() }]; + return [createLiteralValueSegment(Object.keys(filteredProperties).toString())]; }; export const getOptions = (propertiesSchema: StaticResultRootSchemaType, required: string[]): DropdownItem[] => { diff --git a/libs/designer-ui/src/lib/table/index.tsx b/libs/designer-ui/src/lib/table/index.tsx index 21dc8066352..a471e738ed2 100644 --- a/libs/designer-ui/src/lib/table/index.tsx +++ b/libs/designer-ui/src/lib/table/index.tsx @@ -1,10 +1,10 @@ import type { DictionaryEditorItemProps, DictionaryEditorProps } from '../dictionary'; import { DictionaryEditor, DictionaryType } from '../dictionary'; -import { ValueSegmentType } from '../editor'; import type { ChangeState } from '../editor/base'; +import { createEmptyLiteralValueSegment } from '../editor/base/utils/helper'; import type { IDropdownOption, IDropdownStyles } from '@fluentui/react'; import { Dropdown } from '@fluentui/react'; -import { getIntl, guid } from '@microsoft/logic-apps-shared'; +import { getIntl } from '@microsoft/logic-apps-shared'; import type { FormEvent } from 'react'; import { useState } from 'react'; @@ -76,13 +76,12 @@ export const TableEditor: React.FC = ({ }), }, ]; - const emptyValue = [{ id: guid(), type: ValueSegmentType.LITERAL, value: '' }]; const [selectedKey, setSelectedKey] = useState(columnMode); const [items] = useState(initialItems ?? []); const onOptionChange = (_event: FormEvent, option?: IDropdownOption): void => { if (option) { setSelectedKey(option.key as ColumnMode); - onChange?.({ value: emptyValue, viewModel: { items, columnMode: option.key } }); + onChange?.({ value: [createEmptyLiteralValueSegment()], viewModel: { items, columnMode: option.key } }); } }; diff --git a/libs/designer-ui/src/lib/tokenpicker/tokenpicker.less b/libs/designer-ui/src/lib/tokenpicker/tokenpicker.less index ad605989950..65021e44294 100644 --- a/libs/designer-ui/src/lib/tokenpicker/tokenpicker.less +++ b/libs/designer-ui/src/lib/tokenpicker/tokenpicker.less @@ -233,6 +233,7 @@ .msla-theme-dark { .msla-token-picker-header { border-bottom: 1px solid @ms-color-secondaryBackground; + background-color: @ms-color-primaryBackground; } .msla-token-picker-footer { border-top: 1px solid @ms-color-secondaryBackground; @@ -272,4 +273,18 @@ .ms-TextField-field { background: none; } + .msla-token-picker-expression-subheader { + background: @ms-color-primaryBackground; + border-bottom: 1px solid @ms-color-secondaryBackground; + .msla-panel-menu { + button[role='tab'] { + .ms-Pivot-text { + color: #fff; + &:hover { + color: #0078d4; + } + } + } + } + } } diff --git a/libs/designer-ui/src/lib/tokenpicker/tokenpickersection/tokenpickeroption.tsx b/libs/designer-ui/src/lib/tokenpicker/tokenpickersection/tokenpickeroption.tsx index 98b565a14ff..fd053ab6f11 100644 --- a/libs/designer-ui/src/lib/tokenpicker/tokenpickersection/tokenpickeroption.tsx +++ b/libs/designer-ui/src/lib/tokenpicker/tokenpickersection/tokenpickeroption.tsx @@ -3,14 +3,13 @@ import { TokenPickerMode } from '../'; import type { ValueSegment } from '../../editor'; import { INSERT_TOKEN_NODE } from '../../editor/base/plugins/InsertTokenNode'; import { SINGLE_VALUE_SEGMENT } from '../../editor/base/plugins/SingleValueSegment'; -import { convertUIElementNameToAutomationId } from '../../utils'; import type { Token, TokenGroup } from '../models/token'; import { getReducedTokenList, hasAdvanced } from './tokenpickerhelpers'; import type { TokenPickerBaseProps } from './tokenpickersection'; -import { Icon } from '@fluentui/react'; +import { Icon, useTheme } from '@fluentui/react'; import { useBoolean } from '@fluentui/react-hooks'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { hex2rgb, lighten } from '@microsoft/logic-apps-shared'; +import { darken, hex2rgb, lighten, replaceWhiteSpaceWithUnderscore } from '@microsoft/logic-apps-shared'; import Fuse from 'fuse.js'; import type { LexicalEditor } from 'lexical'; import type { Dispatch, SetStateAction } from 'react'; @@ -40,6 +39,8 @@ export const TokenPickerOptions = ({ tokenClickedCallback, }: TokenPickerOptionsProps): JSX.Element => { const intl = useIntl(); + const { isInverted } = useTheme(); + let editor: LexicalEditor | null; try { [editor] = useLexicalComposerContext(); @@ -167,7 +168,9 @@ export const TokenPickerOptions = ({ const sectionBrandColorRgb = hex2rgb(getSectionBrandColor()); const sectionHeaderColorRgb = lighten(sectionBrandColorRgb, 0.9); + const sectionHeaderColorRgbDark = darken(sectionBrandColorRgb, 0.5); const sectionHeaderColorCss = `rgb(${sectionHeaderColorRgb.red}, ${sectionHeaderColorRgb.green}, ${sectionHeaderColorRgb.blue})`; + const sectionHeaderColorCssDark = `rgb(${sectionHeaderColorRgbDark.red}, ${sectionHeaderColorRgbDark.green}, ${sectionHeaderColorRgbDark.blue})`; const maxRowsShown = selectedKey === TokenPickerMode.EXPRESSION ? section.tokens.length : maxTokensPerSection; const showSeeMoreOrLessButton = !searchQuery && (hasAdvanced(section.tokens) || section.tokens.length > maxRowsShown); @@ -176,7 +179,10 @@ export const TokenPickerOptions = ({ <> {(searchQuery && filteredTokens.length > 0) || !searchQuery ? ( <> -
+
token icon {getSectionSecurity() ? (
@@ -188,7 +194,7 @@ export const TokenPickerOptions = ({ diff --git a/libs/designer-ui/src/lib/utils/utils.ts b/libs/designer-ui/src/lib/utils/utils.ts index 67ca3dfcbaa..539d1ac599e 100644 --- a/libs/designer-ui/src/lib/utils/utils.ts +++ b/libs/designer-ui/src/lib/utils/utils.ts @@ -399,10 +399,6 @@ export const getConnectorCategoryString = (connector: Connector | OperationApi | return isBuiltInConnector(connector) ? builtInText : isCustomConnector(connector) ? customText : azureText; }; -export const convertUIElementNameToAutomationId = (uiElementName: string): string => { - return uiElementName?.replace(/\W/g, '_')?.toLowerCase(); -}; - export const getPreviewTag = (status: string | undefined): string | undefined => { const intl = getIntl(); return equals(status, 'preview') diff --git a/libs/designer/src/lib/common/constants.ts b/libs/designer/src/lib/common/constants.ts index 999f2cf7778..b69c02997ac 100644 --- a/libs/designer/src/lib/common/constants.ts +++ b/libs/designer/src/lib/common/constants.ts @@ -188,6 +188,16 @@ export default { HTML: 'html', RECURRENCE: 'recurrence', }, + EDITOR_OPTIONS: { + LANGUAGE: { + CSHARP: 'csharp', + JAVASCRIPT: 'javascript', + JSON: 'json', + POWERSHELL: 'powershell', + }, + }, + DEFAULT_CUSTOM_CODE_INPUT: 'CodeFile', + INLINECODE: 'connectionProviders/inlineCode', EVENT_AUTH_COMPLETED: 'MSLA_AUTH_COMPLETED', ERROR_MESSAGES: { FAILED_TO_FETCH: 'Failed to fetch', diff --git a/libs/designer/src/lib/common/models/customcode.ts b/libs/designer/src/lib/common/models/customcode.ts new file mode 100644 index 00000000000..24132f72d82 --- /dev/null +++ b/libs/designer/src/lib/common/models/customcode.ts @@ -0,0 +1,12 @@ +export interface CustomCode { + nodeId: string; + fileExtension: string; + isModified?: boolean; + isDeleted?: boolean; +} + +export interface CustomCodeWithData extends CustomCode { + fileData: string; +} + +export type CustomCodeFileNameMapping = Record; diff --git a/libs/designer/src/lib/core/BJSWorkflowProvider.tsx b/libs/designer/src/lib/core/BJSWorkflowProvider.tsx index 407b5e17a4f..b90e4c132d1 100644 --- a/libs/designer/src/lib/core/BJSWorkflowProvider.tsx +++ b/libs/designer/src/lib/core/BJSWorkflowProvider.tsx @@ -1,6 +1,7 @@ import type { Workflow } from '../common/models/workflow'; import { ProviderWrappedContext } from './ProviderWrappedContext'; import { initializeGraphState } from './parsers/ParseReduxAction'; +import { initCustomCode } from './state/customcode/customcodeSlice'; import { useAreDesignerOptionsInitialized, useAreServicesInitialized } from './state/designerOptions/designerOptionsSelectors'; import { initializeServices } from './state/designerOptions/designerOptionsSlice'; import { initWorkflowKind, initRunInstance, initWorkflowSpec } from './state/workflow/workflowSlice'; @@ -14,19 +15,21 @@ import { useDispatch } from 'react-redux'; export interface BJSWorkflowProviderProps { workflow: Workflow; + customCode?: Record; runInstance?: LogicAppsV2.RunInstanceDefinition | null; children?: React.ReactNode; appSettings?: Record; } -const DataProviderInner: React.FC = ({ workflow, children, runInstance, appSettings }) => { +const DataProviderInner: React.FC = ({ workflow, children, runInstance, customCode, appSettings }) => { const dispatch = useDispatch(); useDeepCompareEffect(() => { dispatch(initWorkflowSpec('BJS')); dispatch(initWorkflowKind(parseWorkflowKind(workflow?.kind))); dispatch(initRunInstance(runInstance ?? null)); + dispatch(initCustomCode(customCode)); dispatch(initializeGraphState({ workflowDefinition: workflow, runInstance })); - }, [runInstance, workflow]); + }, [runInstance, workflow, customCode]); // Store app settings in query to access outside of functional components useQuery({ queryKey: ['appSettings'], initialData: appSettings }); diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/delete.ts b/libs/designer/src/lib/core/actions/bjsworkflow/delete.ts index 4f266ee331b..dbf08fca803 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/delete.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/delete.ts @@ -1,14 +1,17 @@ import type { RootState } from '../../..'; +import constants from '../../../common/constants'; import type { WorkflowNode } from '../../parsers/models/workflowNode'; import { removeNodeConnectionData } from '../../state/connection/connectionSlice'; +import { deleteCustomCode } from '../../state/customcode/customcodeSlice'; import { deinitializeNodes, deinitializeOperationInfo } from '../../state/operation/operationMetadataSlice'; import { clearPanel } from '../../state/panel/panelSlice'; import { setValidationError } from '../../state/setting/settingSlice'; import { deinitializeStaticResultProperty } from '../../state/staticresultschema/staticresultsSlice'; import { deinitializeTokensAndVariables } from '../../state/tokens/tokensSlice'; import { clearFocusNode, deleteNode } from '../../state/workflow/workflowSlice'; +import { getParameterFromName } from '../../utils/parameters/helper'; import { updateAllUpstreamNodes } from './initialize'; -import { WORKFLOW_NODE_TYPES } from '@microsoft/logic-apps-shared'; +import { WORKFLOW_NODE_TYPES, getRecordEntry, CustomCodeService } from '@microsoft/logic-apps-shared'; import type { Dispatch } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { batch } from 'react-redux'; @@ -33,6 +36,7 @@ export const deleteOperation = createAsyncThunk( dispatch(clearPanel()); dispatch(deleteNode(deletePayload)); + deleteCustomCodeInfo(nodeId, dispatch, getState() as RootState); deleteOperationDetails(nodeId, dispatch); updateAllUpstreamNodes(getState() as RootState, dispatch); }); @@ -48,6 +52,18 @@ const deleteOperationDetails = async (nodeId: string, dispatch: Dispatch): Promi dispatch(deinitializeStaticResultProperty({ id: nodeId + 0 })); }; +const deleteCustomCodeInfo = (nodeId: string, dispatch: Dispatch, state: RootState): void => { + const nodeInputs = getRecordEntry(state.operations.inputParameters, nodeId); + if (nodeInputs) { + const parameter = getParameterFromName(nodeInputs, constants.DEFAULT_CUSTOM_CODE_INPUT); + if (CustomCodeService().isCustomCode(parameter?.editor, parameter?.editorOptions?.language)) { + const fileName = parameter?.editorViewModel?.customCodeData?.fileName; + // if the file name is not present, then it is a new custom code and we just need to remove the file data + dispatch(deleteCustomCode({ nodeId, fileName })); + } + } +}; + export const deleteGraphNode = createAsyncThunk('deleteGraph', async (deletePayload: DeleteGraphPayload, { dispatch }) => { const { graphNode } = deletePayload; diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/initialize.ts b/libs/designer/src/lib/core/actions/bjsworkflow/initialize.ts index 34f13888a1e..ec046c7cc8b 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/initialize.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/initialize.ts @@ -1,5 +1,7 @@ +import type { CustomCodeFileNameMapping } from '../../..'; import Constants from '../../../common/constants'; -import { ImpersonationSource, type ConnectionReferences, type WorkflowParameter } from '../../../common/models/workflow'; +import type { ConnectionReferences, WorkflowParameter } from '../../../common/models/workflow'; +import { ImpersonationSource } from '../../../common/models/workflow'; import type { WorkflowNode } from '../../parsers/models/workflowNode'; import { getConnectorWithSwagger, getSwaggerFromEndpoint } from '../../queries/connections'; import { getOperationManifest } from '../../queries/operation'; @@ -16,6 +18,7 @@ import { getSplitOnOptions, getUpdatedManifestForSchemaDependency, getUpdatedMan import { addRecurrenceParametersInGroup, getAllInputParameters, + getCustomCodeFileName, getDependentParameters, getInputsValueFromDefinitionForManifest, getParameterFromName, @@ -60,6 +63,7 @@ import { getBrandColorFromConnector, getIconUriFromConnector, getObjectPropertyValue, + getRecordEntry, isDynamicListExtension, isDynamicPropertiesExtension, isDynamicSchemaExtension, @@ -70,6 +74,7 @@ import { PropertyName, unmap, UnsupportedException, + isNullOrEmpty, } from '@microsoft/logic-apps-shared'; import type { OutputToken, ParameterInfo } from '@microsoft/designer-ui'; import type { Dispatch } from '@reduxjs/toolkit'; @@ -447,6 +452,36 @@ export const updateCallbackUrlInInputs = async ( return; }; +export const updateCustomCodeInInputs = async ( + nodeId: string, + fileExtension: string, + nodeInputs: NodeInputs, + customCode: CustomCodeFileNameMapping +) => { + if (isNullOrEmpty(customCode)) return; + // getCustomCodeFileName does not return the file extension because the editor view model is not populated yet + const fileName = getCustomCodeFileName(nodeId, nodeInputs) + fileExtension; + try { + const customCodeValue = getRecordEntry(customCode, fileName)?.fileData; + const parameter = getParameterFromName(nodeInputs, Constants.DEFAULT_CUSTOM_CODE_INPUT); + + if (parameter && customCodeValue) { + parameter.editorViewModel = { + customCodeData: { fileData: customCodeValue, fileExtension, fileName }, + }; + } + } catch (error) { + const errorMessage = `Failed to populate code file ${fileName}: ${error}`; + LoggerService().log({ + level: LogEntryLevel.Error, + area: 'fetchCustomCode', + message: errorMessage, + error: error instanceof Error ? error : undefined, + }); + return; + } +}; + export const updateAllUpstreamNodes = (state: RootState, dispatch: Dispatch): void => { const allOperations = state.workflow.operations; const payload: UpdateUpstreamNodesPayload = {}; diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/operationdeserializer.ts b/libs/designer/src/lib/core/actions/bjsworkflow/operationdeserializer.ts index b3d67045f38..8af8f366c11 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/operationdeserializer.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/operationdeserializer.ts @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import type { CustomCodeFileNameMapping } from '../../..'; import Constants from '../../../common/constants'; import type { ConnectionReference, ConnectionReferences, WorkflowParameter } from '../../../common/models/workflow'; import type { DeserializedWorkflow } from '../../parsers/BJSWorkflow/BJSDeserializer'; @@ -46,6 +47,7 @@ import { getInputParametersFromManifest, getOutputParametersFromManifest, updateCallbackUrlInInputs, + updateCustomCodeInInputs, updateInvokerSettings, } from './initialize'; import { getOperationSettings, getSplitOnValue } from './settings'; @@ -64,6 +66,7 @@ import { aggregate, equals, getRecordEntry, + getFileExtensionNameFromOperationId, parseErrorMessage, } from '@microsoft/logic-apps-shared'; import type { InputParameter, OutputParameter, LogicAppsV2, OperationManifest } from '@microsoft/logic-apps-shared'; @@ -95,6 +98,7 @@ export const initializeOperationMetadata = async ( deserializedWorkflow: DeserializedWorkflow, references: ConnectionReferences, workflowParameters: Record, + customCode: CustomCodeFileNameMapping, workflowKind: WorkflowKind, forceEnableSplitOn: boolean, dispatch: Dispatch @@ -115,7 +119,9 @@ export const initializeOperationMetadata = async ( triggerNodeId = operationId; } if (operationManifestService.isSupported(operation.type, operation.kind)) { - promises.push(initializeOperationDetailsForManifest(operationId, operation, !!isTrigger, workflowKind, forceEnableSplitOn, dispatch)); + promises.push( + initializeOperationDetailsForManifest(operationId, operation, customCode, !!isTrigger, workflowKind, forceEnableSplitOn, dispatch) + ); } else { promises.push( initializeOperationDetailsForSwagger(operationId, operation, references, !!isTrigger, workflowKind, forceEnableSplitOn, dispatch) @@ -198,6 +204,7 @@ const initializeConnectorsForReferences = async (references: ConnectionReference export const initializeOperationDetailsForManifest = async ( nodeId: string, _operation: LogicAppsV2.ActionDefinition | LogicAppsV2.TriggerDefinition, + customCode: CustomCodeFileNameMapping, isTrigger: boolean, workflowKind: WorkflowKind, forceEnableSplitOn: boolean, @@ -237,6 +244,11 @@ export const initializeOperationDetailsForManifest = async ( await updateCallbackUrlInInputs(nodeId, nodeOperationInfo, nodeInputs); } + // Populate Customcode with values gotten from file system + if (equals(operationInfo.connectorId, Constants.INLINECODE) && !equals(operationInfo.operationId, 'javascriptcode')) { + updateCustomCodeInInputs(nodeId, getFileExtensionNameFromOperationId(operationInfo.operationId), nodeInputs, customCode); + } + const { outputs: nodeOutputs, dependencies: outputDependencies } = getOutputParametersFromManifest( manifest, isTrigger, diff --git a/libs/designer/src/lib/core/index.ts b/libs/designer/src/lib/core/index.ts index f9410c439a3..9d72e9971a5 100644 --- a/libs/designer/src/lib/core/index.ts +++ b/libs/designer/src/lib/core/index.ts @@ -4,27 +4,69 @@ export * from './ProviderWrappedContext'; export { getReactQueryClient } from './ReactQueryProvider'; export type { RootState, AppDispatch } from './store'; export { store } from './store'; -export { useConnectionMapping, useConnectionRefs, useIsOperationMissingConnection } from './state/connection/connectionSelector'; +export { + useConnectionMapping, + useConnectionRefs, + useIsOperationMissingConnection, +} from './state/connection/connectionSelector'; export type { NodeInputs } from './state/operation/operationMetadataSlice'; -export { useOperationsInputParameters, useNodesInitialized, useNodesAndDynamicDataInitialized } from './state/operation/operationSelector'; +export { + useOperationsInputParameters, + useNodesInitialized, + useNodesAndDynamicDataInitialized, +} from './state/operation/operationSelector'; export type { ErrorMessage } from './state/workflow/workflowInterfaces'; -export { discardAllChanges, setFocusNode, setIsWorkflowDirty, setHostErrorMessages } from './state/workflow/workflowSlice'; -export { useIsWorkflowDirty, useNodeDisplayName, useNodeMetadata } from './state/workflow/workflowSelectors'; -export { useIsWorkflowParametersDirty, useWorkflowParameterValidationErrors } from './state/workflowparameters/workflowparametersselector'; +export { + discardAllChanges, + setFocusNode, + setIsWorkflowDirty, + setHostErrorMessages, +} from './state/workflow/workflowSlice'; +export { + useIsWorkflowDirty, + useNodeDisplayName, + useNodeMetadata, +} from './state/workflow/workflowSelectors'; +export { + useIsWorkflowParametersDirty, + useWorkflowParameterValidationErrors, +} from './state/workflowparameters/workflowparametersselector'; export { useIsDesignerDirty, resetDesignerDirtyState } from './state/global'; export { useAllSettingsValidationErrors } from './state/setting/settingSelector'; export { useAllConnectionErrors } from './state/operation/operationSelector'; export { serializeWorkflow } from './actions/bjsworkflow/serializer'; -export { setSelectedNodeId, changePanelNode, clearPanel, openPanel, collapsePanel } from './state/panel/panelSlice'; +export { + setSelectedNodeId, + changePanelNode, + clearPanel, + openPanel, + collapsePanel, +} from './state/panel/panelSlice'; export { useOperationInfo } from './state/selectors/actionMetadataSelector'; export { useReplacedIds } from './state/workflow/workflowSelectors'; -export { useSelectedNodeId, useSelectedNodeIds } from './state/panel/panelSelectors'; +export { + useSelectedNodeId, + useSelectedNodeIds, +} from './state/panel/panelSelectors'; export { initializeServices } from './state/designerOptions/designerOptionsSlice'; export { resetWorkflowState, resetNodesLoadStatus } from './state/global'; -export { validateParameter } from './utils/parameters/helper'; -export { createLiteralValueSegment, createTokenValueSegment } from './utils/parameters/segment'; -export { getOutputTokenSections, getExpressionTokenSections } from './utils/tokens'; +export { + validateParameter, + getCustomCodeFilesWithData, +} from './utils/parameters/helper'; +export { + createLiteralValueSegment, + createTokenValueSegment, +} from './utils/parameters/segment'; +export { + getOutputTokenSections, + getExpressionTokenSections, +} from './utils/tokens'; export { getTriggerNodeId } from './utils/graph'; export { updateParameterValidation } from './state/operation/operationMetadataSlice'; export { updateWorkflowParameters } from './actions/bjsworkflow/initialize'; -export { getBrandColorFromManifest, getIconUriFromManifest } from './utils/card'; +export { + getBrandColorFromManifest, + getIconUriFromManifest, +} from './utils/card'; +export { resetCustomCode } from './state/customcode/customcodeSlice'; diff --git a/libs/designer/src/lib/core/parsers/ParseReduxAction.ts b/libs/designer/src/lib/core/parsers/ParseReduxAction.ts index 6101e5d7276..33606f4aab3 100644 --- a/libs/designer/src/lib/core/parsers/ParseReduxAction.ts +++ b/libs/designer/src/lib/core/parsers/ParseReduxAction.ts @@ -6,6 +6,7 @@ import { getConnectionsQuery } from '../queries/connections'; import { initializeConnectionReferences } from '../state/connection/connectionSlice'; import { initializeStaticResultProperties } from '../state/staticresultschema/staticresultsSlice'; import type { RootState } from '../store'; +import { getCustomCodeFilesWithData } from '../utils/parameters/helper'; import type { DeserializedWorkflow } from './BJSWorkflow/BJSDeserializer'; import { Deserialize as BJSDeserialize } from './BJSWorkflow/BJSDeserializer'; import type { WorkflowNode } from './models/workflowNode'; @@ -49,14 +50,18 @@ export const initializeGraphState = createAsyncThunk< thunkAPI.dispatch(initializeStaticResultProperties(deserializedWorkflow.staticResults ?? {})); updateWorkflowParameters(parameters ?? {}, thunkAPI.dispatch); + const { connections, customCode } = thunkAPI.getState(); + const customCodeWithData = getCustomCodeFilesWithData(customCode); + const asyncInitialize = async () => { batch(async () => { try { await Promise.all([ initializeOperationMetadata( deserializedWorkflow, - thunkAPI.getState().connections.connectionReferences, + connections.connectionReferences, parameters ?? {}, + customCodeWithData, workflow.workflowKind, designerOptions.hostOptions.forceEnableSplitOn ?? false, thunkAPI.dispatch diff --git a/libs/designer/src/lib/core/state/customcode/customcodeInterfaces.ts b/libs/designer/src/lib/core/state/customcode/customcodeInterfaces.ts new file mode 100644 index 00000000000..8e3e5f58ca0 --- /dev/null +++ b/libs/designer/src/lib/core/state/customcode/customcodeInterfaces.ts @@ -0,0 +1,26 @@ +import type { CustomCode } from '../../../common/models/customcode'; + +export interface CustomCodeState { + // by fileName + files: Record; + // by nodeId + fileData: Record; +} + +export interface AddCustomCodePayload { + nodeId: string; + fileData: string; + fileExtension: string; + fileName: string; +} + +export interface DeleteCustomCodePayload { + nodeId: string; + fileName: string; +} + +export interface RenameCustomCodePayload { + nodeId: string; + oldFileName: string; + newFileName: string; +} diff --git a/libs/designer/src/lib/core/state/customcode/customcodeSlice.ts b/libs/designer/src/lib/core/state/customcode/customcodeSlice.ts new file mode 100644 index 00000000000..ed6c1940566 --- /dev/null +++ b/libs/designer/src/lib/core/state/customcode/customcodeSlice.ts @@ -0,0 +1,86 @@ +import { resetWorkflowState } from '../global'; +import type { AddCustomCodePayload, CustomCodeState, DeleteCustomCodePayload, RenameCustomCodePayload } from './customcodeInterfaces'; +import { splitFileName } from '@microsoft/logic-apps-shared'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +export const initialState: CustomCodeState = { + files: {}, + fileData: {}, +}; + +export const customCodeSlice = createSlice({ + name: 'customCode', + initialState, + reducers: { + initCustomCode: (state, action: PayloadAction | undefined>) => { + const customCodeData = action.payload; + if (!customCodeData) return; + Object.entries(customCodeData).forEach(([fileName, fileData]) => { + const [nodeId, fileExtension] = splitFileName(fileName); + state.files[fileName] = { + nodeId, + fileExtension, + isModified: false, + isDeleted: false, + }; + state.fileData[nodeId] = fileData; + }); + }, + addOrUpdateCustomCode: (state, action: PayloadAction) => { + const { nodeId, fileData, fileExtension, fileName } = action.payload; + // only update if the fileData is different + if (state.fileData[nodeId] === fileData) { + return; + } + state.files[fileName] = { nodeId, fileExtension, isModified: true }; + // cycle through the old files, and mark as deleted to all that share the same nodeId + Object.entries(state.files).forEach(([existingFileName, file]) => { + if (file.nodeId === nodeId && existingFileName !== fileName) { + state.files[existingFileName] = { ...file, isDeleted: true }; + } + }); + state.fileData[nodeId] = fileData; + }, + deleteCustomCode: (state, action: PayloadAction) => { + const { nodeId, fileName } = action.payload; + if (fileName) { + state.files[fileName] = { nodeId, fileExtension: '', isDeleted: true }; + } + delete state.fileData[nodeId]; + }, + renameCustomCode: (state, action: PayloadAction) => { + const { nodeId, oldFileName, newFileName } = action.payload; + if (state.files[oldFileName]) { + state.files[newFileName] = { + ...state.files[oldFileName], + isModified: true, + isDeleted: false, + }; + } + // cycle through the existing files, and mark as deleted to all that share the same nodeId + Object.entries(state.files).forEach(([fileName, file]) => { + if (file.nodeId === nodeId && fileName !== newFileName) { + state.files[fileName] = { ...file, isDeleted: true }; + } + }); + }, + // on save we want to remove all deleted files and reset the modified flag + resetCustomCode: (state) => { + Object.entries(state.files).forEach(([fileName, file]) => { + if (file.isDeleted) { + delete state.files[fileName]; + } else { + state.files[fileName] = { ...file, isModified: false }; + } + }); + }, + }, + extraReducers: (builder) => { + builder.addCase(resetWorkflowState, () => initialState); + }, +}); + +export const { initCustomCode, addOrUpdateCustomCode, deleteCustomCode, renameCustomCode, resetCustomCode } = customCodeSlice.actions; + +export default customCodeSlice.reducer; diff --git a/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts b/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts index 814c816336f..c00fd4cc1d5 100644 --- a/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts +++ b/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts @@ -16,6 +16,7 @@ import type { IEditorService, IConnectionParameterEditorService, IChatbotService, + ICustomCodeService, LogicApps, } from '@microsoft/logic-apps-shared'; @@ -59,4 +60,5 @@ export interface ServiceOptions { editorService?: IEditorService; connectionParameterEditorService?: IConnectionParameterEditorService; chatbotService?: IChatbotService; + customCodeService?: ICustomCodeService; } diff --git a/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts b/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts index e2432b6838a..abc9319d78e 100644 --- a/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts +++ b/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts @@ -18,6 +18,7 @@ import { InitEditorService, InitConnectionParameterEditorService, InitChatbotService, + InitCustomCodeService, } from '@microsoft/logic-apps-shared'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -58,6 +59,7 @@ export const initializeServices = createAsyncThunk( editorService, connectionParameterEditorService, chatbotService, + customCodeService, }: ServiceOptions) => { const loggerServices: ILoggerService[] = []; if (loggerService) { @@ -79,6 +81,7 @@ export const initializeServices = createAsyncThunk( if (functionService) InitFunctionService(functionService); if (appServiceService) InitAppServiceService(appServiceService); if (chatbotService) InitChatbotService(chatbotService); + if (customCodeService) InitCustomCodeService(customCodeService); if (hostService) { InitHostService(hostService); diff --git a/libs/designer/src/lib/core/state/operation/operationMetadataSlice.ts b/libs/designer/src/lib/core/state/operation/operationMetadataSlice.ts index c37390c0f90..d88bd2a541f 100644 --- a/libs/designer/src/lib/core/state/operation/operationMetadataSlice.ts +++ b/libs/designer/src/lib/core/state/operation/operationMetadataSlice.ts @@ -6,9 +6,16 @@ import type { RepetitionContext } from '../../utils/parameters/helper'; import { createTokenValueSegment, isTokenValueSegment } from '../../utils/parameters/segment'; import { normalizeKey } from '../../utils/tokens'; import { resetNodesLoadStatus, resetWorkflowState } from '../global'; -import { LogEntryLevel, LoggerService, getRecordEntry, type OpenAPIV2, type OperationInfo } from '@microsoft/logic-apps-shared'; +import { LogEntryLevel, LoggerService, getRecordEntry } from '@microsoft/logic-apps-shared'; import type { ParameterInfo } from '@microsoft/designer-ui'; -import type { FilePickerInfo, InputParameter, OutputParameter, SwaggerParser } from '@microsoft/logic-apps-shared'; +import type { + FilePickerInfo, + InputParameter, + OutputParameter, + SwaggerParser, + OpenAPIV2, + OperationInfo, +} from '@microsoft/logic-apps-shared'; import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { WritableDraft } from 'immer/dist/internal'; @@ -239,7 +246,9 @@ export const operationMetadataSlice = createSlice({ }, addDynamicInputs: (state, action: PayloadAction) => { const { nodeId, groupId, inputs, newInputs: rawInputs, swagger } = action.payload; - const inputParameters = getRecordEntry(state.inputParameters, nodeId) ?? { parameterGroups: {} }; + const inputParameters = getRecordEntry(state.inputParameters, nodeId) ?? { + parameterGroups: {}, + }; const parameterGroups = getRecordEntry(inputParameters?.parameterGroups, groupId); if (parameterGroups) { const { parameters } = parameterGroups; @@ -257,7 +266,10 @@ export const operationMetadataSlice = createSlice({ const dependencies = getInputDependencies(inputParameters, rawInputs, swagger); if (dependencies) { - state.dependencies[nodeId].inputs = { ...state.dependencies[nodeId].inputs, ...dependencies }; + state.dependencies[nodeId].inputs = { + ...state.dependencies[nodeId].inputs, + ...dependencies, + }; } }, addDynamicOutputs: (state, action: PayloadAction) => { @@ -337,7 +349,11 @@ export const operationMetadataSlice = createSlice({ updateStaticResults: (state, action: PayloadAction) => { const { id, staticResults } = action.payload; const nodeStaticResults = getRecordEntry(state.staticResults, id); - if (!nodeStaticResults) state.staticResults[id] = { name: '', staticResultOptions: StaticResultOption.DISABLED }; + if (!nodeStaticResults) + state.staticResults[id] = { + name: '', + staticResultOptions: StaticResultOption.DISABLED, + }; state.staticResults[id] = { ...nodeStaticResults, ...staticResults }; LoggerService().log({ @@ -357,7 +373,10 @@ export const operationMetadataSlice = createSlice({ const parameterGroup = nodeInputs.parameterGroups[groupId]; const index = parameterGroup.parameters.findIndex((parameter) => parameter.id === parameterId); if (index > -1) { - parameterGroup.parameters[index] = { ...parameterGroup.parameters[index], ...propertiesToUpdate }; + parameterGroup.parameters[index] = { + ...parameterGroup.parameters[index], + ...propertiesToUpdate, + }; nodeInputs.parameterGroups[groupId] = parameterGroup; } } @@ -365,10 +384,16 @@ export const operationMetadataSlice = createSlice({ const nodeDependencies = getRecordEntry(state.dependencies, nodeId); if (nodeDependencies && dependencies?.inputs) { - nodeDependencies.inputs = { ...nodeDependencies.inputs, ...dependencies.inputs }; + nodeDependencies.inputs = { + ...nodeDependencies.inputs, + ...dependencies.inputs, + }; } if (nodeDependencies && dependencies?.outputs) { - nodeDependencies.outputs = { ...nodeDependencies.outputs, ...dependencies.outputs }; + nodeDependencies.outputs = { + ...nodeDependencies.outputs, + ...dependencies.outputs, + }; } LoggerService().log({ @@ -380,7 +405,12 @@ export const operationMetadataSlice = createSlice({ }, updateParameterConditionalVisibility: ( state, - action: PayloadAction<{ nodeId: string; groupId: string; parameterId: string; value?: boolean }> + action: PayloadAction<{ + nodeId: string; + groupId: string; + parameterId: string; + value?: boolean; + }> ) => { const { nodeId, groupId, parameterId, value } = action.payload; const inputParameters = getRecordEntry(state.inputParameters, nodeId); @@ -402,9 +432,32 @@ export const operationMetadataSlice = createSlice({ args: [action.payload], }); }, + updateParameterEditorViewModel: ( + state, + action: PayloadAction<{ + nodeId: string; + groupId: string; + parameterId: string; + editorViewModel: any; + }> + ) => { + const { nodeId, groupId, parameterId, editorViewModel } = action.payload; + const inputParameters = getRecordEntry(state.inputParameters, nodeId); + const parameterGroup = getRecordEntry(inputParameters?.parameterGroups, groupId); + if (!inputParameters || !parameterGroup) return; + const index = parameterGroup.parameters.findIndex((parameter) => parameter.id === parameterId); + if (index > -1) { + parameterGroup.parameters[index].editorViewModel = editorViewModel; + } + }, updateParameterValidation: ( state, - action: PayloadAction<{ nodeId: string; groupId: string; parameterId: string; validationErrors: string[] | undefined }> + action: PayloadAction<{ + nodeId: string; + groupId: string; + parameterId: string; + validationErrors: string[] | undefined; + }> ) => { const { nodeId, groupId, parameterId, validationErrors } = action.payload; const inputParameters = getRecordEntry(state.inputParameters, nodeId); @@ -417,7 +470,12 @@ export const operationMetadataSlice = createSlice({ }, removeParameterValidationError: ( state, - action: PayloadAction<{ nodeId: string; groupId: string; parameterId: string; validationError: string }> + action: PayloadAction<{ + nodeId: string; + groupId: string; + parameterId: string; + validationError: string; + }> ) => { const { nodeId, groupId, parameterId, validationError } = action.payload; const inputParameters = getRecordEntry(state.inputParameters, nodeId); @@ -445,10 +503,20 @@ export const operationMetadataSlice = createSlice({ const nodeRepetition = getRecordEntry(state.repetitionInfos, id); state.repetitionInfos[id] = { ...nodeRepetition, ...repetition }; }, - updateErrorDetails: (state, action: PayloadAction<{ id: string; errorInfo?: ErrorInfo; clear?: boolean }>) => { + updateErrorDetails: ( + state, + action: PayloadAction<{ + id: string; + errorInfo?: ErrorInfo; + clear?: boolean; + }> + ) => { const { id, errorInfo, clear } = action.payload; if (errorInfo) { - state.errors[id] = { ...(getRecordEntry(state.errors, id) as any), [errorInfo.level]: errorInfo }; + state.errors[id] = { + ...(getRecordEntry(state.errors, id) as any), + [errorInfo.level]: errorInfo, + }; } else if (clear) { delete state.errors[id]; } @@ -496,6 +564,7 @@ export const { updateStaticResults, updateParameterConditionalVisibility, updateParameterValidation, + updateParameterEditorViewModel, updateExistingInputTokenTitles, removeParameterValidationError, updateOutputs, diff --git a/libs/designer/src/lib/core/store.ts b/libs/designer/src/lib/core/store.ts index 05248f2d41d..41ae301cbab 100644 --- a/libs/designer/src/lib/core/store.ts +++ b/libs/designer/src/lib/core/store.ts @@ -1,4 +1,5 @@ import connectionsReducer from './state/connection/connectionSlice'; +import customCodeReducer from './state/customcode/customcodeSlice'; import designerOptionsReducer from './state/designerOptions/designerOptionsSlice'; import designerViewReducer from './state/designerView/designerViewSlice'; import operationMetadataReducer from './state/operation/operationMetadataSlice'; @@ -22,6 +23,7 @@ export const store = configureStore({ tokens: tokens, workflowParameters: workflowParametersReducer, staticResults: staticResultsSchemasReducer, + customCode: customCodeReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/libs/designer/src/lib/core/utils/loops.ts b/libs/designer/src/lib/core/utils/loops.ts index 85a6acb4eaa..49680ccf946 100644 --- a/libs/designer/src/lib/core/utils/loops.ts +++ b/libs/designer/src/lib/core/utils/loops.ts @@ -22,6 +22,7 @@ import { getParameterFromName, parameterValueToString, shouldIncludeSelfForRepetitionReference, + getCustomCodeFilesWithData, } from './parameters/helper'; import { isTokenValueSegment } from './parameters/segment'; import { TokenSegmentConvertor } from './parameters/tokensegment'; @@ -152,9 +153,11 @@ export const addForeachToNode = createAsyncThunk( // Initializing details for newly added foreach operation. const foreachOperation = newState.workflow.operations[foreachNodeId]; + const customCodeWithData = getCustomCodeFilesWithData(state.customCode); const [{ nodeInputs, nodeOutputs, nodeDependencies, settings }] = (await initializeOperationDetailsForManifest( foreachNodeId, foreachOperation, + customCodeWithData, /* isTrigger */ false, state.workflow.workflowKind, state.designerOptions.hostOptions.forceEnableSplitOn ?? false, diff --git a/libs/designer/src/lib/core/utils/parameters/helper.ts b/libs/designer/src/lib/core/utils/parameters/helper.ts index b96bd8cf10c..8e8b243e305 100644 --- a/libs/designer/src/lib/core/utils/parameters/helper.ts +++ b/libs/designer/src/lib/core/utils/parameters/helper.ts @@ -1,10 +1,12 @@ /* eslint-disable no-case-declarations */ +import type { CustomCodeFileNameMapping } from '../../..'; import constants from '../../../common/constants'; import type { ConnectionReference, WorkflowParameter } from '../../../common/models/workflow'; import { getReactQueryClient } from '../../ReactQueryProvider'; import type { NodeDataWithOperationMetadata } from '../../actions/bjsworkflow/operationdeserializer'; import type { Settings } from '../../actions/bjsworkflow/settings'; import { getConnectorWithSwagger } from '../../queries/connections'; +import type { CustomCodeState } from '../../state/customcode/customcodeInterfaces'; import type { DependencyInfo, NodeDependencies, @@ -112,6 +114,7 @@ import { nthLastIndexOf, parseErrorMessage, getRecordEntry, + replaceWhiteSpaceWithUnderscore, isRecordNotEmpty, } from '@microsoft/logic-apps-shared'; import type { @@ -571,7 +574,7 @@ const toSimpleQueryBuilderViewModel = ( let operand1: ValueSegment, operand2: ValueSegment, operationLiteral: ValueSegment; // default value if (!input || input.length === 0) { - return { isOldFormat: true, isRowFormat: true, itemValue: [{ id: guid(), type: ValueSegmentType.LITERAL, value: "@equals('','')" }] }; + return { isOldFormat: true, isRowFormat: true, itemValue: [createLiteralValueSegment("@equals('','')")] }; } if (!input.includes('@') || !input.includes(',')) { @@ -588,11 +591,11 @@ const toSimpleQueryBuilderViewModel = ( stringValue = stringValue.replace('@not(', '@'); const baseOperator = stringValue.substring(stringValue.indexOf('@') + 1, stringValue.indexOf('(')); operator = 'not' + baseOperator; - operationLiteral = { id: guid(), type: ValueSegmentType.LITERAL, value: `@not(${baseOperator}(` }; - endingLiteral = { id: guid(), type: ValueSegmentType.LITERAL, value: `))` }; + operationLiteral = createLiteralValueSegment(`@not(${baseOperator}(`); + endingLiteral = createLiteralValueSegment(`))`); } else { - operationLiteral = { id: guid(), type: ValueSegmentType.LITERAL, value: `@${operator}(` }; - endingLiteral = { id: guid(), type: ValueSegmentType.LITERAL, value: ')' }; + operationLiteral = createLiteralValueSegment(`@${operator}(`); + endingLiteral = createLiteralValueSegment(')'); } // if operator is not of the dropdownlist, it cannot be converted into row format @@ -604,7 +607,7 @@ const toSimpleQueryBuilderViewModel = ( const operand2String = removeQuotes(operandSubstring.substring(getOuterMostCommaIndex(operandSubstring) + 1).trim()); operand1 = loadParameterValueFromString(operand1String, true, true, true)[0]; operand2 = loadParameterValueFromString(operand2String, true, true, true)[0]; - const separatorLiteral: ValueSegment = { id: guid(), type: ValueSegmentType.LITERAL, value: `,` }; + const separatorLiteral: ValueSegment = createLiteralValueSegment(`,`); return { isOldFormat: true, isRowFormat: true, @@ -2422,8 +2425,8 @@ export const recurseSerializeCondition = ( { type: GroupType.ROW, operator: RowDropdownOptions.EQUALS, - operand1: [{ id: guid(), type: ValueSegmentType.LITERAL, value: '' }], - operand2: [{ id: guid(), type: ValueSegmentType.LITERAL, value: '' }], + operand1: [createLiteralValueSegment('')], + operand2: [createLiteralValueSegment('')], }, ]; } @@ -2502,6 +2505,36 @@ export function getGroupAndParameterFromParameterKey( return undefined; } +export const getCustomCodeFileName = (nodeId: string, nodeInputs?: NodeInputs, idReplacements?: Record): string => { + const updatedNodeId = idReplacements?.[nodeId] || nodeId; + let fileName = replaceWhiteSpaceWithUnderscore(updatedNodeId); + + if (nodeInputs) { + const parameter = getParameterFromName(nodeInputs, constants.DEFAULT_CUSTOM_CODE_INPUT); + const fileExtension = parameter?.editorViewModel?.customCodeData?.fileExtension; + if (fileExtension) { + fileName = `${fileName}${fileExtension}`; + } + } + return fileName; +}; + +export const getCustomCodeFilesWithData = (state: CustomCodeState): CustomCodeFileNameMapping => { + const { files, fileData } = state; + const customCodeFileWithData: CustomCodeFileNameMapping = {}; + Object.entries(files).forEach(([fileName, fileInfo]) => { + const { nodeId } = fileInfo; + const fileDataInfo = getRecordEntry(fileData, nodeId); + if (fileDataInfo || fileInfo.isDeleted) { + customCodeFileWithData[fileName] = { + ...fileInfo, + fileData: fileDataInfo ?? '', + }; + } + }); + return customCodeFileWithData; +}; + export function getInputsValueFromDefinitionForManifest( inputsLocation: string[], manifest: OperationManifest, diff --git a/libs/designer/src/lib/index.tsx b/libs/designer/src/lib/index.tsx index ec78ced41eb..ae6876757a6 100644 --- a/libs/designer/src/lib/index.tsx +++ b/libs/designer/src/lib/index.tsx @@ -15,6 +15,7 @@ if (process.env.NODE_ENV === 'development') { export * from './ui/index'; export * from './core/index'; export * from './common/models/workflow'; +export * from './common/models/customcode'; export { default as Constants } from './common/constants'; export { serializeWorkflow as serializeBJSWorkflow } from './core/actions/bjsworkflow/serializer'; export { updateCallbackUrl } from './core/actions/bjsworkflow/initialize'; diff --git a/libs/designer/src/lib/ui/Designer.tsx b/libs/designer/src/lib/ui/Designer.tsx index 6609576f87c..05436910b2b 100644 --- a/libs/designer/src/lib/ui/Designer.tsx +++ b/libs/designer/src/lib/ui/Designer.tsx @@ -253,7 +253,7 @@ export const Designer = (props: DesignerProps) => { hideAttribution: true, }} > - + {backgroundProps ? : null} diff --git a/libs/designer/src/lib/ui/connections/dropzone.tsx b/libs/designer/src/lib/ui/connections/dropzone.tsx index 38a9fbf3d11..2c42d453c7d 100644 --- a/libs/designer/src/lib/ui/connections/dropzone.tsx +++ b/libs/designer/src/lib/ui/connections/dropzone.tsx @@ -18,8 +18,16 @@ import { // import AddBranchIcon from './edgeContextMenuSvgs/addBranchIcon.svg'; // import AddNodeIcon from './edgeContextMenuSvgs/addNodeIcon.svg'; import { css } from '@fluentui/utilities'; -import { LogEntryLevel, LoggerService, containsIdTag, guid, normalizeAutomationId, removeIdTag } from '@microsoft/logic-apps-shared'; -import { ActionButtonV2, convertUIElementNameToAutomationId } from '@microsoft/designer-ui'; +import { ActionButtonV2 } from '@microsoft/designer-ui'; +import { + containsIdTag, + guid, + normalizeAutomationId, + removeIdTag, + replaceWhiteSpaceWithUnderscore, + LogEntryLevel, + LoggerService, +} from '@microsoft/logic-apps-shared'; import { useCallback, useMemo, useState } from 'react'; import { useDrop } from 'react-dnd'; import { useIntl } from 'react-intl'; @@ -106,7 +114,13 @@ export const DropZone: React.FC = ({ graphId, parentId, childId, const addParallelBranch = useCallback(() => { const newId = guid(); const relationshipIds = { graphId, childId: undefined, parentId }; - dispatch(expandDiscoveryPanel({ nodeId: newId, relationshipIds, isParallelBranch: true })); + dispatch( + expandDiscoveryPanel({ + nodeId: newId, + relationshipIds, + isParallelBranch: true, + }) + ); LoggerService().log({ area: 'DropZone:addParallelBranch', level: LogEntryLevel.Verbose, @@ -132,7 +146,11 @@ export const DropZone: React.FC = ({ graphId, parentId, childId, () => ({ accept: 'BOX', drop: () => ({ graphId, parentId, childId }), - canDrop: (item: { id: string; dependencies?: string[]; graphId?: string }) => { + canDrop: (item: { + id: string; + dependencies?: string[]; + graphId?: string; + }) => { // This supports preventing moving a node with a dependency above its upstream node for (const dec of item.dependencies ?? []) { if (!upstreamNodes.has(dec)) { @@ -198,7 +216,7 @@ export const DropZone: React.FC = ({ graphId, parentId, childId, ); const buttonId = normalizeAutomationId( - `msla-edge-button-${convertUIElementNameToAutomationId(parentName)}-${convertUIElementNameToAutomationId(childName) || 'undefined'}` + `msla-edge-button-${replaceWhiteSpaceWithUnderscore(parentName)}-${replaceWhiteSpaceWithUnderscore(childName) || 'undefined'}` ); const showParallelBranchButton = !isLeaf && parentId; @@ -206,8 +224,8 @@ export const DropZone: React.FC = ({ graphId, parentId, childId, const automationId = useCallback( (buttonName: string) => normalizeAutomationId( - `msla-${buttonName}-button-${convertUIElementNameToAutomationId(parentName)}-${ - convertUIElementNameToAutomationId(childName) || 'undefined' + `msla-${buttonName}-button-${replaceWhiteSpaceWithUnderscore(parentName)}-${ + replaceWhiteSpaceWithUnderscore(childName) || 'undefined' }` ), [parentName, childName] @@ -217,7 +235,12 @@ export const DropZone: React.FC = ({ graphId, parentId, childId,
{isOver && (
diff --git a/libs/designer/src/lib/ui/panel/nodeDetailsPanel/nodeDetailsPanel.tsx b/libs/designer/src/lib/ui/panel/nodeDetailsPanel/nodeDetailsPanel.tsx index 92fa23bae30..1bab4a53b58 100644 --- a/libs/designer/src/lib/ui/panel/nodeDetailsPanel/nodeDetailsPanel.tsx +++ b/libs/designer/src/lib/ui/panel/nodeDetailsPanel/nodeDetailsPanel.tsx @@ -1,3 +1,4 @@ +import constants from '../../../common/constants'; import type { AppDispatch, RootState } from '../../../core'; import { clearPanel, @@ -8,9 +9,10 @@ import { useSelectedNodeId, validateParameter, } from '../../../core'; +import { renameCustomCode } from '../../../core/state/customcode/customcodeSlice'; import { useReadOnly } from '../../../core/state/designerOptions/designerOptionsSelectors'; import { setShowDeleteModal } from '../../../core/state/designerView/designerViewSlice'; -import { ErrorLevel } from '../../../core/state/operation/operationMetadataSlice'; +import { ErrorLevel, updateParameterEditorViewModel } from '../../../core/state/operation/operationMetadataSlice'; import { useIconUri, useOperationErrorInfo } from '../../../core/state/operation/operationSelector'; import { useIsPanelCollapsed, useSelectedPanelTabId } from '../../../core/state/panel/panelSelectors'; import { expandPanel, selectPanelTab, setSelectedNodeId, updatePanelLocation } from '../../../core/state/panel/panelSlice'; @@ -18,12 +20,20 @@ import { useOperationQuery } from '../../../core/state/selectors/actionMetadataS import { useNodeDescription, useRunData, useRunInstance } from '../../../core/state/workflow/workflowSelectors'; import { replaceId, setNodeDescription } from '../../../core/state/workflow/workflowSlice'; import { isOperationNameValid, isRootNodeInGraph } from '../../../core/utils/graph'; +import { ParameterGroupKeys, getCustomCodeFileName, getParameterFromName } from '../../../core/utils/parameters/helper'; import { CommentMenuItem } from '../../menuItems/commentMenuItem'; import { DeleteMenuItem } from '../../menuItems/deleteMenuItem'; import { usePanelTabs } from './usePanelTabs'; -import { WorkflowService, SUBGRAPH_TYPES, isNullOrUndefined } from '@microsoft/logic-apps-shared'; import type { CommonPanelProps, PageActionTelemetryData } from '@microsoft/designer-ui'; -import { PanelContainer, PanelLocation, PanelScope, PanelSize } from '@microsoft/designer-ui'; +import { PanelContainer, PanelScope, PanelSize } from '@microsoft/designer-ui'; +import { + CustomCodeService, + WorkflowService, + SUBGRAPH_TYPES, + isNullOrUndefined, + replaceWhiteSpaceWithUnderscore, + splitFileName, +} from '@microsoft/logic-apps-shared'; import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -48,7 +58,7 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { })); const selectedNodeDisplayName = useNodeDisplayName(selectedNode); - const [width, setWidth] = useState(PanelSize.Auto); + const [width, setWidth] = useState(PanelSize.Auto); const inputs = useSelector((state: RootState) => state.operations.inputParameters[selectedNode]); const comment = useNodeDescription(selectedNode); @@ -80,7 +90,12 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { const handleCommentMenuClick = (_: React.MouseEvent): void => { showCommentBox = !showCommentBox; - dispatch(setNodeDescription({ nodeId: selectedNode, ...(showCommentBox && { description: '' }) })); + dispatch( + setNodeDescription({ + nodeId: selectedNode, + ...(showCommentBox && { description: '' }), + }) + ); }; // Removing the 'add a note' button for subgraph nodes @@ -98,6 +113,41 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { return { valid: isValid, oldValue: isValid ? newId : selectedNode }; }; + // if is customcode file, on blur title, + // delete the existing custom code file name and upload the new file with updated name + const onTitleBlur = (prevTitle: string) => { + const parameter = getParameterFromName(inputs, constants.DEFAULT_CUSTOM_CODE_INPUT); + if (parameter && CustomCodeService().isCustomCode(parameter?.editor, parameter?.editorOptions?.language)) { + const newFileName = getCustomCodeFileName(selectedNode, inputs, idReplacements); + const [, fileExtension] = splitFileName(newFileName); + const oldFileName = replaceWhiteSpaceWithUnderscore(prevTitle) + fileExtension; + if (newFileName === oldFileName) return; + // update the view model with the latest file name + dispatch( + updateParameterEditorViewModel({ + nodeId: selectedNode, + groupId: ParameterGroupKeys.DEFAULT, + parameterId: parameter.id, + editorViewModel: { + ...(parameter.editorViewModel ?? {}), + customCodeData: { + ...(parameter.editorViewModel?.customCodeData ?? {}), + fileName: newFileName, + }, + }, + }) + ); + + dispatch( + renameCustomCode({ + nodeId: selectedNode, + newFileName, + oldFileName, + }) + ); + } + }; + const onCommentChange = (newDescription?: string) => { dispatch(setNodeDescription({ nodeId: selectedNode, description: newDescription })); }; @@ -130,7 +180,8 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { toggleCollapse: dismissPanel, width, layerProps, - panelLocation: panelLocation ?? PanelLocation.Right, + panelLocation, + isResizeable: props.isResizeable, }; return ( @@ -161,7 +212,12 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { inputs.parameterGroups[parameterGroup].parameters.forEach((parameter: any) => { const validationErrors = validateParameter(parameter, parameter.value); dispatch( - updateParameterValidation({ nodeId: selectedNode, groupId: parameterGroup, parameterId: parameter.id, validationErrors }) + updateParameterValidation({ + nodeId: selectedNode, + groupId: parameterGroup, + parameterId: parameter.id, + validationErrors, + }) ); }); }); @@ -172,6 +228,8 @@ export const NodeDetailsPanel = (props: CommonPanelProps): JSX.Element => { onCommentChange={onCommentChange} title={selectedNodeDisplayName} onTitleChange={onTitleChange} + onTitleBlur={onTitleBlur} + setCurrWidth={setWidth} /> ); }; diff --git a/libs/designer/src/lib/ui/panel/nodeDetailsPanel/tabs/parametersTab/index.tsx b/libs/designer/src/lib/ui/panel/nodeDetailsPanel/tabs/parametersTab/index.tsx index bf714f17cb5..bc9504db5dc 100644 --- a/libs/designer/src/lib/ui/panel/nodeDetailsPanel/tabs/parametersTab/index.tsx +++ b/libs/designer/src/lib/ui/panel/nodeDetailsPanel/tabs/parametersTab/index.tsx @@ -1,5 +1,6 @@ import constants from '../../../../../common/constants'; import { useShowIdentitySelectorQuery } from '../../../../../core/state/connection/connectionSelector'; +import { addOrUpdateCustomCode } from '../../../../../core/state/customcode/customcodeSlice'; import { useHostOptions, useReadOnly } from '../../../../../core/state/designerOptions/designerOptionsSelectors'; import type { ParameterGroup } from '../../../../../core/state/operation/operationMetadataSlice'; import { DynamicLoadStatus, ErrorLevel } from '../../../../../core/state/operation/operationMetadataSlice'; @@ -18,7 +19,7 @@ import { } from '../../../../../core/state/selectors/actionMetadataSelector'; import type { VariableDeclaration } from '../../../../../core/state/tokens/tokensSlice'; import { updateVariableInfo } from '../../../../../core/state/tokens/tokensSlice'; -import { useNodeMetadata, useReplacedIds } from '../../../../../core/state/workflow/workflowSelectors'; +import { useNodeDisplayName, useNodeMetadata, useReplacedIds } from '../../../../../core/state/workflow/workflowSelectors'; import type { AppDispatch, RootState } from '../../../../../core/store'; import { getConnectionReference } from '../../../../../core/utils/connectors/connections'; import { isRootNodeInGraph } from '../../../../../core/utils/graph'; @@ -41,7 +42,6 @@ import { ConnectionDisplay } from './connectionDisplay'; import { IdentitySelector } from './identityselector'; import { MessageBar, MessageBarType, Spinner, SpinnerSize } from '@fluentui/react'; import { Divider } from '@fluentui/react-components'; -import { EditorService, equals, getPropertyValue, getRecordEntry, isRecordNotEmpty } from '@microsoft/logic-apps-shared'; import { DynamicCallStatus, PanelLocation, @@ -51,6 +51,15 @@ import { toCustomEditorAndOptions, } from '@microsoft/designer-ui'; import type { ChangeState, ParameterInfo, ValueSegment, OutputToken, TokenPickerMode, PanelTabFn } from '@microsoft/designer-ui'; +import { + CustomCodeService, + EditorService, + equals, + getPropertyValue, + getRecordEntry, + isRecordNotEmpty, + replaceWhiteSpaceWithUnderscore, +} from '@microsoft/logic-apps-shared'; import type { OperationInfo } from '@microsoft/logic-apps-shared'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -197,6 +206,7 @@ const ParameterSection = ({ const rootState = useSelector((state: RootState) => state); const displayNameResult = useConnectorName(operationInfo); const panelLocation = usePanelLocation(); + const nodeTitle = replaceWhiteSpaceWithUnderscore(useNodeDisplayName(nodeId)); const { suppressCastingForSerialize, hideUTFExpressions } = useHostOptions(); @@ -207,11 +217,15 @@ const ParameterSection = ({ const { value, viewModel } = newState; const parameter = nodeInputs.parameterGroups[group.id].parameters.find((param: any) => param.id === id); - const propertiesToUpdate = { value, preservedValue: undefined } as Partial; + const propertiesToUpdate = { + value, + preservedValue: undefined, + } as Partial; if (viewModel !== undefined) { propertiesToUpdate.editorViewModel = viewModel; } + if (getRecordEntry(variables, nodeId)) { if (parameter?.parameterKey === 'inputs.$.name') { dispatch(updateVariableInfo({ id: nodeId, name: value[0]?.value })); @@ -220,6 +234,11 @@ const ParameterSection = ({ } } + if (CustomCodeService().isCustomCode(parameter?.editor, parameter?.editorOptions?.language)) { + const { fileData, fileExtension, fileName } = viewModel.customCodeData; + dispatch(addOrUpdateCustomCode({ nodeId, fileData, fileExtension, fileName })); + } + updateParameterAndDependencies( nodeId, group.id, @@ -395,10 +414,19 @@ const ParameterSection = ({ const remappedEditorViewModel = isRecordNotEmpty(idReplacements) ? remapEditorViewModelWithNewIds(editorViewModel, idReplacements) : editorViewModel; - const paramSubset = { id, label, required, showTokens, placeholder, editorViewModel: remappedEditorViewModel, conditionalVisibility }; + const paramSubset = { + id, + label, + required, + showTokens, + placeholder, + editorViewModel: remappedEditorViewModel, + conditionalVisibility, + }; const { editor, editorOptions } = getEditorAndOptions(operationInfo, param, upstreamNodeIds ?? [], variables); const { value: remappedValues } = isRecordNotEmpty(idReplacements) ? remapValueSegmentsWithNewIds(value, idReplacements) : { value }; + const isCodeEditor = editor?.toLowerCase() === constants.EDITOR.CODE; return { settingType: 'SettingTokenField', @@ -413,6 +441,7 @@ const ParameterSection = ({ errorDetails: dynamicData?.error ? { message: dynamicData.error.message } : undefined, validationErrors, tokenMapping, + nodeTitle, loadParameterValueFromString: (value: string) => loadParameterValueFromString(value), onValueChange: (newState: ChangeState) => onValueChange(id, newState), onComboboxMenuOpen: () => onComboboxMenuOpen(param), @@ -423,7 +452,12 @@ const ParameterSection = ({ suppressCastingForSerialize: suppressCastingForSerialize ?? false, onCastParameter: (value: ValueSegment[], type?: string, format?: string, suppressCasting?: boolean) => parameterValueToString( - { value, type: type ?? 'string', info: { format }, suppressCasting } as ParameterInfo, + { + value, + type: type ?? 'string', + info: { format }, + suppressCasting, + } as ParameterInfo, false, idReplacements ) ?? '', @@ -433,16 +467,7 @@ const ParameterSection = ({ tokenPickerMode?: TokenPickerMode, editorType?: string, tokenClickedCallback?: (token: ValueSegment) => void - ) => - getTokenPicker( - id, - editorId, - labelId, - tokenPickerMode, - editorType, - editor?.toLowerCase() === constants.EDITOR.CODE, - tokenClickedCallback - ), + ) => getTokenPicker(id, editorId, labelId, tokenPickerMode, editorType, isCodeEditor, tokenClickedCallback), }, }; }); @@ -503,7 +528,11 @@ const hasParametersToAuthor = (parameterGroups: Record): export const parametersTab: PanelTabFn = (intl) => ({ id: constants.PANEL_TAB_NAMES.PARAMETERS, - title: intl.formatMessage({ defaultMessage: 'Parameters', id: 'uxKRO/', description: 'Parameters tab title' }), + title: intl.formatMessage({ + defaultMessage: 'Parameters', + id: 'uxKRO/', + description: 'Parameters tab title', + }), description: intl.formatMessage({ defaultMessage: 'Configure parameters for this node', id: 'SToblZ', diff --git a/libs/designer/src/lib/ui/panel/panelRoot.tsx b/libs/designer/src/lib/ui/panel/panelRoot.tsx index 1767a828df5..d1f3c069218 100644 --- a/libs/designer/src/lib/ui/panel/panelRoot.tsx +++ b/libs/designer/src/lib/ui/panel/panelRoot.tsx @@ -18,13 +18,14 @@ import { Panel, PanelType } from '@fluentui/react'; import { Spinner } from '@fluentui/react-components'; import { isUndefined } from '@microsoft/applicationinsights-core-js'; import type { CommonPanelProps, CustomPanelLocation } from '@microsoft/designer-ui'; -import { PanelLocation, PanelSize } from '@microsoft/designer-ui'; +import { PanelLocation, PanelResizer, PanelSize } from '@microsoft/designer-ui'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; export interface PanelRootProps { panelLocation?: PanelLocation; customPanelLocations?: CustomPanelLocation[]; + isResizeable?: boolean; } const layerProps = { @@ -33,7 +34,7 @@ const layerProps = { }; export const PanelRoot = (props: PanelRootProps): JSX.Element => { - const { panelLocation, customPanelLocations } = props; + const { panelLocation = PanelLocation.Right, customPanelLocations, isResizeable } = props; const dispatch = useDispatch(); const isDarkMode = useIsDarkMode(); @@ -41,7 +42,7 @@ export const PanelRoot = (props: PanelRootProps): JSX.Element => { const currentPanelMode = useCurrentPanelMode(); const focusReturnElementId = useFocusReturnElementId(); - const [width, setWidth] = useState(PanelSize.Auto); + const [width, setWidth] = useState(PanelSize.Auto); useEffect(() => { setWidth(collapsed ? PanelSize.Auto : PanelSize.Medium); @@ -57,8 +58,9 @@ export const PanelRoot = (props: PanelRootProps): JSX.Element => { width, layerProps, panelLocation: customLocation ?? panelLocation ?? PanelLocation.Right, + isResizeable: isResizeable, }; - }, [customPanelLocations, currentPanelMode, collapsed, dismissPanel, panelLocation, width]); + }, [customPanelLocations, collapsed, dismissPanel, width, panelLocation, isResizeable, currentPanelMode]); const onRenderFooterContent = useMemo( () => (currentPanelMode === 'WorkflowParameters' ? () => : undefined), @@ -82,7 +84,7 @@ export const PanelRoot = (props: PanelRootProps): JSX.Element => { className={`msla-panel-root-${currentPanelMode}`} isLightDismiss isBlocking={!isLoadingPanel && !nonBlockingPanels.includes(currentPanelMode ?? '')} - type={commonPanelProps.panelLocation === PanelLocation.Right ? PanelType.medium : PanelType.customNear} + type={panelLocation === PanelLocation.Right ? PanelType.custom : PanelType.customNear} isOpen={!collapsed} onDismiss={dismissPanel} hasCloseButton={false} @@ -98,6 +100,7 @@ export const PanelRoot = (props: PanelRootProps): JSX.Element => { }, })} > + {isResizeable ? : null} { isLoadingPanel ? ( diff --git a/libs/designer/src/lib/ui/settings/settingsection.tsx b/libs/designer/src/lib/ui/settings/settingsection.tsx index a8a35e28351..7967754f13d 100644 --- a/libs/designer/src/lib/ui/settings/settingsection.tsx +++ b/libs/designer/src/lib/ui/settings/settingsection.tsx @@ -53,6 +53,7 @@ const ClearIcon = bundleIcon(Dismiss24Filled, Dismiss24Regular); type SettingBase = { visible?: boolean; + nodeTitle?: string; }; export type Settings = SettingBase & @@ -334,7 +335,7 @@ const Setting = ({ id, settings, isReadOnly }: { id?: string; settings: Settings return visible && conditionalVisibility !== false ? (
-
+
{renderSetting()} {errorMessage && !hideErrorMessage[i] && ( diff --git a/libs/logic-apps-shared/src/designer-client-services/index.ts b/libs/logic-apps-shared/src/designer-client-services/index.ts index effc1e92ca3..4d1e959d54b 100644 --- a/libs/logic-apps-shared/src/designer-client-services/index.ts +++ b/libs/logic-apps-shared/src/designer-client-services/index.ts @@ -23,3 +23,4 @@ export * from './lib/staticresult'; export * from './lib/editor'; export * from './lib/connectionParameterEditor'; export * from './lib/chatbot'; +export * from './lib/customcode'; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/__test__/__mocks__/builtInOperationResponse.ts b/libs/logic-apps-shared/src/designer-client-services/lib/__test__/__mocks__/builtInOperationResponse.ts index 6829622d416..fde43d97c1d 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/__test__/__mocks__/builtInOperationResponse.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/__test__/__mocks__/builtInOperationResponse.ts @@ -2084,4 +2084,46 @@ export const almostAllBuiltInOperations: DiscoveryOperation = { operationId: 'invokeFunction', }, [javascriptcode]: { - connectorId: 'connectionProviders/inlineCode', + connectorId: inlineCodeConnectorId, operationId: 'javaScriptCode', }, + [powershellcode]: { + connectorId: inlineCodeConnectorId, + operationId: powershellcode, + }, + [csharpcode]: { + connectorId: inlineCodeConnectorId, + operationId: 'cSharpScriptCode', + }, [join]: { connectorId: dataOperationConnectorId, operationId: join, diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/customcode.ts b/libs/logic-apps-shared/src/designer-client-services/lib/customcode.ts new file mode 100644 index 00000000000..b7b91892d98 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/customcode.ts @@ -0,0 +1,50 @@ +import { AssertionErrorCode, AssertionException } from '@microsoft/logic-apps-shared'; + +export interface UploadCustomCode { + fileData: string; + fileName: string; + fileExtension: string; +} + +export interface VFSObject { + name: string; + size: number; + mtime: string; + crtime: string; + mime: string; + href: string; + path: string; +} + +export const CustomCodeConstants = { + EDITOR: { + CODE: 'code', + }, + EDITOR_OPTIONS: { + LANGUAGE: { + JAVASCRIPT: 'javascript', + }, + }, +}; + +export interface ICustomCodeService { + isCustomCode(editor?: string, language?: string): boolean; + getAllCustomCodeFiles(): Promise; + getCustomCodeFile(fileName: string): Promise; + uploadCustomCode(customCode: UploadCustomCode): Promise; + deleteCustomCode(fileName: string): Promise; +} + +let service: ICustomCodeService; + +export const InitCustomCodeService = (customCodeService: ICustomCodeService): void => { + service = customCodeService; +}; + +export const CustomCodeService = (): ICustomCodeService => { + if (!service) { + throw new AssertionException(AssertionErrorCode.SERVICE_NOT_INITIALIZED, 'Custom Code Service needs to be initialized before using'); + } + + return service; +}; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts b/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts index be698e9bd62..c0d3f48b37a 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts @@ -15,7 +15,7 @@ export interface HttpRequestOptions { uri: string; type?: BatchHttpMethod; content?: ContentType; - headers?: Record; + headers?: Record; queryParameters?: QueryParameters; noAuth?: boolean; returnHeaders?: boolean; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/standard/customcode.ts b/libs/logic-apps-shared/src/designer-client-services/lib/standard/customcode.ts new file mode 100644 index 00000000000..b84fce0ec62 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/standard/customcode.ts @@ -0,0 +1,142 @@ +import type { ICustomCodeService, UploadCustomCode, VFSObject } from '../customcode'; +import { CustomCodeConstants } from '../customcode'; +import type { IHttpClient } from '../httpClient'; +import { equals } from '@microsoft/logic-apps-shared'; + +export interface CustomCodeServiceOptions { + apiVersion: string; + baseUrl: string; + subscriptionId: string; + resourceGroup: string; + appName: string; + workflowName: string; + httpClient: IHttpClient; +} + +export class StandardCustomCodeService implements ICustomCodeService { + constructor(public readonly options: CustomCodeServiceOptions) { + const { apiVersion, baseUrl, subscriptionId, resourceGroup, appName, workflowName, httpClient } = this.options; + if (!apiVersion) { + throw new Error('apiVersion required'); + } else if (!baseUrl) { + throw new Error('baseUrl required'); + } else if (!subscriptionId) { + throw new Error('subscriptionId required'); + } else if (!resourceGroup) { + throw new Error('resourceGroup required'); + } else if (!appName) { + throw new Error('appName required'); + } else if (!workflowName) { + throw new Error('workflowName required'); + } else if (!httpClient) { + throw new Error('httpClient required'); + } + } + + isCustomCode(editor?: string, language?: string): boolean { + return equals(editor, CustomCodeConstants.EDITOR.CODE) && !equals(language, CustomCodeConstants.EDITOR_OPTIONS.LANGUAGE.JAVASCRIPT); + } + + async getAllCustomCodeFiles(): Promise { + const { apiVersion, baseUrl, subscriptionId, resourceGroup, appName, workflowName, httpClient } = this.options; + const uri = `${baseUrl}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${appName}/hostruntime/admin/vfs/${workflowName}`; + + const queryParameters = { + relativePath: 1, + 'api-version': apiVersion, + }; + + try { + const response = await httpClient.get({ + uri, + queryParameters, + }); + + return response; + } catch (error: any) { + if (error?.httpStatusCode === 404) { + return []; + } else { + throw error; + } + } + } + + async getCustomCodeFile(fileName: string): Promise { + const { apiVersion, baseUrl, subscriptionId, resourceGroup, appName, workflowName, httpClient } = this.options; + const uri = `${baseUrl}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${appName}/hostruntime/admin/vfs/${workflowName}/${fileName}`; + const headers: Record = { + 'If-Match': ['*'], + }; + + const queryParameters = { + relativePath: 1, + 'api-version': apiVersion, + }; + + try { + const response = await httpClient.get({ + uri, + queryParameters, + headers, + }); + return response; + } catch (error: any) { + if (error?.httpStatusCode !== 404) { + throw error; + } + return ''; + } + } + + async uploadCustomCode({ fileData, fileName, fileExtension }: UploadCustomCode): Promise { + const { apiVersion, baseUrl, subscriptionId, resourceGroup, appName, workflowName, httpClient } = this.options; + const uri = `${baseUrl}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${appName}/hostruntime/admin/vfs/${workflowName}/${fileName}`; + + const queryParameters = { + relativePath: 1, + 'api-version': apiVersion, + }; + + const headers = { + 'Cache-Control': 'no-cache', + 'Content-Type': fileExtension.substring(fileExtension.indexOf('.') + 1) ?? 'plain/text', + 'If-Match': '*', + }; + try { + await httpClient.put({ + uri, + queryParameters, + headers, + content: fileData, + }); + } catch (error: any) { + if (error?.httpStatusCode !== 404) { + throw error; + } + } + } + + async deleteCustomCode(fileName: string): Promise { + const { apiVersion, baseUrl, subscriptionId, resourceGroup, appName, workflowName, httpClient } = this.options; + const uri = `${baseUrl}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${appName}/hostruntime/admin/vfs/${workflowName}/${fileName}`; + const headers: Record = { + 'If-Match': ['*'], + }; + const queryParameters = { + relativePath: 1, + 'api-version': apiVersion, + }; + try { + await httpClient.delete({ + uri, + queryParameters, + headers, + }); + } catch (error: any) { + if (error?.httpStatusCode !== 404) { + throw error; + } + } + } +} diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/standard/index.ts b/libs/logic-apps-shared/src/designer-client-services/lib/standard/index.ts index 235ab2949fb..588bde06408 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/standard/index.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/standard/index.ts @@ -4,3 +4,4 @@ export { StandardOperationManifestService, isServiceProviderOperation } from './ export { StandardSearchService } from './search'; export { StandardRunService } from './run'; export { StandardArtifactService } from './artifact'; +export { StandardCustomCodeService } from './customcode'; diff --git a/libs/logic-apps-shared/src/intl/compiled-lang/strings.en-XA.json b/libs/logic-apps-shared/src/intl/compiled-lang/strings.en-XA.json index 9371e40e61d..b7098504ef3 100644 --- a/libs/logic-apps-shared/src/intl/compiled-lang/strings.en-XA.json +++ b/libs/logic-apps-shared/src/intl/compiled-lang/strings.en-XA.json @@ -8361,6 +8361,20 @@ "value": "]" } ], + "Mcvr0B": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ŧǿǿ ŭŭşḗḗ ḿǿǿḓŭŭŀḗḗş ǿǿř ḓḗḗƥḗḗƞḓḗḗƈīḗḗş, ƥŀḗḗȧȧşḗḗ ȧȧḓḓ ȧȧŧ Ƈŭŭşŧǿǿḿ Ƈǿǿḓḗḗ Ḓḗḗƥḗḗƞḓḗḗƞƞƈīḗḗş īƞ Ƥǿǿřŧȧȧŀ ŦǾƇ" + }, + { + "type": 0, + "value": "]" + } + ], "MfAdfx": [ { "type": 0, @@ -10751,6 +10765,20 @@ "value": "]" } ], + "TjkOzp": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƈŀǿǿşḗḗ" + }, + { + "type": 0, + "value": "]" + } + ], "TlX98E": [ { "type": 0, diff --git a/libs/logic-apps-shared/src/intl/compiled-lang/strings.json b/libs/logic-apps-shared/src/intl/compiled-lang/strings.json index 342d0fffa61..f5f4aeb5df1 100644 --- a/libs/logic-apps-shared/src/intl/compiled-lang/strings.json +++ b/libs/logic-apps-shared/src/intl/compiled-lang/strings.json @@ -3801,6 +3801,12 @@ "value": "Search" } ], + "Mcvr0B": [ + { + "type": 0, + "value": "To use modules or dependecies, please add at Custom Code Dependenncies in Portal TOC" + } + ], "MfAdfx": [ { "type": 0, @@ -4895,6 +4901,12 @@ "value": "(UTC-06:00) Easter Island" } ], + "TjkOzp": [ + { + "type": 0, + "value": "Close" + } + ], "TlX98E": [ { "type": 0, diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/color.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/color.ts index 15e8da0a5a0..a65c9dc7500 100644 --- a/libs/logic-apps-shared/src/utils/src/lib/helpers/color.ts +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/color.ts @@ -141,3 +141,16 @@ export const lighten = ({ blue, green, red }: RGB, amount: number): RGB => { red: lightenColor(red), }; }; + +export const darken = ({ blue, green, red }: RGB, amount: number): RGB => { + const darkenColor = (color: number): number => { + const adjustBy = color * amount; + return Math.round(color - adjustBy); + }; + + return { + blue: darkenColor(blue), + green: darkenColor(green), + red: darkenColor(red), + }; +}; diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/customcode.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/customcode.ts new file mode 100644 index 00000000000..bcf30287163 --- /dev/null +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/customcode.ts @@ -0,0 +1,37 @@ +export const EditorLanguage = { + javascript: 'javascript', + json: 'json', + xml: 'xml', + templateExpressionLanguage: 'TemplateExpressionLanguage', + yaml: 'yaml', + csharp: 'csharp', + powershell: 'powershell', +} as const; +export type EditorLanguage = (typeof EditorLanguage)[keyof typeof EditorLanguage]; + +/** + * Gets the extension name based on EditorLanguage. + * @arg {EditorLanguage} language - The Editor Language to get extension name of. + * @return {string} - The Extension Name + */ +export const getFileExtensionName = (language: EditorLanguage): string => { + switch (language) { + case EditorLanguage.csharp: + return '.cs'; + case EditorLanguage.powershell: + return '.ps1'; + default: + return '.txt'; + } +}; + +export const getFileExtensionNameFromOperationId = (operationId: string): string => { + switch (operationId) { + case 'csharpcode': + return '.cs'; + case 'powershellcode': + return '.ps1'; + default: + return '.txt'; + } +}; diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/index.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/index.ts index 17f110289b3..48b5a12eb6d 100644 --- a/libs/logic-apps-shared/src/utils/src/lib/helpers/index.ts +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/index.ts @@ -1,14 +1,15 @@ export * from './color'; export * from './connections'; export * from './connectors'; +export * from './customcode'; +export * from './flow-utils'; export * from './functions'; export * from './guid'; -export * from './stringFunctions'; export * from './hooks'; export * from './http'; -export * from './flow-utils'; -export * from './operations'; export * from './logicapps'; +export * from './navigator'; +export * from './operations'; export * from './recurrence'; export * from './run'; -export * from './navigator'; +export * from './stringFunctions'; diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/stringFunctions.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/stringFunctions.ts index 4a1b07ae8b3..a620f75a067 100644 --- a/libs/logic-apps-shared/src/utils/src/lib/helpers/stringFunctions.ts +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/stringFunctions.ts @@ -1,6 +1,10 @@ export const idDisplayCase = (s: string) => removeIdTag(labelCase(s)); export const labelCase = (label: string) => label?.replace(/_/g, ' '); +export const replaceWhiteSpaceWithUnderscore = (uiElementName: string): string => { + return uiElementName?.replace(/\W/g, '_')?.toLowerCase(); +}; + export const containsIdTag = (id: string) => id?.includes('-#'); export const removeIdTag = (id: string) => id?.split('-#')[0]; @@ -20,3 +24,8 @@ export const wrapTokenValue = (s: string) => `@{${s}}`; export const cleanConnectorId = (id: string) => id.replace(/[()]/g, ''); export const prettifyJsonString = (json: string) => JSON.stringify(JSON.parse(json), null, 4); + +export const splitFileName = (fileName: string) => { + const splitFileName = fileName.lastIndexOf('.'); + return [fileName.slice(0, splitFileName), fileName.slice(splitFileName)]; +};