You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by po...@apache.org on 2023/09/21 14:30:03 UTC
[incubator-kie-tools-temporary-rnd-do-not-use] 11/27: NO-ISSUE: merge main into KOGITO-8015-feature-preview (#1529)
This is an automated email from the ASF dual-hosted git repository.
porcelli pushed a commit to branch KOGITO-8015-feature-preview
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools-temporary-rnd-do-not-use.git
commit 1ffcadda8811af01ac6efadb4d95eafa9f922e17
Author: Fabrizio Antonangeli <fa...@gmail.com>
AuthorDate: Tue Mar 21 00:01:51 2023 +0100
NO-ISSUE: merge main into KOGITO-8015-feature-preview (#1529)
Signed-off-by: dependabot[bot] <su...@github.com>
Co-authored-by: Handreyrc <47...@users.noreply.github.com>
Co-authored-by: Roger Martinez <ro...@gmail.com>
Co-authored-by: Tiago Bento <15...@users.noreply.github.com>
Co-authored-by: Kumar Aditya Raj <se...@gmail.com>
Co-authored-by: Kirill Gaevskii <kg...@redhat.com>
Co-authored-by: William Antônio Siqueira <wi...@gmail.com>
Co-authored-by: Jaime Enriquez <ja...@inodesoft.com>
Co-authored-by: Luiz João Motta <lu...@gmail.com>
Co-authored-by: Zbyněk Drápela <61...@users.noreply.github.com>
Co-authored-by: Manaswini Das <da...@gmail.com>
Co-authored-by: Pere Fernández <pe...@users.noreply.github.com>
Co-authored-by: Paulo Martins <pa...@gmail.com>
Co-authored-by: Guilherme Caponetto <63...@users.noreply.github.com>
Co-authored-by: Thiago Lugli <th...@users.noreply.github.com>
Co-authored-by: Daniel J dos Santos <da...@users.noreply.github.com>
Co-authored-by: Daniel J dos Santos <da...@ibm.com>
Co-authored-by: Lubo Terifaj <lu...@gmail.com>
Co-authored-by: dependabot[bot] <49...@users.noreply.github.com>
Co-authored-by: Barbora Kapustova (Siskova) <ba...@users.noreply.github.com>
Co-authored-by: Saravana Balaji Srinivasan <40...@users.noreply.github.com>
Co-authored-by: Bai Xiaofeng <bx...@gmail.com>
Co-authored-by: Kbowers <92...@users.noreply.github.com>
Co-authored-by: Kennedy Bowers <ke...@ibm.com>
Co-authored-by: Ajay Jaganathan <36...@users.noreply.github.com>
Co-authored-by: Jan Stastny <st...@gmail.com>
Co-authored-by: jstastny-cz <ja...@ibm.com>
Co-authored-by: Tiago <ti...@gmail.com>
---
.github/workflows/release_build.yml | 4 +-
.../base64png-editor-vscode-extension/package.json | 2 +-
examples/base64png-editor/src/Base64PngEditor.tsx | 6 +-
examples/commit-message-validation-service/go.mod | 8 +-
examples/commit-message-validation-service/go.sum | 15 +-
.../todo-list-view-vscode-extension/package.json | 2 +-
examples/uniforms-patternfly/src/App.tsx | 4 +-
examples/webapp/src/App.tsx | 4 +-
examples/webapp/src/Home.tsx | 3 +-
.../src/Pages/Base64Png/Base64PngGallery.tsx | 4 +-
.../webapp/src/Pages/Base64Png/Base64PngPage.tsx | 2 +-
.../webapp/src/Pages/KogitoEditors/BpmnPage.tsx | 2 +-
.../webapp/src/Pages/KogitoEditors/DmnPage.tsx | 2 +-
.../webapp/src/Pages/KogitoEditors/Sidebar.tsx | 3 +-
.../Angular/PingPongAngularDivViewsPage.tsx | 2 +-
.../Angular/PingPongAngularIFrameViewsPage.tsx | 2 +-
.../PingPong/Mixed/PingPongMixedViewsPage.tsx | 2 +-
.../PingPong/React/PingPongReactDivViewsPage.tsx | 2 +-
.../React/PingPongReactIFrameViewsPage.tsx | 2 +-
.../webapp/src/Pages/PingPong/StatsSidebar.tsx | 4 +-
.../StandaloneEditors/DmnStandaloneEditorPage.tsx | 2 +-
.../SwfStandaloneDiagramOnlyEditorPage.tsx | 2 +-
.../StandaloneEditors/SwfStandaloneEditorPage.tsx | 2 +-
.../SwfStandaloneTextOnlyEditorPage.tsx | 2 +-
.../webapp/src/Pages/TodoList/TodoListViewPage.tsx | 4 +-
.../boxed-expression-component/showcase/index.tsx | 3 +-
.../DecisionTableExpression/HitPolicySelector.tsx | 2 +-
.../EditExpressionMenu/DataTypeSelector.tsx | 3 +-
.../EditExpressionMenu/EditExpressionMenu.tsx | 2 +-
.../FunctionExpression/EditParameters.tsx | 2 +-
.../FunctionExpression/FunctionKindSelector.tsx | 2 +-
.../LiteralExpression/PMMLLiteralExpression.tsx | 2 +-
.../LogicTypeSelector/LogicTypeSelector.tsx | 2 +-
.../src/components/PopoverMenu/PopoverMenu.tsx | 2 +-
.../src/components/Table/TableHandler.tsx | 2 +-
.../src/components/Table/TableHandlerMenu.tsx | 2 +-
.../src/FilteredTable.tsx | 4 +-
.../src/UniformsComponent.tsx | 2 +-
.../src/VictoryChartComponent.tsx | 2 +-
.../webpack.config.js | 4 +
.../external/ExternalDataSetClientProvider.java | 23 +-
.../services/RuntimeDataSetClientServices.java | 2 +-
.../dashbuilder/displayer/DisplayerSettings.java | 2 +-
.../dataset/json/ExternalDataSetJSONParser.java | 23 +-
packages/eslint/.eslintrc.js | 21 +
packages/i18n-common-dictionary/src/CommonI18n.ts | 1 +
packages/i18n-common-dictionary/src/locales/en.ts | 1 +
.../ImportJavaClasses/ImportJavaClassesWizard.tsx | 5 +-
.../ImportJavaClassesWizardFieldListTable.tsx | 2 +-
.../ImportJavaClassesWizardFirstStep.tsx | 16 +-
.../ImportJavaClassesWizardSecondStep.tsx | 3 +-
packages/kie-sandbox-image/README.md | 85 +-
packages/kn-plugin-workflow/env/index.js | 2 +-
packages/kn-plugin-workflow/go.mod | 2 +-
packages/kn-plugin-workflow/go.sum | 4 +-
packages/online-editor/build/defaultEnvJson.ts | 11 +
packages/online-editor/package.json | 2 +
.../src/accelerators/AcceleratorsApi.ts | 41 +
.../src/accelerators/AcceleratorsHooks.tsx | 413 +
.../src/authSessions/AuthSessionSelect.tsx | 3 +-
packages/online-editor/src/editor/EditorPage.tsx | 2 +-
.../online-editor/src/editor/EditorToolbar.tsx | 2237 --
.../Toolbar/Accelerators/AcceleratorIcon.tsx} | 20 +-
.../Toolbar/Accelerators/AcceleratorIndicator.tsx | 63 +
.../Toolbar/Accelerators/AcceleratorModal.tsx | 125 +
.../Toolbar/Accelerators/AcceleratorsDropdown.tsx | 109 +
.../src/editor/Toolbar/EditorToolbar.tsx | 438 +
.../Toolbar/EditorToolbarContextProvider.tsx | 207 +
.../src/editor/Toolbar/FileStatus.tsx | 121 +
.../src/editor/{ => Toolbar}/FileSwitcher.tsx | 802 +-
.../GitIntegration}/CreateGistOrSnippetModal.tsx | 46 +-
.../GitIntegration}/CreateGitRepositoryModal.tsx | 35 +-
.../GitIntegration/GitIntegrationAlerts.tsx | 396 +
.../GitIntegrationContextProvider.tsx | 630 +
.../GitIntegration}/LoadOrganizationsSelect.tsx | 50 +-
.../GitIntegration/PushToGitAlertActionLinks.tsx | 90 +
.../src/editor/Toolbar/KebabDropdown.tsx | 46 +
.../editor/{ => Toolbar}/NewFileDropdownMenu.tsx | 106 +-
.../editor/Toolbar/Share/DownloadDropdownGroup.tsx | 112 +
.../editor/Toolbar/Share/EmbedDropdownGroup.tsx | 63 +
.../src/editor/{ => Toolbar/Share}/EmbedModal.tsx | 4 +-
.../src/editor/Toolbar/Share/GitDropdownGroup.tsx | 161 +
.../src/editor/Toolbar/SyncDropdownMenu.tsx | 279 +
.../src/editor/Toolbar/VsCodeDropdownMenu.tsx | 160 +
.../src/editor/Toolbar/Workspace/Hooks.tsx | 154 +
.../editor/Toolbar/Workspace/WorkspaceToolbar.tsx | 177 +
packages/online-editor/src/env/EnvJson.ts | 2 +
.../EditorEnvelopeLocatorFactory.ts | 70 +-
.../online-editor/src/filesList/FileDataList.tsx | 139 +-
packages/online-editor/src/home/HomePage.tsx | 24 +-
packages/online-editor/src/i18n/OnlineI18n.ts | 28 +
packages/online-editor/src/i18n/locales/en.ts | 37 +-
.../importFromUrl/ImportFromUrlHomePageCard.tsx | 3 +-
.../src/pageTemplate/OnlineEditorPage.tsx | 3 +-
.../components/GitStatusIndicatorActions.tsx | 479 +
.../src/workspace/components/PromiseModal.tsx | 30 +-
.../workspace/components/WorkspaceCommitModal.tsx | 12 +-
.../components/WorkspaceStatusIndicator.tsx | 286 +-
...ontextProviderWithCustomCommitMessagesModal.tsx | 17 +-
packages/online-editor/static/resources/style.css | 70 +-
.../it-tests/e2e/CreateNewFile.cy.ts | 4 +-
packages/serverless-logic-web-tools/package.json | 2 +
.../src/editor/EditorPage.tsx | 13 +-
.../src/editor/EditorPageErrorPage.tsx | 2 +-
.../src/editor/FileSwitcher.tsx | 16 +-
.../src/editor/NewFileDropdownMenu.tsx | 20 +-
.../src/editor/api/WebToolsSwfLanguageService.ts | 23 +-
.../src/homepage/breadcrumb/HomepageBreadcrumb.tsx | 2 +-
.../src/homepage/pageTemplate/OnlineEditorPage.tsx | 15 +-
.../src/homepage/uiNav/HomePageNav.tsx | 2 +-
.../KieSandboxExtendedServicesSettings.tsx | 4 +-
.../src/settings/github/GitHubSettings.tsx | 5 +-
.../src/settings/kafka/ApacheKafkaSettings.tsx | 4 +-
.../src/settings/openshift/OpenShiftSettings.tsx | 4 +-
.../serviceAccount/ServiceAccountSettings.tsx | 4 +-
.../serviceRegistry/ServiceRegistrySettings.tsx | 4 +-
.../src/settings/uiNav/SettingsPageNav.tsx | 2 +-
.../components/WorkspaceStatusIndicator.tsx | 2 +-
.../static/resources/style.css | 4 +
.../common/stunner/sw/autolayout/AutoLayout.java | 149 +-
.../stunner/sw/resources/i18n/SWConstants.java | 60 +
.../sw/resources/i18n/SWConstants.properties | 22 +
.../stunner/sw/autolayout/AutoLayoutTest.java | 13 +-
.../sw/client/CanvasShortcutsControlImpl.java | 4 +-
.../common/stunner/sw/client/ShapeFactory.java | 9 +-
.../sw/client/shapes/CallbackStateShape.java | 41 +-
.../common/stunner/sw/client/shapes/EndShape.java | 8 +-
.../stunner/sw/client/shapes/EndShapeView.java | 4 +-
.../stunner/sw/client/shapes/EventStateShape.java | 33 +-
.../sw/client/shapes/ForEachStateShape.java | 38 +-
.../stunner/sw/client/shapes/HasActions.java | 79 +
.../stunner/sw/client/shapes/HasDataFilter.java | 49 +
.../stunner/sw/client/shapes/HasTranslation.java} | 11 +-
.../stunner/sw/client/shapes/InjectStateShape.java | 28 +-
.../sw/client/shapes/OperationStateShape.java | 44 +-
.../sw/client/shapes/ParallelStateShape.java | 30 +-
.../sw/client/shapes/ServerlessWorkflowShape.java | 25 +-
.../stunner/sw/client/shapes/SleepStateShape.java | 17 +-
.../stunner/sw/client/shapes/StartShape.java | 8 +-
.../stunner/sw/client/shapes/StartShapeView.java | 4 +-
.../stunner/sw/client/shapes/StateShape.java | 41 +-
.../stunner/sw/client/shapes/StateShapeView.java | 21 +-
.../stunner/sw/client/shapes/SwitchStateShape.java | 30 +-
.../stunner/sw/client/shapes/TransitionShape.java | 4 +-
.../sw/client/shapes/icons/BottomDepiction.java | 63 +
.../stunner/sw/client/shapes/icons/CornerIcon.java | 29 +-
.../stunner/sw/client/shapes/icons/IconPath.java | 3 +
.../sw/client/shapes/icons/IconPosition.java | 1 +
.../stunner/sw/client/shapes/HasActionsTest.java | 182 +
.../sw/client/shapes/HasDataFilterTest.java | 123 +
.../client/shapes/HasTranslationGeneralTest.java | 66 +
.../stunner/sw/client/shapes/StateShapeTest.java | 20 +-
.../sw/client/selenium/SWEditorSeleniumIT.java | 164 +-
.../stunner/sw/client/selenium/LoanBroker.sw.json | 215 +
.../serverless-workflow-jq-expressions/README.md | 1 +
.../env/index.js} | 15 +-
.../jest.config.js} | 41 +-
.../package.json | 10 +-
.../src/api/JqExpressionReadSchemas.ts} | 13 +-
.../src/api}/index.ts | 10 +-
.../src/api/types.ts} | 13 +-
.../src/impl/JqExpressionReadSchemasImpl.ts | 89 +
.../src/impl}/index.ts | 9 +-
.../src/utils/JqBuiltInFunctions.ts | 575 +
.../src/utils}/index.ts | 10 +-
.../src/utils/utils.ts} | 23 +-
.../tests/JqExpressionReadSchemasImpl.test.ts | 72 +
.../tests/mockSchema.ts | 20 +
.../tests/utils.test.ts} | 22 +-
.../tsconfig.json | 8 +
.../package.json | 1 +
.../empty-workflow-completions.ts} | 21 +-
.../src/assets/code-completions/index.ts | 1 +
.../src/channel/SwfLanguageService.ts | 95 +-
.../channel/SwfLanguageServiceCodeCompletion.ts | 296 +-
.../src/channel/SwfYamlLanguageService.ts | 16 +-
.../src/channel/types.ts | 14 +
.../tests/SwfJqExpressionTestUtils.ts | 80 +
.../tests/SwfJsonLanguageService.test.ts | 378 +-
.../tests/SwfLanguageServiceConfigs.ts | 8 +-
.../tests/SwfYamlLanguageService.test.ts | 301 +-
.../SwfJsonLanguageService.test.ts.snap | 20886 ++++++++++++++++++-
.../SwfYamlLanguageService.test.ts.snap | 17330 ++++++++++++++-
.../tests/findNodesAtLocation.test.ts | 14 +-
.../tests/getNodeFormat.test.ts | 4 +-
.../tests/matchNodeWithLocation.test.ts | 15 +-
.../tests/nodeUpUntilType.test.ts | 4 +-
.../tests/refValidations.test.ts | 3 +
.../tests/testUtils.ts | 1 -
.../package.json | 1 +
.../src/api/types.ts | 3 +
.../src/channel/parsers/impl/asyncapi/asyncapi.ts | 1 -
.../parsers/impl/camelRoutes/CamelRoutesParser.ts | 30 +
.../parsers/impl/camelRoutes/camelRoutes.ts | 94 +
.../src/channel/parsers/impl/camelRoutes/types.ts | 52 +
.../parsers/impl/jsonschema/JsonSchemaParser.ts} | 16 +-
.../channel/parsers/impl/jsonschema/jsonschema.ts | 71 +
.../src/channel/parsers/impl/openapi/openapi.ts | 20 +-
.../src/channel/parsers/index.ts | 2 +
.../src/channel/parsers/parseApiContent.ts | 15 +-
.../parsers/camelRoutes/camelRoutes.test.ts | 79 +
.../tests/channel/parsers/examples/expression.json | 20 +
.../channel/parsers/examples/numberToWords.yaml | 22 +
.../channel/parsers/examples/sendMessage.yaml | 7 +
.../channel/parsers/jsonschema/jsonschema.test.ts | 62 +
.../tests/channel/parsers/parseApiContent.test.ts | 9 +-
.../package.json | 1 +
.../getLanguageServiceChannelApi.ts | 22 +
.../dev-webapp/App.tsx | 108 +-
.../dev-webapp/HistoryButtons.tsx | 72 +-
.../channel/DevWebAppSwfLanguageService.ts} | 23 +-
.../channel/SwfLanguageServiceChannelApiImpl.ts | 39 +
.../channel/SwfTextEditorChannelApiImpl.ts | 156 +
.../serverless-workflow-text-editor/package.json | 1 +
.../textEditor/augmentation/completion/index.ts | 4 +-
.../expressions/types/list/AddListRowCommand.java | 4 +-
.../common/dmn/client/graph/DMNSizeHandler.java | 41 +
.../client/widgets/toolbar/DMNGraphProcessor.java | 194 +-
.../widgets/toolbar/DMNSugiyamaLayoutService.java | 16 +-
.../types/list/AddListRowCommandTest.java | 4 +-
.../dmn/client/graph/DMNSizeHandlerTest.java | 75 +
.../widgets/toolbar/DMNGraphProcessorTest.java | 414 +-
.../client/editor/AbstractDMNDiagramEditor.java | 7 +-
.../editor/AbstractDMNDiagramEditorTest.java | 6 +-
.../showcase/client/editor/DMNDiagramEditor.java | 4 +-
.../dmn13-expected/0082-feel-coercion.dmn | 2061 +-
.../dmn13-expected/0085-decision-services.dmn | 2627 +--
.../dmn13-expected/0088-no-decision-logic.dmn | 485 +-
.../graph/processing/layout/GraphProcessor.java | 77 +
.../graph/processing/layout/LayoutExecutor.java | 3 +-
.../graph/processing/layout/LayoutService.java | 7 +
.../{LayoutExecutor.java => SizeHandler.java} | 12 +-
.../layout/sugiyama/step04/VertexPositioning.java | 8 +-
.../client/components/layout/LayoutHelper.java | 5 +-
.../layout/OpenDiagramLayoutExecutor.java | 32 +-
.../components/layout/UndoableLayoutExecutor.java | 3 +-
.../client/components/layout/LayoutHelperTest.java | 4 +-
.../layout/UndoableLayoutExecutorTest.java | 6 +-
.../processing/layout/sugiyama/LayeredGraph.java | 3 +-
.../layout/sugiyama/SugiyamaLayoutService.java | 102 +-
.../sugiyama/step04/DefaultVertexPositioning.java | 83 +-
.../graph/processing/layout/IntegrationTests.java | 9 +-
.../layout/sugiyama/SugiyamaLayoutServiceTest.java | 6 +-
.../step04/DefaultVertexPositioningTest.java | 206 +-
.../converters/tostunner/DefinitionResolver.java | 19 +
.../processes/BaseRootProcessConverter.java | 2 +-
.../marshall/service/BPMNClientMarshalling.java | 6 +-
.../tostunner/DefinitionResolverTest.java | 14 +
packages/uniforms-patternfly/src/BaseForm.tsx | 2 +-
packages/unitables/src/Unitables.tsx | 2 +-
packages/vscode-extension-backend/package.json | 2 +-
packages/vscode-extension-bpmn-editor/package.json | 2 +-
.../.mocharc.json | 7 +
.../CHANGELOG.md | 13 +
.../dashbuilder-editor-extension-smoke.test.ts | 78 +
.../it-tests/helpers/VSCodeTestHelper.ts | 219 +
.../dashbuilder/DashbuilderEditorTestHelper.ts | 50 +
.../DashbuilderTextEditorTestHelper.ts} | 16 +-
.../smoke-test/empty-dashbuilder-example.dash.yaml | 0
.../smoke-test/simple-dashbuilder-example.dash.yml | 36 +
.../it-tests/settings.json | 2 +-
.../mocha-reporter-config.json | 15 +
.../package.json | 19 +-
.../tsconfig.it-tests.json | 11 +
packages/vscode-extension-dmn-editor/package.json | 2 +-
.../vscode-extension-kie-ba-bundle/package.json | 2 +-
.../vscode-extension-kogito-bundle/package.json | 2 +-
.../it-tests/settings.json | 1 +
.../package.json | 6 +-
packages/vscode-extension-pmml-editor/package.json | 2 +-
.../package.json | 2 +-
.../CHANGELOG.md | 11 +
.../emptyworkflow_autocompletion.sw.json | 0
.../emptyworkflow_autocompletion.sw.json.result | 11 +
...orkflow-editor-extension-autocompletion.test.ts | 24 +-
...low-editor-extension-diagram-navigation.test.ts | 6 +-
.../it-tests/settings.json | 1 +
.../package.json | 12 +-
.../builtInVsCodeEditorSwfContributions.ts | 3 +-
.../src/extension/configuration.ts | 24 +
.../fs/JqExpressionsReadSchemaFromFs.ts | 62 +
.../languageService/VsCodeSwfLanguageService.ts | 55 +-
.../SwfServiceCatalogSupportActions.ts | 15 +-
.../fs/FsWatchingServiceCatalogRelativeStore.ts | 113 +-
.../it-tests/settings.json | 1 +
packages/vscode-extension-yard-editor/package.json | 6 +-
.../src/context/WorkspacesContext.tsx | 24 +-
.../src/context/WorkspacesContextProvider.tsx | 78 +-
.../src/hooks/WorkspaceFileHooks.tsx | 3 +
.../workspaces-git-fs/src/hooks/WorkspaceHooks.tsx | 38 +-
.../workspaces-git-fs/src/services/GitService.tsx | 77 +-
.../src/services/WorkspaceDescriptorService.tsx | 5 -
.../src/worker/WorkspacesWorkerApiImpl.ts | 127 +-
.../src/worker/api/WorkspaceBroadcastEvents.ts | 4 +-
.../src/worker/api/WorkspacesWorkerGitApi.ts | 25 +-
packages/yard-editor/src/editor/YardEditor.tsx | 8 +-
packages/yard-editor/src/uiEditor/YardUIEditor.tsx | 19 +-
pnpm-lock.yaml | 604 +-
repo/graph.dot | 6 +-
repo/graph.json | 9 +-
300 files changed, 53517 insertions(+), 6607 deletions(-)
diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml
index 9ff10c30c6..2007fdd061 100644
--- a/.github/workflows/release_build.yml
+++ b/.github/workflows/release_build.yml
@@ -184,7 +184,7 @@ jobs:
KIE_SANDBOX__imageAccount: "kie-tools"
KIE_SANDBOX__imageName: "kie-sandbox-image"
KIE_SANDBOX__imageBuildTags: "latest ${{ inputs.tag }}"
- ONLINE_EDITOR__buildInfo: ""
+ ONLINE_EDITOR__buildInfo: "${{ inputs.tag }}"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlLinux: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_linux_${{ inputs.tag }}.tar.gz"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlMacOs: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_macos_${{ inputs.tag }}.dmg"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlWindows: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_windows_${{ inputs.tag }}.exe"
@@ -384,7 +384,7 @@ jobs:
online_editor:
env:
- ONLINE_EDITOR__buildInfo: ""
+ ONLINE_EDITOR__buildInfo: "${{ inputs.tag }}"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlLinux: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_linux_${{ inputs.tag }}.tar.gz"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlMacOs: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_macos_${{ inputs.tag }}.dmg"
ONLINE_EDITOR__kieSandboxExtendedServicesDownloadUrlWindows: "https://github.com/kiegroup/kie-tools/releases/download/${{ inputs.tag }}/kie_sandbox_extended_services_windows_${{ inputs.tag }}.exe"
diff --git a/examples/base64png-editor-vscode-extension/package.json b/examples/base64png-editor-vscode-extension/package.json
index a0e10c5119..adfdd211be 100644
--- a/examples/base64png-editor-vscode-extension/package.json
+++ b/examples/base64png-editor-vscode-extension/package.json
@@ -35,7 +35,7 @@
"@kie-tools/root-env": "workspace:*",
"@kie-tools/tsconfig": "workspace:*",
"@types/vscode": "^1.66.0",
- "@vscode/vsce": "^2.15.0",
+ "@vscode/vsce": "^2.18.0",
"rimraf": "^3.0.2",
"typescript": "^4.4.2",
"webpack": "^5.36.2",
diff --git a/examples/base64png-editor/src/Base64PngEditor.tsx b/examples/base64png-editor/src/Base64PngEditor.tsx
index 426567c628..cc9efd7c55 100644
--- a/examples/base64png-editor/src/Base64PngEditor.tsx
+++ b/examples/base64png-editor/src/Base64PngEditor.tsx
@@ -17,7 +17,11 @@
import * as React from "react";
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import { EditorApi, KogitoEditorEnvelopeContextType } from "@kie-tools-core/editor/dist/api";
-import { EmptyState, EmptyStateIcon, Nav, NavItem, NavList, Page, Switch, Title } from "@patternfly/react-core";
+import { EmptyState, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Page } from "@patternfly/react-core/dist/js/components/Page";
+import { Switch } from "@patternfly/react-core/dist/js/components/Switch";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
import { DEFAULT_RECT } from "@kie-tools-core/guided-tour/dist/api";
import CubesIcon from "@patternfly/react-icons/dist/js/icons/cubes-icon";
import "./styles.scss";
diff --git a/examples/commit-message-validation-service/go.mod b/examples/commit-message-validation-service/go.mod
index d13caea2e9..6a934889d1 100644
--- a/examples/commit-message-validation-service/go.mod
+++ b/examples/commit-message-validation-service/go.mod
@@ -20,10 +20,10 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
- golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
- golang.org/x/net v0.4.0 // indirect
- golang.org/x/sys v0.3.0 // indirect
- golang.org/x/text v0.5.0 // indirect
+ golang.org/x/crypto v0.1.0 // indirect
+ golang.org/x/net v0.7.0 // indirect
+ golang.org/x/sys v0.5.0 // indirect
+ golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/examples/commit-message-validation-service/go.sum b/examples/commit-message-validation-service/go.sum
index 5e818b70f5..0270bb7889 100644
--- a/examples/commit-message-validation-service/go.sum
+++ b/examples/commit-message-validation-service/go.sum
@@ -67,26 +67,27 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/examples/todo-list-view-vscode-extension/package.json b/examples/todo-list-view-vscode-extension/package.json
index f22dfa6049..e2c73d5e44 100644
--- a/examples/todo-list-view-vscode-extension/package.json
+++ b/examples/todo-list-view-vscode-extension/package.json
@@ -33,7 +33,7 @@
"@kie-tools/root-env": "workspace:*",
"@kie-tools/tsconfig": "workspace:*",
"@types/vscode": "^1.66.0",
- "@vscode/vsce": "^2.15.0",
+ "@vscode/vsce": "^2.18.0",
"rimraf": "^3.0.2",
"typescript": "^4.4.2",
"webpack": "^5.36.2",
diff --git a/examples/uniforms-patternfly/src/App.tsx b/examples/uniforms-patternfly/src/App.tsx
index 1edc8c61c0..fd21a4408e 100644
--- a/examples/uniforms-patternfly/src/App.tsx
+++ b/examples/uniforms-patternfly/src/App.tsx
@@ -18,7 +18,9 @@ import "@patternfly/react-core/dist/styles/base.css";
import * as React from "react";
import "../static/resources/styles.css";
import { useState } from "react";
-import { Brand, Nav, NavItem, NavList, Page, PageHeader } from "@patternfly/react-core";
+import { Brand } from "@patternfly/react-core/dist/js/components/Brand";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Page, PageHeader } from "@patternfly/react-core/dist/js/components/Page";
import { Form } from "./Form";
export enum SchemaType {
diff --git a/examples/webapp/src/App.tsx b/examples/webapp/src/App.tsx
index ddf0de2c78..5c841d4085 100644
--- a/examples/webapp/src/App.tsx
+++ b/examples/webapp/src/App.tsx
@@ -17,7 +17,9 @@
import "@patternfly/react-core/dist/styles/base.css";
import * as React from "react";
import { useEffect, useState } from "react";
-import { Brand, Nav, NavItem, NavList, Page, PageHeader } from "@patternfly/react-core";
+import { Brand } from "@patternfly/react-core/dist/js/components/Brand";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Page, PageHeader } from "@patternfly/react-core/dist/js/components/Page";
import { HashRouter as Router, Link, Route, Switch } from "react-router-dom";
import { Base64PngPage } from "./Pages/Base64Png/Base64PngPage";
import { BpmnPage } from "./Pages/KogitoEditors/BpmnPage";
diff --git a/examples/webapp/src/Home.tsx b/examples/webapp/src/Home.tsx
index 76f394f426..7c5ba9f840 100644
--- a/examples/webapp/src/Home.tsx
+++ b/examples/webapp/src/Home.tsx
@@ -15,7 +15,8 @@
*/
import * as React from "react";
-import { Page, PageSection, Text, TextContent } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
+import { Text, TextContent } from "@patternfly/react-core/dist/js/components/Text";
export function Home() {
return (
diff --git a/examples/webapp/src/Pages/Base64Png/Base64PngGallery.tsx b/examples/webapp/src/Pages/Base64Png/Base64PngGallery.tsx
index c1b42ff7ce..585cffcdd2 100644
--- a/examples/webapp/src/Pages/Base64Png/Base64PngGallery.tsx
+++ b/examples/webapp/src/Pages/Base64Png/Base64PngGallery.tsx
@@ -16,7 +16,9 @@
import * as React from "react";
import { useCallback, useEffect, useState } from "react";
-import { Nav, NavItem, Title, Card, NavList } from "@patternfly/react-core";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
+import { Card } from "@patternfly/react-core/dist/js/components/Card";
import { EmbeddedEditorFile } from "@kie-tools-core/editor/dist/channel";
const samplePaths = [
diff --git a/examples/webapp/src/Pages/Base64Png/Base64PngPage.tsx b/examples/webapp/src/Pages/Base64Png/Base64PngPage.tsx
index dc745abdf9..4548b4562b 100644
--- a/examples/webapp/src/Pages/Base64Png/Base64PngPage.tsx
+++ b/examples/webapp/src/Pages/Base64Png/Base64PngPage.tsx
@@ -18,7 +18,7 @@ import { ChannelType, EnvelopeContentType } from "@kie-tools-core/editor/dist/ap
import * as React from "react";
import { EditorEnvelopeLocator, EnvelopeMapping } from "@kie-tools-core/editor/dist/api";
import { useMemo, useState } from "react";
-import { Page } from "@patternfly/react-core";
+import { Page } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedEditor } from "@kie-tools-core/editor/dist/embedded";
import { EmbeddedEditorFile } from "@kie-tools-core/editor/dist/channel";
import { Base64PngGallery } from "./Base64PngGallery";
diff --git a/examples/webapp/src/Pages/KogitoEditors/BpmnPage.tsx b/examples/webapp/src/Pages/KogitoEditors/BpmnPage.tsx
index a38503343d..d9750f9790 100644
--- a/examples/webapp/src/Pages/KogitoEditors/BpmnPage.tsx
+++ b/examples/webapp/src/Pages/KogitoEditors/BpmnPage.tsx
@@ -18,7 +18,7 @@ import { ChannelType, EnvelopeContentType } from "@kie-tools-core/editor/dist/ap
import * as React from "react";
import { EditorEnvelopeLocator, EnvelopeMapping } from "@kie-tools-core/editor/dist/api";
import { useMemo, useState } from "react";
-import { Page } from "@patternfly/react-core";
+import { Page } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedEditor, useEditorRef } from "@kie-tools-core/editor/dist/embedded";
import { EmbeddedEditorFile } from "@kie-tools-core/editor/dist/channel";
import { Sidebar } from "./Sidebar";
diff --git a/examples/webapp/src/Pages/KogitoEditors/DmnPage.tsx b/examples/webapp/src/Pages/KogitoEditors/DmnPage.tsx
index ba228d153d..0629922dd7 100644
--- a/examples/webapp/src/Pages/KogitoEditors/DmnPage.tsx
+++ b/examples/webapp/src/Pages/KogitoEditors/DmnPage.tsx
@@ -18,7 +18,7 @@ import { ChannelType, EnvelopeContentType } from "@kie-tools-core/editor/dist/ap
import * as React from "react";
import { EditorEnvelopeLocator, EnvelopeMapping } from "@kie-tools-core/editor/dist/api";
import { useMemo, useState } from "react";
-import { Page } from "@patternfly/react-core";
+import { Page } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedEditor, useEditorRef } from "@kie-tools-core/editor/dist/embedded";
import { EmbeddedEditorFile } from "@kie-tools-core/editor/dist/channel";
import { Sidebar } from "./Sidebar";
diff --git a/examples/webapp/src/Pages/KogitoEditors/Sidebar.tsx b/examples/webapp/src/Pages/KogitoEditors/Sidebar.tsx
index 601890e738..136add7b14 100644
--- a/examples/webapp/src/Pages/KogitoEditors/Sidebar.tsx
+++ b/examples/webapp/src/Pages/KogitoEditors/Sidebar.tsx
@@ -17,7 +17,8 @@
import * as React from "react";
import { EditorEnvelopeLocator } from "@kie-tools-core/editor/dist/api";
import { useCallback, useRef, useState } from "react";
-import { Nav, NavItem, NavList, TextInput } from "@patternfly/react-core";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
import { EmbeddedEditorRef, useDirtyState } from "@kie-tools-core/editor/dist/embedded";
import { EmbeddedEditorFile } from "@kie-tools-core/editor/dist/channel";
diff --git a/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularDivViewsPage.tsx b/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularDivViewsPage.tsx
index bee3e610af..270a1a09e9 100644
--- a/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularDivViewsPage.tsx
+++ b/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularDivViewsPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useMemo, useRef } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedDivPingPong } from "@kie-tools-examples/ping-pong-view/dist/embedded";
import { StatsSidebar } from "../StatsSidebar";
import { pingPongEnvelopViewRenderDiv } from "@kie-tools-examples/ping-pong-view-angular/dist/wc/lib";
diff --git a/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularIFrameViewsPage.tsx b/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularIFrameViewsPage.tsx
index 46f4d709b1..79081b24f5 100644
--- a/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularIFrameViewsPage.tsx
+++ b/examples/webapp/src/Pages/PingPong/Angular/PingPongAngularIFrameViewsPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useMemo, useRef } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedIFramePingPong } from "@kie-tools-examples/ping-pong-view/dist/embedded";
import { PingPongApi } from "@kie-tools-examples/ping-pong-view/dist/api";
import { StatsSidebar } from "../StatsSidebar";
diff --git a/examples/webapp/src/Pages/PingPong/Mixed/PingPongMixedViewsPage.tsx b/examples/webapp/src/Pages/PingPong/Mixed/PingPongMixedViewsPage.tsx
index 4a4ce9b9e5..8e8a40937b 100644
--- a/examples/webapp/src/Pages/PingPong/Mixed/PingPongMixedViewsPage.tsx
+++ b/examples/webapp/src/Pages/PingPong/Mixed/PingPongMixedViewsPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useMemo, useRef } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedDivPingPong, EmbeddedIFramePingPong } from "@kie-tools-examples/ping-pong-view/dist/embedded";
import { PingPongApi } from "@kie-tools-examples/ping-pong-view/dist/api";
import { StatsSidebar } from "../StatsSidebar";
diff --git a/examples/webapp/src/Pages/PingPong/React/PingPongReactDivViewsPage.tsx b/examples/webapp/src/Pages/PingPong/React/PingPongReactDivViewsPage.tsx
index c57701d3e6..26e2de395b 100644
--- a/examples/webapp/src/Pages/PingPong/React/PingPongReactDivViewsPage.tsx
+++ b/examples/webapp/src/Pages/PingPong/React/PingPongReactDivViewsPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useMemo, useRef } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedDivPingPong } from "@kie-tools-examples/ping-pong-view/dist/embedded";
import { PingPongApi } from "@kie-tools-examples/ping-pong-view/dist/api";
import { pingPongEnvelopViewRenderDiv } from "@kie-tools-examples/ping-pong-view-react";
diff --git a/examples/webapp/src/Pages/PingPong/React/PingPongReactIFrameViewsPage.tsx b/examples/webapp/src/Pages/PingPong/React/PingPongReactIFrameViewsPage.tsx
index 172377dd90..e87b24be16 100644
--- a/examples/webapp/src/Pages/PingPong/React/PingPongReactIFrameViewsPage.tsx
+++ b/examples/webapp/src/Pages/PingPong/React/PingPongReactIFrameViewsPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useMemo, useRef } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import { EmbeddedIFramePingPong } from "@kie-tools-examples/ping-pong-view/dist/embedded";
import { PingPongApi } from "@kie-tools-examples/ping-pong-view/dist/api";
import { StatsSidebar } from "../StatsSidebar";
diff --git a/examples/webapp/src/Pages/PingPong/StatsSidebar.tsx b/examples/webapp/src/Pages/PingPong/StatsSidebar.tsx
index faf0d7c3eb..9bb3db22c7 100644
--- a/examples/webapp/src/Pages/PingPong/StatsSidebar.tsx
+++ b/examples/webapp/src/Pages/PingPong/StatsSidebar.tsx
@@ -16,7 +16,9 @@
import * as React from "react";
import { useState, useEffect } from "react";
-import { Label, Nav, NavItem, NavList, Title } from "@patternfly/react-core";
+import { Label } from "@patternfly/react-core/dist/js/components/Label";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
interface Props {
lastPing: string;
diff --git a/examples/webapp/src/Pages/StandaloneEditors/DmnStandaloneEditorPage.tsx b/examples/webapp/src/Pages/StandaloneEditors/DmnStandaloneEditorPage.tsx
index 346057e875..ee3530897b 100644
--- a/examples/webapp/src/Pages/StandaloneEditors/DmnStandaloneEditorPage.tsx
+++ b/examples/webapp/src/Pages/StandaloneEditors/DmnStandaloneEditorPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useEffect, useRef } from "react";
-import { Page } from "@patternfly/react-core";
+import { Page } from "@patternfly/react-core/dist/js/components/Page";
import * as DmnEditor from "@kie-tools/kie-editors-standalone/dist/dmn";
export function DmnStandaloneEditorPage() {
diff --git a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneDiagramOnlyEditorPage.tsx b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneDiagramOnlyEditorPage.tsx
index 2fad41e470..67412f63c0 100644
--- a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneDiagramOnlyEditorPage.tsx
+++ b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneDiagramOnlyEditorPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useEffect, useRef, useState, useCallback } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import * as SwfEditor from "@kie-tools/serverless-workflow-standalone-editor/dist/swf";
import { ServerlessWorkflowEmptyState } from "./SwfEditorEmptyState";
import { StandaloneEditorApi } from "@kie-tools/serverless-workflow-standalone-editor/dist/common/Editor";
diff --git a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneEditorPage.tsx b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneEditorPage.tsx
index 856491dfd2..13ea211147 100644
--- a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneEditorPage.tsx
+++ b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneEditorPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useEffect, useRef, useState, useCallback } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import * as SwfEditor from "@kie-tools/serverless-workflow-standalone-editor/dist/swf";
import { ServerlessWorkflowEmptyState } from "./SwfEditorEmptyState";
import {
diff --git a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneTextOnlyEditorPage.tsx b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneTextOnlyEditorPage.tsx
index 9d579c69e0..773bd86f94 100644
--- a/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneTextOnlyEditorPage.tsx
+++ b/examples/webapp/src/Pages/StandaloneEditors/SwfStandaloneTextOnlyEditorPage.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useEffect, useRef, useState, useCallback } from "react";
-import { Page, PageSection } from "@patternfly/react-core";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
import * as SwfEditor from "@kie-tools/serverless-workflow-standalone-editor/dist/swf";
import { ServerlessWorkflowEmptyState } from "./SwfEditorEmptyState";
import {
diff --git a/examples/webapp/src/Pages/TodoList/TodoListViewPage.tsx b/examples/webapp/src/Pages/TodoList/TodoListViewPage.tsx
index d625f7d32b..8d23304ead 100644
--- a/examples/webapp/src/Pages/TodoList/TodoListViewPage.tsx
+++ b/examples/webapp/src/Pages/TodoList/TodoListViewPage.tsx
@@ -16,7 +16,9 @@
import * as React from "react";
import { useCallback, useMemo, useRef, useState } from "react";
-import { Nav, NavItem, NavList, Page, PageSection, Title } from "@patternfly/react-core";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
import { EmbeddedTodoList, EmbeddedTodoListRef } from "@kie-tools-examples/todo-list-view/dist/embedded";
import { useStateAsSharedValue } from "@kie-tools-core/envelope-bus/dist/hooks";
diff --git a/packages/boxed-expression-component/showcase/index.tsx b/packages/boxed-expression-component/showcase/index.tsx
index c4dd801878..c11fcebdbb 100644
--- a/packages/boxed-expression-component/showcase/index.tsx
+++ b/packages/boxed-expression-component/showcase/index.tsx
@@ -32,7 +32,8 @@ import {
RelationProps,
} from "../src/api";
import { BoxedExpressionEditor } from "../src/components";
-import { Button, Modal } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import { Modal } from "@patternfly/react-core/dist/js/components/Modal";
import { PenIcon } from "@patternfly/react-icons/dist/js/icons/pen-icon";
import "../src/components/BoxedExpressionEditor/base-no-reset-wrapped.css";
import ReactJson from "react-json-view";
diff --git a/packages/boxed-expression-component/src/components/DecisionTableExpression/HitPolicySelector.tsx b/packages/boxed-expression-component/src/components/DecisionTableExpression/HitPolicySelector.tsx
index 9eeefa5f13..960032b464 100644
--- a/packages/boxed-expression-component/src/components/DecisionTableExpression/HitPolicySelector.tsx
+++ b/packages/boxed-expression-component/src/components/DecisionTableExpression/HitPolicySelector.tsx
@@ -19,7 +19,7 @@ import { BuiltinAggregation, getEnumKeyByEnumValue, HitPolicy } from "../../api"
import * as React from "react";
import { useCallback, useState } from "react";
import { PopoverMenu } from "../PopoverMenu";
-import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
+import { Select, SelectOption, SelectVariant } from "@patternfly/react-core/dist/js/components/Select";
import * as _ from "lodash";
import { useBoxedExpressionEditorI18n } from "../../i18n";
import { useBoxedExpression } from "../../context";
diff --git a/packages/boxed-expression-component/src/components/EditExpressionMenu/DataTypeSelector.tsx b/packages/boxed-expression-component/src/components/EditExpressionMenu/DataTypeSelector.tsx
index 4ede8db56b..f237a194b7 100644
--- a/packages/boxed-expression-component/src/components/EditExpressionMenu/DataTypeSelector.tsx
+++ b/packages/boxed-expression-component/src/components/EditExpressionMenu/DataTypeSelector.tsx
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import { Divider, Select, SelectGroup, SelectOption, SelectVariant } from "@patternfly/react-core";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { Select, SelectGroup, SelectOption, SelectVariant } from "@patternfly/react-core/dist/js/components/Select";
import * as React from "react";
import { useCallback, useState, useRef, useEffect } from "react";
import { useBoxedExpressionEditorI18n } from "../../i18n";
diff --git a/packages/boxed-expression-component/src/components/EditExpressionMenu/EditExpressionMenu.tsx b/packages/boxed-expression-component/src/components/EditExpressionMenu/EditExpressionMenu.tsx
index ce01ea8a7e..3f073718fc 100644
--- a/packages/boxed-expression-component/src/components/EditExpressionMenu/EditExpressionMenu.tsx
+++ b/packages/boxed-expression-component/src/components/EditExpressionMenu/EditExpressionMenu.tsx
@@ -23,7 +23,7 @@ import { DataType, ExpressionProps } from "../../api";
import { useBoxedExpression } from "../../context";
import { DataTypeSelector } from "./DataTypeSelector";
import { CogIcon } from "@patternfly/react-icons/dist/js/icons/cog-icon";
-import { Button } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { NavigationKeysUtils } from "../common";
export interface EditExpressionMenuProps {
diff --git a/packages/boxed-expression-component/src/components/FunctionExpression/EditParameters.tsx b/packages/boxed-expression-component/src/components/FunctionExpression/EditParameters.tsx
index 3cad24c345..9afc481ebf 100644
--- a/packages/boxed-expression-component/src/components/FunctionExpression/EditParameters.tsx
+++ b/packages/boxed-expression-component/src/components/FunctionExpression/EditParameters.tsx
@@ -17,7 +17,7 @@
import "./EditParameters.css";
import * as _ from "lodash";
import { DataTypeSelector } from "../EditExpressionMenu";
-import { Button } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { OutlinedTrashAltIcon } from "@patternfly/react-icons/dist/js/icons/outlined-trash-alt-icon";
import * as React from "react";
import { ChangeEvent, useCallback } from "react";
diff --git a/packages/boxed-expression-component/src/components/FunctionExpression/FunctionKindSelector.tsx b/packages/boxed-expression-component/src/components/FunctionExpression/FunctionKindSelector.tsx
index ca42bf5123..b1febab4ba 100644
--- a/packages/boxed-expression-component/src/components/FunctionExpression/FunctionKindSelector.tsx
+++ b/packages/boxed-expression-component/src/components/FunctionExpression/FunctionKindSelector.tsx
@@ -15,7 +15,7 @@
*/
import { PopoverMenu } from "../PopoverMenu";
-import { Menu, MenuItem, MenuList } from "@patternfly/react-core";
+import { Menu, MenuItem, MenuList } from "@patternfly/react-core/dist/js/components/Menu";
import * as _ from "lodash";
import * as React from "react";
import { useCallback } from "react";
diff --git a/packages/boxed-expression-component/src/components/LiteralExpression/PMMLLiteralExpression.tsx b/packages/boxed-expression-component/src/components/LiteralExpression/PMMLLiteralExpression.tsx
index 35cfd1365d..972acd3517 100644
--- a/packages/boxed-expression-component/src/components/LiteralExpression/PMMLLiteralExpression.tsx
+++ b/packages/boxed-expression-component/src/components/LiteralExpression/PMMLLiteralExpression.tsx
@@ -18,7 +18,7 @@ import "./PMMLLiteralExpression.css";
import * as React from "react";
import { useCallback, useRef, useState } from "react";
import { PMMLLiteralExpressionProps } from "../../api";
-import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
+import { Select, SelectOption, SelectVariant } from "@patternfly/react-core/dist/js/components/Select";
import * as _ from "lodash";
import { useBoxedExpression } from "../../context";
diff --git a/packages/boxed-expression-component/src/components/LogicTypeSelector/LogicTypeSelector.tsx b/packages/boxed-expression-component/src/components/LogicTypeSelector/LogicTypeSelector.tsx
index 3e8345c410..529b7b12c6 100644
--- a/packages/boxed-expression-component/src/components/LogicTypeSelector/LogicTypeSelector.tsx
+++ b/packages/boxed-expression-component/src/components/LogicTypeSelector/LogicTypeSelector.tsx
@@ -37,7 +37,7 @@ import { RelationExpression } from "../RelationExpression";
import { ContextExpression } from "../ContextExpression";
import { useBoxedExpressionEditorI18n } from "../../i18n";
import { PopoverMenu } from "../PopoverMenu";
-import { Menu, MenuGroup, MenuItem, MenuList } from "@patternfly/react-core";
+import { Menu, MenuGroup, MenuItem, MenuList } from "@patternfly/react-core/dist/js/components/Menu";
import * as _ from "lodash";
import { useContextMenuHandler } from "../../hooks";
import { useBoxedExpression } from "../../context";
diff --git a/packages/boxed-expression-component/src/components/PopoverMenu/PopoverMenu.tsx b/packages/boxed-expression-component/src/components/PopoverMenu/PopoverMenu.tsx
index 074075b9d0..4aa5a40def 100644
--- a/packages/boxed-expression-component/src/components/PopoverMenu/PopoverMenu.tsx
+++ b/packages/boxed-expression-component/src/components/PopoverMenu/PopoverMenu.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useCallback, useState, useImperativeHandle } from "react";
-import { Popover } from "@patternfly/react-core";
+import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
import "./PopoverMenu.css";
import { useBoxedExpression } from "../../context";
import { NavigationKeysUtils } from "../common";
diff --git a/packages/boxed-expression-component/src/components/Table/TableHandler.tsx b/packages/boxed-expression-component/src/components/Table/TableHandler.tsx
index 2323044cd8..bfb6033f74 100644
--- a/packages/boxed-expression-component/src/components/Table/TableHandler.tsx
+++ b/packages/boxed-expression-component/src/components/Table/TableHandler.tsx
@@ -19,7 +19,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { DataType, generateUuid, TableHandlerConfiguration, TableOperation } from "../../api";
import * as _ from "lodash";
import { Column, ColumnInstance, DataRecord } from "react-table";
-import { Popover } from "@patternfly/react-core";
+import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
import { TableHandlerMenu } from "./TableHandlerMenu";
import { useBoxedExpression } from "../../context";
import { getColumnsAtLastLevel, getColumnSearchPredicate } from "./Table";
diff --git a/packages/boxed-expression-component/src/components/Table/TableHandlerMenu.tsx b/packages/boxed-expression-component/src/components/Table/TableHandlerMenu.tsx
index d723c27f9f..93066a7b13 100644
--- a/packages/boxed-expression-component/src/components/Table/TableHandlerMenu.tsx
+++ b/packages/boxed-expression-component/src/components/Table/TableHandlerMenu.tsx
@@ -16,7 +16,7 @@
import "./TableHandlerMenu.css";
import * as React from "react";
-import { Menu, MenuGroup, MenuItem, MenuList } from "@patternfly/react-core";
+import { Menu, MenuGroup, MenuItem, MenuList } from "@patternfly/react-core/dist/js/components/Menu";
import { AllowedOperations, GroupOperations, TableOperation } from "../../api";
import * as _ from "lodash";
diff --git a/packages/dashbuilder-component-table/src/FilteredTable.tsx b/packages/dashbuilder-component-table/src/FilteredTable.tsx
index 58f4387958..e23e0c3315 100644
--- a/packages/dashbuilder-component-table/src/FilteredTable.tsx
+++ b/packages/dashbuilder-component-table/src/FilteredTable.tsx
@@ -17,7 +17,9 @@ import * as React from "react";
import { TableComposable, Thead, Tbody, Tr, Th, Td, selectable } from "@patternfly/react-table";
import { useState, useCallback, useMemo } from "react";
-import { Flex, FlexItem, Pagination, SearchInput } from "@patternfly/react-core";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Pagination } from "@patternfly/react-core/dist/js/components/Pagination";
+import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 10;
diff --git a/packages/dashbuilder-component-uniforms/src/UniformsComponent.tsx b/packages/dashbuilder-component-uniforms/src/UniformsComponent.tsx
index 1b61877f5a..2be8ec2b90 100644
--- a/packages/dashbuilder-component-uniforms/src/UniformsComponent.tsx
+++ b/packages/dashbuilder-component-uniforms/src/UniformsComponent.tsx
@@ -18,7 +18,7 @@ import * as React from "react";
import { ComponentController, DataSet } from "@kie-tools/dashbuilder-component-api";
import { useState, useEffect, useCallback } from "react";
import { Uniforms } from "./Uniforms";
-import { Alert, AlertActionCloseButton } from "@patternfly/react-core";
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert";
interface Props {
controller: ComponentController;
diff --git a/packages/dashbuilder-component-victory-charts/src/VictoryChartComponent.tsx b/packages/dashbuilder-component-victory-charts/src/VictoryChartComponent.tsx
index 2ef494c596..0900c2b300 100644
--- a/packages/dashbuilder-component-victory-charts/src/VictoryChartComponent.tsx
+++ b/packages/dashbuilder-component-victory-charts/src/VictoryChartComponent.tsx
@@ -18,7 +18,7 @@ import * as React from "react";
import { ComponentController, DataSet } from "@kie-tools/dashbuilder-component-api";
import { useState, useEffect } from "react";
import { VictoryChart, VictoryChartProps } from "./VictoryChart";
-import { Alert } from "@patternfly/react-core";
+import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
import { ChartType, ThemeColorType, ThemeVariantType } from "./charts/BaseChart";
interface Props {
diff --git a/packages/dashbuilder-component-victory-charts/webpack.config.js b/packages/dashbuilder-component-victory-charts/webpack.config.js
index ab9cacec08..5862b64d41 100644
--- a/packages/dashbuilder-component-victory-charts/webpack.config.js
+++ b/packages/dashbuilder-component-victory-charts/webpack.config.js
@@ -19,6 +19,7 @@ const patternflyBase = require("@kie-tools-core/patternfly-base");
const { merge } = require("webpack-merge");
const common = require("@kie-tools-core/webpack-base/webpack.common.config");
const HtmlWebpackPlugin = require("html-webpack-plugin");
+const CopyPlugin = require("copy-webpack-plugin");
module.exports = async (env) => {
return merge(common(env), {
@@ -30,6 +31,9 @@ module.exports = async (env) => {
template: "./static/index.html",
minify: false,
}),
+ new CopyPlugin({
+ patterns: [{ from: path.resolve(__dirname, "./static/index.css"), to: "." }],
+ }),
],
module: {
diff --git a/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/external/ExternalDataSetClientProvider.java b/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/external/ExternalDataSetClientProvider.java
index de98a704cc..29f4dab87e 100644
--- a/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/external/ExternalDataSetClientProvider.java
+++ b/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/external/ExternalDataSetClientProvider.java
@@ -162,13 +162,32 @@ public class ExternalDataSetClientProvider {
if (response.status == HttpResponseCodes.SC_OK) {
return register(def, callback, responseText, mimeType);
} else {
- return notAbleToRetrieveDataSet(def, callback);
+ var exception = buildExceptionForResponse(responseText, response);
+ return notAbleToRetrieveDataSet(def, callback, exception);
}
}, error -> notAbleToRetrieveDataSet(def, callback));
- }).catch_(e -> notAbleToRetrieveDataSet(def, callback));
+ }).catch_(e -> notAbleToRetrieveDataSet(def, callback,
+ new RuntimeException("Request not started, make sure that CORS is enabled. Message: " + e)
+ ));
}
+
+
+ private Throwable buildExceptionForResponse(String responseText, Response response) {
+ var sb = new StringBuffer("The dataset URL is unreachable with status ");
+ sb.append(response.status);
+ sb.append(" - ");
+ sb.append(response.statusText);
+
+ if (responseText != null && !responseText.trim().isEmpty()) {
+ sb.append("\n");
+ sb.append("Response Text: ");
+ sb.append(responseText);
+ }
+ return new RuntimeException(sb.toString());
+ }
+
private IThenable<Object> register(ExternalDataSetDef def,
final DataSetReadyCallback callback,
final String responseText,
diff --git a/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/services/RuntimeDataSetClientServices.java b/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/services/RuntimeDataSetClientServices.java
index b6c9843083..d9ba619fba 100644
--- a/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/services/RuntimeDataSetClientServices.java
+++ b/packages/dashbuilder/dashbuilder-runtime-parent/dashbuilder-runtime-client/src/main/java/org/dashbuilder/client/services/RuntimeDataSetClientServices.java
@@ -151,7 +151,7 @@ public class RuntimeDataSetClientServices implements DataSetClientServices {
@Override
public boolean onError(ClientRuntimeError error) {
- if (loader.isEditor()) {
+ if (loader.isEditor() || loader.isClient() ) {
listener.onError(error);
} else {
DomGlobal.console.debug("Error retrieving dataset from client, trying from backend");
diff --git a/packages/dashbuilder/dashbuilder-shared/dashbuilder-displayer-api/src/main/java/org/dashbuilder/displayer/DisplayerSettings.java b/packages/dashbuilder/dashbuilder-shared/dashbuilder-displayer-api/src/main/java/org/dashbuilder/displayer/DisplayerSettings.java
index acfcf33299..674a150ca0 100644
--- a/packages/dashbuilder/dashbuilder-shared/dashbuilder-displayer-api/src/main/java/org/dashbuilder/displayer/DisplayerSettings.java
+++ b/packages/dashbuilder/dashbuilder-shared/dashbuilder-displayer-api/src/main/java/org/dashbuilder/displayer/DisplayerSettings.java
@@ -379,7 +379,7 @@ public class DisplayerSettings {
}
public boolean isFilterEnabled() {
- return parseBoolean(settings.get(getSettingPath(DisplayerAttributeDef.FILTER_ENABLED)));
+ return parseBoolean(settings.get(getSettingPath(DisplayerAttributeDef.FILTER_ENABLED))) || isFilterListeningEnabled() || isFilterNotificationEnabled();
}
public void setFilterEnabled(boolean filterEnabled) {
diff --git a/packages/dashbuilder/kie-soup-dataset/kie-soup-dataset-api/src/main/java/org/dashbuilder/dataset/json/ExternalDataSetJSONParser.java b/packages/dashbuilder/kie-soup-dataset/kie-soup-dataset-api/src/main/java/org/dashbuilder/dataset/json/ExternalDataSetJSONParser.java
index 8784fa6935..c2468679ca 100644
--- a/packages/dashbuilder/kie-soup-dataset/kie-soup-dataset-api/src/main/java/org/dashbuilder/dataset/json/ExternalDataSetJSONParser.java
+++ b/packages/dashbuilder/kie-soup-dataset/kie-soup-dataset-api/src/main/java/org/dashbuilder/dataset/json/ExternalDataSetJSONParser.java
@@ -46,14 +46,13 @@ public class ExternalDataSetJSONParser {
private static final String COLUMN_PREFIX = "Column ";
private static final ColumnType DEFAULT_COLUMN_TYPE = ColumnType.LABEL;
-
-
+
private Function<String, Date> dateParser;
-
+
public ExternalDataSetJSONParser() {
this(s -> null);
- }
-
+ }
+
public ExternalDataSetJSONParser(Function<String, Date> dateParser) {
super();
this.dateParser = dateParser;
@@ -107,9 +106,13 @@ public class ExternalDataSetJSONParser {
}
} else if (trimedJson.startsWith(ARRAY_START_TOKEN)) {
- JsonArray dataSetArray = Json.instance().parse(json);
- fillDataSetColumns(dataSet, dataSetArray);
- addValues(dataSet, dataSetArray);
+ try {
+ JsonArray dataSetArray = Json.instance().parse(json);
+ fillDataSetColumns(dataSet, dataSetArray);
+ addValues(dataSet, dataSetArray);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("DataSet JSON is invalid. Please check that the data is in correct format.", e);
+ }
}
}
return dataSet;
@@ -215,7 +218,7 @@ public class ExternalDataSetJSONParser {
} catch (NumberFormatException e) {
// empty
}
-
+
try {
convertToDate(value);
return ColumnType.DATE;
@@ -225,7 +228,7 @@ public class ExternalDataSetJSONParser {
return DEFAULT_COLUMN_TYPE;
}
-
+
protected Date convertToDate(String value) {
return dateParser.apply(value);
}
diff --git a/packages/eslint/.eslintrc.js b/packages/eslint/.eslintrc.js
index 18bf908273..ec04717598 100644
--- a/packages/eslint/.eslintrc.js
+++ b/packages/eslint/.eslintrc.js
@@ -65,5 +65,26 @@ module.exports = {
amd: true,
},
},
+ {
+ files: ["*.ts", "*.tsx"],
+ rules: {
+ "no-restricted-imports": [
+ "error",
+ {
+ paths: [
+ {
+ name: "@patternfly/react-core",
+ message: "Please use specific imports from @patternfly/react-core/dist/js/**)",
+ },
+ {
+ name: "@patternfly/react-icons",
+ message: "Please use specific imports from @patternfly/react-icons/dist/js/**)",
+ },
+ ],
+ patterns: ["!@patternfly/react-core/dist/*", "!@patternfly/react-icons/dist/*"],
+ },
+ ],
+ },
+ },
],
};
diff --git a/packages/i18n-common-dictionary/src/CommonI18n.ts b/packages/i18n-common-dictionary/src/CommonI18n.ts
index cf239eaf62..c9ae016c2e 100644
--- a/packages/i18n-common-dictionary/src/CommonI18n.ts
+++ b/packages/i18n-common-dictionary/src/CommonI18n.ts
@@ -18,6 +18,7 @@ import { ReferenceDictionary } from "@kie-tools-core/i18n/dist/core";
import { names } from "./names";
export type CommonDictionary = {
+ apply: string;
available: string;
back: string;
cancel: string;
diff --git a/packages/i18n-common-dictionary/src/locales/en.ts b/packages/i18n-common-dictionary/src/locales/en.ts
index afd397b07c..e18efe1bb9 100644
--- a/packages/i18n-common-dictionary/src/locales/en.ts
+++ b/packages/i18n-common-dictionary/src/locales/en.ts
@@ -20,6 +20,7 @@ import { names } from "../names";
export const en: CommonI18n = {
names,
terms: {
+ apply: "Apply",
available: "Available",
back: "Back",
cancel: "Cancel",
diff --git a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizard.tsx b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizard.tsx
index 0d18a18cd5..a4069e741e 100644
--- a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizard.tsx
+++ b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizard.tsx
@@ -25,7 +25,10 @@ import { JavaField } from "./model/JavaField";
import { DMNSimpleType } from "./model/DMNSimpleType";
import { getJavaClassSimpleName } from "./model/JavaClassUtils";
import { GWTLayerService, JavaCodeCompletionService } from "./services";
-import { Button, Modal, ModalVariant, Tooltip, Wizard } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Wizard } from "@patternfly/react-core/dist/js/components/Wizard";
export interface ImportJavaClassesWizardProps {
/** Service class which contains all API methods to dialog with GWT layer */
diff --git a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFieldListTable.tsx b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFieldListTable.tsx
index 4a5a439cef..08ec524d41 100644
--- a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFieldListTable.tsx
+++ b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFieldListTable.tsx
@@ -18,7 +18,7 @@ import * as React from "react";
import "./ImportJavaClassesWizardFieldListTable.css";
import { ExpandableRowContent, TableComposable, Tbody, Td, Tr } from "@patternfly/react-table";
import { JavaClass } from "./model/JavaClass";
-import { Button } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { JavaField } from "./model/JavaField";
import { DMNSimpleType, JAVA_TO_DMN_MAP } from "./model/DMNSimpleType";
import { getJavaClassSimpleName } from "./model/JavaClassUtils";
diff --git a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFirstStep.tsx b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFirstStep.tsx
index 9a5b8294e5..10d60ff452 100644
--- a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFirstStep.tsx
+++ b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardFirstStep.tsx
@@ -16,21 +16,19 @@
import * as React from "react";
import "./ImportJavaClassesWizardFirstStep.css";
+import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye";
import {
- Bullseye,
DataList,
DataListCell,
DataListCheck,
DataListItem,
DataListItemRow,
- EmptyState,
- EmptyStateBody,
- EmptyStateIcon,
- SearchInput,
- Spinner,
- Tooltip,
- Title,
-} from "@patternfly/react-core";
+} from "@patternfly/react-core/dist/js/components/DataList";
+import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState";
+import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
+import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
import CubesIcon from "@patternfly/react-icons/dist/js/icons/cubes-icon";
import { useImportJavaClassesWizardI18n } from "../../i18n";
import { useCallback, useState } from "react";
diff --git a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardSecondStep.tsx b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardSecondStep.tsx
index f4539736da..c39bf6ac44 100644
--- a/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardSecondStep.tsx
+++ b/packages/import-java-classes-component/src/components/ImportJavaClasses/ImportJavaClassesWizardSecondStep.tsx
@@ -15,7 +15,8 @@
*/
import * as React from "react";
-import { Bullseye, Spinner } from "@patternfly/react-core";
+import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye";
+import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
import { useCallback, useEffect } from "react";
import { ImportJavaClassesWizardFieldListTable } from "./ImportJavaClassesWizardFieldListTable";
import { JavaField } from "./model/JavaField";
diff --git a/packages/kie-sandbox-image/README.md b/packages/kie-sandbox-image/README.md
index e281919b19..5749026936 100644
--- a/packages/kie-sandbox-image/README.md
+++ b/packages/kie-sandbox-image/README.md
@@ -54,13 +54,14 @@ This package contains the `Containerfile/Dockerfile` and scripts to build a cont
[comment]: <> (//TODO: Use EnvJson.schema.json to generate this documentation somehow.. See https://github.com/kiegroup/kie-issues/issues/16)
- | Name | Description | Default |
- | :--------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------: |
- | `KIE_SANDBOX_EXTENDED_SERVICES_URL` | The URL that points to the KIE Sandbox Extended Services. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
- | `KIE_SANDBOX_GIT_CORS_PROXY_URL` | The URL that points to the Git CORS proxy for interacting with Git providers. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
- | `KIE_SANDBOX_REQUIRE_CUSTOM_COMMIT_MESSAGE` | Require users to type a custom commit message when creating a new commit. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
- | `KIE_SANDBOX_CUSTOM_COMMIT_MESSAGE_VALIDATION_SERVICE_URL` | Service URL to validate commit messages. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
- | `KIE_SANDBOX_AUTH_PROVIDERS` | Authentication providers configuration. Used to enable integration with GitHub Enterprise Server instances and more. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | Name | Description | Default |
+ | :---------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------: |
+ | `KIE_SANDBOX_EXTENDED_SERVICES_URL` | The URL that points to the KIE Sandbox Extended Services. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | `KIE_SANDBOX_GIT_CORS_PROXY_URL` | The URL that points to the Git CORS proxy for interacting with Git providers. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | `KIE_SANDBOX_REQUIRE_CUSTOM_COMMIT_MESSAGES` | Require users to type a custom commit message when creating a new commit. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | `KIE_SANDBOX_CUSTOM_COMMIT_MESSAGES_VALIDATION_SERVICE_URL` | Service URL to validate commit messages. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | `KIE_SANDBOX_AUTH_PROVIDERS` | Authentication providers configuration. Used to enable integration with GitHub Enterprise Server instances and more. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
+ | `KIE_SANDBOX_ACCELERATORS` | Accelerators configuration. Used to add a template to a set of Decisions and Workflows, making it buildable and deployable. | See [ defaultEnvJson.ts ](../online-editor/build/defaultEnvJson.ts) |
### Examples
@@ -73,7 +74,15 @@ This package contains the `Containerfile/Dockerfile` and scripts to build a cont
2. Enabling authentication with a GitHub Enterprise Server instance.
```bash
- podman run -t -p 8080:8080 -e KIE_SANDBOX_AUTH_PROVIDERS='[{"id":"github_at_my_company","domain":"github.my-company.com","supportedGitRemoteDomains":["github.my-company.com","gist.github.my-company.com"],"type":"github","name":"GitHub @ MyCompany","enabled":true, "group":"git" }]' -i --rm quay.io/kie-tools/kie-sandbox-image:latest
+ podman run -t -p 8080:8080 -e KIE_SANDBOX_AUTH_PROVIDERS='[{
+ "id":"github_at_my_company", \
+ "domain":"github.my-company.com", \
+ "supportedGitRemoteDomains":["github.my-company.com","gist.github.my-company.com"], \
+ "type":"github", \
+ "name":"GitHub @ MyCompany", \
+ "enabled":true, \
+ "group":"git" \
+ }]' -i --rm quay.io/kie-tools/kie-sandbox-image:latest
```
3. Requiring users to input a custom commit message on every commit.
@@ -88,6 +97,20 @@ This package contains the `Containerfile/Dockerfile` and scripts to build a cont
podman run -t -p 8080:8080 -e KIE_SANDBOX_REQUIRE_CUSTOM_COMMIT_MESSAGE='true' KIE_SANDBOX_CUSTOM_COMMIT_MESSAGE_VALIDATION_SERVICE_URL='http://localhost:8090/validate' -i --rm quay.io/kie-tools/kie-sandbox-image:latest
```
+ 5. Adding Accelerators available for your users.
+
+ ```bash
+ podman run -t -p 8080:8080 -e KIE_SANDBOX_ACCELERATORS='[{ \
+ name: "Quarkus", \
+ iconUrl: "https://github.com/kiegroup/kie-sandbox-quarkus-accelerator/raw/0.0.0/quarkus-logo.png", \
+ gitRepositoryUrl: "https://github.com/kiegroup/kie-sandbox-quarkus-accelerator", \
+ gitRepositoryGitRef: "0.0.0", \
+ dmnDestinationFolder: "src/main/resources/dmn", \
+ bpmnDestinationFolder: "src/main/resources/bpmn", \
+ otherFilesDestinationFolder: "src/main/resources/others", \
+ }]' -i --rm quay.io/kie-tools/kie-sandbox-image:latest
+ ```
+
2. Write a custom `Containerfile/Dockerfile` from the image:
```docker
@@ -98,6 +121,7 @@ This package contains the `Containerfile/Dockerfile` and scripts to build a cont
ENV KIE_SANDBOX_REQUIRE_CUSTOM_COMMIT_MESSAGE=<my_value>
ENV KIE_SANDBOX_CUSTOM_COMMIT_MESSAGE_VALIDATION_SERVICE_URL=<my_value>
ENV KIE_SANDBOX_AUTH_PROVIDERS=<my_value>
+ ENV KIE_SANDBOX_ACCELERATORS=<my_value>
```
3. Create the application from the image in OpenShift and set the deployment environment variable right from the OpenShift UI.
@@ -168,3 +192,48 @@ Content-Type: application/json
- #### HTTP Status different from 200
If the service responds with an HTTP code other than 200, an error message is displayed alongside the HTTP Code + the response body of the request in full.
+
+### Accelerators
+
+Accelerators are Git repositories that contain a skeleton of an application and will convert a working directory with your .dmn and .bpmn files into a fully functional application that can be built and deployed.
+
+After creating yours you must define where resources should be placed inside these repositories. For example, `.dmn` files should be placed inside `src/main/resources` for a Quarkus application. As a bonus, adding an image/logo can be used to better represent your Accelerator wherever it's listed.
+
+#### The Accelerator configuration
+
+Having all of that, it's time to create the configuration required to add it to the **KIE_SANDBOX_ACCELERATORS** list env var.
+It looks like this:
+
+```js
+{
+ name: "Your Accelerator name",
+ iconUrl: "https://link.to/your/logo/image",
+ gitRepositoryUrl: "https://github.com/...",
+ gitRepositoryGitRef: "branchName",
+ dmnDestinationFolder: "path/to/place/dmn/files",
+ bpmnDestinationFolder: "path/to/place/bpmn/files",
+ otherFilesDestinationFolder: "path/to/place/other/files",
+}
+```
+
+- **name**: This is how the Accelerator will be known inside KIE Sandbox.
+- **iconUrl**: An optional parameter to add an image/logo besides you Accelerator name.
+- **gitRepositoryUrl**: This is where your Accelerator is hosted. Should be an URL that can be used with `git clone`.
+- **gitRepositoryGitRef**: Where in your repository is this Accelerator located. Could be a branch, commit, tag, anything that can be used with `git checkout`.
+- **dmnDestinationFolder**: Where your DMN and PMML files will be moved to after applying the Accelerator.
+- **bpmnDestinationFolder**: Where your BPMN files will be moved to after applying the Accelerator.
+- **otherFilesDestinationFolder**: Where other files will be moved to after applying the Accelerator.
+
+Here's an example of what it should look like:
+
+```js
+{
+ name: "Quarkus",
+ iconUrl: `https://github.com/kiegroup/kie-sandbox-quarkus-accelerator/raw/0.0.0/quarkus-logo.png`,
+ gitRepositoryUrl: "https://github.com/kiegroup/kie-sandbox-quarkus-accelerator",
+ gitRepositoryGitRef: "main,
+ dmnDestinationFolder: "src/main/resources/dmn",
+ bpmnDestinationFolder: "src/main/resources/bpmn",
+ otherFilesDestinationFolder: "src/main/resources/others",
+}
+```
diff --git a/packages/kn-plugin-workflow/env/index.js b/packages/kn-plugin-workflow/env/index.js
index c3430b2017..7089c702c3 100644
--- a/packages/kn-plugin-workflow/env/index.js
+++ b/packages/kn-plugin-workflow/env/index.js
@@ -31,7 +31,7 @@ module.exports = composeEnv([require("@kie-tools/root-env/env")], {
},
KN_PLUGIN_WORKFLOW__quarkusVersion: {
name: "KN_PLUGIN_WORKFLOW__quarkusVersion",
- default: "2.15.0.Final",
+ default: "2.16.0.Final",
description: "Quarkus version to be used when creating the Kogito Serverless Workflow project",
},
}),
diff --git a/packages/kn-plugin-workflow/go.mod b/packages/kn-plugin-workflow/go.mod
index c0be1728e9..96023519b4 100644
--- a/packages/kn-plugin-workflow/go.mod
+++ b/packages/kn-plugin-workflow/go.mod
@@ -25,7 +25,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect
- golang.org/x/text v0.3.7 // indirect
+ golang.org/x/text v0.3.8 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/packages/kn-plugin-workflow/go.sum b/packages/kn-plugin-workflow/go.sum
index 5fbfb1f3d7..7828ef2d5f 100644
--- a/packages/kn-plugin-workflow/go.sum
+++ b/packages/kn-plugin-workflow/go.sum
@@ -352,8 +352,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/packages/online-editor/build/defaultEnvJson.ts b/packages/online-editor/build/defaultEnvJson.ts
index 8d98aa97bc..124b397179 100644
--- a/packages/online-editor/build/defaultEnvJson.ts
+++ b/packages/online-editor/build/defaultEnvJson.ts
@@ -79,4 +79,15 @@ export const defaultEnvJson: EnvJson = {
group: AuthProviderGroup.CLOUD,
},
],
+ KIE_SANDBOX_ACCELERATORS: [
+ {
+ name: "Quarkus",
+ iconUrl: `https://github.com/kiegroup/kie-sandbox-quarkus-accelerator/raw/${buildEnv.root.version}/quarkus-logo.png`,
+ gitRepositoryUrl: "https://github.com/kiegroup/kie-sandbox-quarkus-accelerator",
+ gitRepositoryGitRef: buildEnv.root.version,
+ dmnDestinationFolder: "src/main/resources/dmn",
+ bpmnDestinationFolder: "src/main/resources/bpmn",
+ otherFilesDestinationFolder: "src/main/resources/others",
+ },
+ ],
};
diff --git a/packages/online-editor/package.json b/packages/online-editor/package.json
index 59b241bcc4..0424ec2682 100644
--- a/packages/online-editor/package.json
+++ b/packages/online-editor/package.json
@@ -60,6 +60,7 @@
"buffer": "^6.0.3",
"history": "^4.9.0",
"isomorphic-git": "^1.11.1",
+ "js-yaml": "^4.1.0",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -87,6 +88,7 @@
"@types/history": "^4.7.3",
"@types/jest": "^26.0.23",
"@types/jest-when": "^2.7.4",
+ "@types/js-yaml": "^4.0.5",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"@types/react-router": "^5.1.14",
diff --git a/packages/online-editor/src/accelerators/AcceleratorsApi.ts b/packages/online-editor/src/accelerators/AcceleratorsApi.ts
new file mode 100644
index 0000000000..22a5d50b67
--- /dev/null
+++ b/packages/online-editor/src/accelerators/AcceleratorsApi.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type AcceleratorConfig = {
+ name: string;
+ gitRepositoryUrl: string;
+ gitRepositoryGitRef: string;
+ dmnDestinationFolder: string;
+ bpmnDestinationFolder: string;
+ otherFilesDestinationFolder: string;
+ iconUrl?: string;
+};
+
+export type AcceleratorAppliedConfig = AcceleratorConfig & { appliedAt?: string };
+
+export function validateAcceleratorDestinationFolderPaths(path: string) {
+ if (path.startsWith(".")) {
+ return "Accelerator destination folder misconfigured: Please only use absolute paths.";
+ } else if (path.includes("*") || path.includes("?")) {
+ return "Accelerator destination folder misconfigured: Wildcards and patterns are not allowed.";
+ }
+ return;
+}
+
+export const KIE_SANDBOX_DIRECTORY_PATH = ".kie-sandbox";
+export const ACCELERATOR_CONFIG_FILE_NAME = "accelerator";
+export const ACCELERATOR_CONFIG_FILE_EXTENSION = "yaml";
+export const ACCELERATOR_CONFIG_FILE_RELATIVE_PATH = `${KIE_SANDBOX_DIRECTORY_PATH}/${ACCELERATOR_CONFIG_FILE_NAME}.${ACCELERATOR_CONFIG_FILE_EXTENSION}`;
diff --git a/packages/online-editor/src/accelerators/AcceleratorsHooks.tsx b/packages/online-editor/src/accelerators/AcceleratorsHooks.tsx
new file mode 100644
index 0000000000..def9232810
--- /dev/null
+++ b/packages/online-editor/src/accelerators/AcceleratorsHooks.tsx
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useMemo, useState } from "react";
+import * as yaml from "js-yaml";
+import { useEnv } from "../env/hooks/EnvContext";
+import {
+ ACCELERATOR_CONFIG_FILE_NAME,
+ ACCELERATOR_CONFIG_FILE_EXTENSION,
+ KIE_SANDBOX_DIRECTORY_PATH,
+ AcceleratorConfig,
+ validateAcceleratorDestinationFolderPaths,
+ ACCELERATOR_CONFIG_FILE_RELATIVE_PATH,
+ AcceleratorAppliedConfig,
+} from "./AcceleratorsApi";
+import { WorkspaceFile, useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { dirname, join } from "path";
+import { useHistory } from "react-router";
+import { useRoutes } from "../navigation/Hooks";
+import { useGlobalAlert } from "../alerts";
+import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
+import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
+import { useOnlineI18n } from "../i18n";
+import { useCancelableEffect } from "@kie-tools-core/react-hooks/dist/useCancelableEffect";
+import { useAuthSession } from "../authSessions/AuthSessionsContext";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { useWorkspaceFilePromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceFileHooks";
+import {
+ GIT_DEFAULT_BRANCH,
+ GIT_ORIGIN_REMOTE_NAME,
+} from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
+import { isDecision, isScorecard, isWorkflow } from "../envelopeLocator/EditorEnvelopeLocatorFactory";
+
+const TEMP_ACCELERATOR_REMOTE_NAME = "__kie-sandbox__accelerator-remote";
+const BACKUP_BRANCH_NAME = "__kie-sandbox__accelerator-backup-branch";
+const MOVED_FILES_BRANCH_NAME = "__kie-sandbox__accelerator-moved-files-branch";
+
+class ApplyAcceleratorError extends Error {
+ constructor(acceleratorName: string, message?: string) {
+ super(`Applying Accelerator ${acceleratorName}: ${message}`);
+ }
+}
+
+export function useAvailableAccelerators() {
+ const { env } = useEnv();
+ return useMemo<AcceleratorConfig[]>(() => env.KIE_SANDBOX_ACCELERATORS, [env.KIE_SANDBOX_ACCELERATORS]);
+}
+
+export function useAcceleratorsDispatch(workspace: ActiveWorkspace) {
+ const workspaces = useWorkspaces();
+ const history = useHistory();
+ const routes = useRoutes();
+ const { i18n } = useOnlineI18n();
+ const { gitConfig } = useAuthSession(workspace.descriptor.gitAuthSessionId);
+
+ const apllyingAcceleratorAlert = useGlobalAlert(
+ useCallback(
+ (_, staticArgs: { acceleratorName: string }) => (
+ <Alert
+ className={"kogito--alert"}
+ variant="info"
+ title={
+ <>
+ <Spinner size={"sm"} />
+ {i18n.accelerators.loadingAlert(staticArgs.acceleratorName)}
+ </>
+ }
+ />
+ ),
+ [i18n]
+ )
+ );
+
+ const applyAcceleratorSuccessAlert = useGlobalAlert(
+ useCallback(
+ (_, staticArgs: { acceleratorName: string }) => (
+ <Alert
+ className={"kogito--alert"}
+ variant="success"
+ title={i18n.accelerators.successAlert(staticArgs.acceleratorName)}
+ />
+ ),
+ [i18n]
+ ),
+ { durationInSeconds: 4 }
+ );
+
+ const applyAcceleratorFailAlert = useGlobalAlert(
+ useCallback(
+ (_, staticArgs: { acceleratorName: string }) => (
+ <Alert
+ className={"kogito--alert"}
+ variant="danger"
+ title={i18n.accelerators.failAlert(staticArgs.acceleratorName)}
+ />
+ ),
+ [i18n]
+ ),
+ { durationInSeconds: 4 }
+ );
+
+ const attemptToDeleteTemporaryBranches = useCallback(async () => {
+ const workspaceId = workspace.descriptor.workspaceId;
+ try {
+ await workspaces.deleteBranch({ workspaceId, ref: BACKUP_BRANCH_NAME });
+ } catch (e) {
+ // Do nothing
+ }
+ try {
+ await workspaces.deleteBranch({ workspaceId, ref: MOVED_FILES_BRANCH_NAME });
+ } catch (e) {
+ // Do nothing
+ }
+ }, [workspace.descriptor.workspaceId, workspaces]);
+
+ const applyAcceleratorToWorkspace = useCallback(
+ async (accelerator: AcceleratorConfig, currentFile: WorkspaceFile) => {
+ apllyingAcceleratorAlert.show({ acceleratorName: accelerator.name });
+
+ const workspaceId = workspace.descriptor.workspaceId;
+
+ let movedFiles: WorkspaceFile[] = [];
+
+ // Attempt to delete branches
+ // This is a workaround, as isomorphic-git doesn't necessarilly deletes the branch, even though it resolves
+ attemptToDeleteTemporaryBranches();
+
+ let configFile: WorkspaceFile | undefined = undefined;
+
+ try {
+ const destinationPathValidation = {
+ dmnDestinationFolder: validateAcceleratorDestinationFolderPaths(accelerator.dmnDestinationFolder),
+ bpmnDestinationFolder: validateAcceleratorDestinationFolderPaths(accelerator.bpmnDestinationFolder),
+ otherFilesDestinationFolder: validateAcceleratorDestinationFolderPaths(
+ accelerator.otherFilesDestinationFolder
+ ),
+ };
+
+ if (
+ destinationPathValidation.dmnDestinationFolder ||
+ destinationPathValidation.bpmnDestinationFolder ||
+ destinationPathValidation.otherFilesDestinationFolder
+ ) {
+ throw new ApplyAcceleratorError(accelerator.name, JSON.stringify(destinationPathValidation));
+ }
+
+ const workspaceFiles = await workspaces.getFiles({ workspaceId });
+
+ // Create new temporary branch with current files, but stay on main
+ await workspaces.branch({ workspaceId, name: BACKUP_BRANCH_NAME, checkout: false });
+
+ // Commit moved files to moved files branch (this commit will never be pushed, as this branch will be deleted)
+ await workspaces.commit({
+ workspaceId,
+ commitMessage: `KIE Sandbox: Backup files before applying ${accelerator.name} Accelerator`,
+ targetBranch: BACKUP_BRANCH_NAME,
+ });
+
+ // Create new temporary branch for moved files, but stay on main
+ await workspaces.branch({ workspaceId, name: MOVED_FILES_BRANCH_NAME, checkout: false });
+
+ // Checkout to moved files branch
+ await workspaces.checkout({
+ workspaceId,
+ ref: MOVED_FILES_BRANCH_NAME,
+ remote: GIT_ORIGIN_REMOTE_NAME,
+ });
+
+ // Move files
+ let currentFileAfterAccelerator: WorkspaceFile | undefined;
+ movedFiles = await Promise.all(
+ workspaceFiles.map(async (file) => {
+ let fileNewDestination: string;
+
+ if (isDecision(file.relativePath) || isScorecard(file.relativePath)) {
+ fileNewDestination = accelerator.dmnDestinationFolder;
+ } else if (isWorkflow(file.relativePath)) {
+ fileNewDestination = accelerator.bpmnDestinationFolder;
+ } else {
+ fileNewDestination = accelerator.otherFilesDestinationFolder;
+ }
+
+ const movedFile = await workspaces.moveFile({
+ file,
+ newDirPath: join(fileNewDestination, dirname(file.relativePath)),
+ });
+
+ await workspaces.stageFile({ workspaceId, relativePath: movedFile.relativePath });
+
+ if (file.relativePath === currentFile.relativePath) {
+ currentFileAfterAccelerator = movedFile;
+ }
+ return movedFile;
+ })
+ );
+
+ const movedFilesPaths = movedFiles.map((file) => file.relativePath);
+
+ if (!currentFileAfterAccelerator) {
+ throw new ApplyAcceleratorError(accelerator.name, "Failed to find current file after moving.");
+ }
+
+ // Commit moved files to moved files branch (this commit will never be pushed, as this branch will be deleted)
+ await workspaces.commit({
+ workspaceId,
+ commitMessage: `KIE Sandbox: Moving files to apply ${accelerator.name} Accelerator.`,
+ targetBranch: MOVED_FILES_BRANCH_NAME,
+ });
+
+ // Go back to main
+ await workspaces.checkout({ workspaceId, ref: GIT_DEFAULT_BRANCH, remote: GIT_ORIGIN_REMOTE_NAME });
+
+ // Add Accelerator remote and fetch it
+ await workspaces.addRemote({
+ workspaceId,
+ name: TEMP_ACCELERATOR_REMOTE_NAME,
+ url: accelerator.gitRepositoryUrl,
+ force: true,
+ });
+
+ const fetchResult = await workspaces.fetch({
+ workspaceId,
+ remote: TEMP_ACCELERATOR_REMOTE_NAME,
+ ref: accelerator.gitRepositoryGitRef,
+ });
+
+ if (!fetchResult.fetchHead) {
+ throw new ApplyAcceleratorError(
+ accelerator.name,
+ `Unable to find remote HEAD for ${accelerator.gitRepositoryGitRef} ref.`
+ );
+ }
+
+ // Checkout Accelerator files, wiping everything else
+ await workspaces.checkoutFilesFromLocalHead({
+ workspaceId,
+ ref: fetchResult.fetchHead,
+ filepaths: ["."],
+ });
+
+ // Delete Accelerator remote
+ await workspaces.deleteRemote({ workspaceId, name: TEMP_ACCELERATOR_REMOTE_NAME });
+
+ // Stage all Accelerator files
+ const acceleratorFiles = await workspaces.getFiles({ workspaceId });
+ await Promise.all(
+ acceleratorFiles.map(async (file) => {
+ return workspaces.stageFile({
+ workspaceId,
+ relativePath: file.relativePath,
+ });
+ })
+ );
+
+ // Bring moved user files back
+ await workspaces.checkoutFilesFromLocalHead({
+ workspaceId,
+ ref: MOVED_FILES_BRANCH_NAME,
+ filepaths: movedFilesPaths,
+ });
+
+ // Stage all moved files
+ await Promise.all(
+ movedFiles.map(async (file) => {
+ return workspaces.stageFile({
+ workspaceId,
+ relativePath: file.relativePath,
+ });
+ })
+ );
+
+ // Add Accelerator YAML config
+ const content = `# This file was automatically created by ${
+ i18n.names.businessModeler.online
+ }. Please don't modify it.\n${yaml.dump({ ...accelerator, appliedAt: new Date().toISOString() })}`;
+ configFile = await workspaces.addFile({
+ workspaceId,
+ name: ACCELERATOR_CONFIG_FILE_NAME,
+ destinationDirRelativePath: KIE_SANDBOX_DIRECTORY_PATH,
+ content,
+ extension: ACCELERATOR_CONFIG_FILE_EXTENSION,
+ });
+
+ await workspaces.stageFile({
+ workspaceId,
+ relativePath: ACCELERATOR_CONFIG_FILE_RELATIVE_PATH,
+ });
+
+ // Create commit
+ await workspaces.createSavePoint({
+ workspaceId,
+ gitConfig,
+ commitMessage: i18n.accelerators.commitMessage(accelerator.name),
+ forceHasChanges: true,
+ });
+
+ apllyingAcceleratorAlert.close();
+
+ applyAcceleratorSuccessAlert.show({ acceleratorName: accelerator.name });
+
+ attemptToDeleteTemporaryBranches();
+
+ history.replace({
+ pathname: routes.workspaceWithFilePath.path({
+ extension: currentFileAfterAccelerator.extension,
+ fileRelativePath: currentFileAfterAccelerator.relativePathWithoutExtension,
+ workspaceId,
+ }),
+ });
+ } catch (e) {
+ apllyingAcceleratorAlert.close();
+ applyAcceleratorFailAlert.show({ acceleratorName: accelerator.name });
+
+ console.error(e);
+
+ // Delete config file
+ if (configFile) {
+ await workspaces.deleteFile({ file: configFile });
+ }
+
+ // Return to main
+ await workspaces.checkout({ workspaceId, ref: GIT_DEFAULT_BRANCH, remote: GIT_ORIGIN_REMOTE_NAME });
+
+ // Revert repo
+ await workspaces.checkoutFilesFromLocalHead({
+ workspaceId,
+ ref: BACKUP_BRANCH_NAME,
+ filepaths: ["."],
+ });
+
+ attemptToDeleteTemporaryBranches();
+
+ // Delete moved files
+ await Promise.all(
+ movedFiles.map(
+ async (file) =>
+ (await workspaces.existsFile({ workspaceId, relativePath: file.relativePath })) &&
+ (await workspaces.deleteFile({ file }))
+ )
+ );
+
+ // Reload to currentFile file
+ history.replace({
+ pathname: routes.workspaceWithFilePath.path({
+ extension: currentFile.extension,
+ fileRelativePath: currentFile.relativePathWithoutExtension,
+ workspaceId,
+ }),
+ });
+ }
+ },
+ [
+ apllyingAcceleratorAlert,
+ applyAcceleratorFailAlert,
+ applyAcceleratorSuccessAlert,
+ attemptToDeleteTemporaryBranches,
+ gitConfig,
+ history,
+ i18n.accelerators,
+ i18n.names.businessModeler.online,
+ routes.workspaceWithFilePath,
+ workspace.descriptor.workspaceId,
+ workspaces,
+ ]
+ );
+
+ return { applyAcceleratorToWorkspace };
+}
+
+export function useCurrentAccelerator(workspaceId: string) {
+ const [currentAccelerator, setCurrentAccelerator] = useState<AcceleratorAppliedConfig>();
+
+ const acceleratorConfigFile = useWorkspaceFilePromise(workspaceId, ACCELERATOR_CONFIG_FILE_RELATIVE_PATH);
+
+ const getCurrentAccelerator = useCallback(async () => {
+ if (!acceleratorConfigFile?.data) {
+ return;
+ }
+
+ return yaml.load(
+ await acceleratorConfigFile.data.workspaceFile.getFileContentsAsString()
+ ) as AcceleratorAppliedConfig;
+ }, [acceleratorConfigFile.data]);
+
+ useCancelableEffect(
+ useCallback(
+ ({ canceled }) => {
+ getCurrentAccelerator().then((accelerator) => {
+ if (canceled.get()) {
+ return;
+ }
+ setCurrentAccelerator(accelerator);
+ });
+ },
+ [getCurrentAccelerator]
+ )
+ );
+
+ return currentAccelerator;
+}
diff --git a/packages/online-editor/src/authSessions/AuthSessionSelect.tsx b/packages/online-editor/src/authSessions/AuthSessionSelect.tsx
index f4ea420f71..e2e8a9f8c3 100644
--- a/packages/online-editor/src/authSessions/AuthSessionSelect.tsx
+++ b/packages/online-editor/src/authSessions/AuthSessionSelect.tsx
@@ -29,8 +29,7 @@ import { IconSize } from "@patternfly/react-icons/dist/js/createIcon";
import { AuthProvider, AuthProviderGroup } from "../authProviders/AuthProvidersApi";
import { useEffect, useMemo, useState } from "react";
import { ValidatedOptions } from "@patternfly/react-core/dist/js/helpers";
-import { Button } from "@patternfly/react-core/dist/js/components/Button";
-import { ButtonVariant } from "@patternfly/react-core";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import PlusIcon from "@patternfly/react-icons/dist/js/icons/plus-icon";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import { AccountsDispatchActionKind, useAccountsDispatch } from "../accounts/AccountsContext";
diff --git a/packages/online-editor/src/editor/EditorPage.tsx b/packages/online-editor/src/editor/EditorPage.tsx
index 60c8a43a61..0b1889ebd7 100644
--- a/packages/online-editor/src/editor/EditorPage.tsx
+++ b/packages/online-editor/src/editor/EditorPage.tsx
@@ -18,7 +18,7 @@ import * as React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHistory } from "react-router";
import { useRoutes } from "../navigation/Hooks";
-import { EditorToolbar } from "./EditorToolbar";
+import { EditorToolbar } from "./Toolbar/EditorToolbar";
import { useDmnTour } from "../tour";
import { useOnlineI18n } from "../i18n";
import { ChannelType } from "@kie-tools-core/editor/dist/api";
diff --git a/packages/online-editor/src/editor/EditorToolbar.tsx b/packages/online-editor/src/editor/EditorToolbar.tsx
deleted file mode 100644
index 148a3b2c10..0000000000
--- a/packages/online-editor/src/editor/EditorToolbar.tsx
+++ /dev/null
@@ -1,2237 +0,0 @@
-/*
- * Copyright 2019 Red Hat, Inc. and/or its affiliates.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {
- Dropdown,
- DropdownGroup,
- DropdownItem,
- DropdownPosition,
- DropdownToggle,
-} from "@patternfly/react-core/dist/js/components/Dropdown";
-import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
-import { Title } from "@patternfly/react-core/dist/js/components/Title";
-import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
-import {
- Toolbar,
- ToolbarContent,
- ToolbarGroup,
- ToolbarItem,
- ToolbarItemProps,
-} from "@patternfly/react-core/dist/js/components/Toolbar";
-import { EllipsisVIcon } from "@patternfly/react-icons/dist/js/icons/ellipsis-v-icon";
-import { SaveIcon } from "@patternfly/react-icons/dist/js/icons/save-icon";
-import { AngleLeftIcon } from "@patternfly/react-icons/dist/js/icons/angle-left-icon";
-import * as React from "react";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useOnlineI18n } from "../i18n";
-import { KieSandboxExtendedServicesButtons } from "./KieSandboxExtendedServices/KieSandboxExtendedServicesButtons";
-import {
- useNavigationBlockersBypass,
- useNavigationStatus,
- useNavigationStatusToggle,
- useRoutes,
-} from "../navigation/Hooks";
-import { EmbeddedEditorRef, useDirtyState } from "@kie-tools-core/editor/dist/embedded";
-import { useHistory } from "react-router";
-import { EmbedModal } from "./EmbedModal";
-import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/js/components/Alert";
-import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
-import { OutlinedClockIcon } from "@patternfly/react-icons/dist/js/icons/outlined-clock-icon";
-import { FolderIcon } from "@patternfly/react-icons/dist/js/icons/folder-icon";
-import { ImageIcon } from "@patternfly/react-icons/dist/js/icons/image-icon";
-import { DownloadIcon } from "@patternfly/react-icons/dist/js/icons/download-icon";
-import { PlusIcon } from "@patternfly/react-icons/dist/js/icons/plus-icon";
-import { ArrowCircleUpIcon } from "@patternfly/react-icons/dist/js/icons/arrow-circle-up-icon";
-import { ColumnsIcon } from "@patternfly/react-icons/dist/js/icons/columns-icon";
-import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
-import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
-import { NewFileDropdownMenu } from "./NewFileDropdownMenu";
-import { PageHeaderToolsItem, PageSection } from "@patternfly/react-core/dist/js/components/Page";
-import { FileLabel } from "../filesList/FileLabel";
-import { useWorkspacePromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks";
-import { OutlinedHddIcon } from "@patternfly/react-icons/dist/js/icons/outlined-hdd-icon";
-import { DesktopIcon } from "@patternfly/react-icons/dist/js/icons/desktop-icon";
-import { FileSwitcher } from "./FileSwitcher";
-import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
-import { KieSandboxExtendedServicesDropdownGroup } from "./KieSandboxExtendedServices/KieSandboxExtendedServicesDropdownGroup";
-import { TrashIcon } from "@patternfly/react-icons/dist/js/icons/trash-icon";
-import { CaretDownIcon } from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
-import {
- GIST_ORIGIN_REMOTE_NAME,
- GIT_ORIGIN_REMOTE_NAME,
-} from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
-import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
-import { PromiseStateWrapper } from "@kie-tools-core/react-hooks/dist/PromiseState";
-import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
-import { WorkspaceLabel } from "../workspace/components/WorkspaceLabel";
-import { EditorPageDockDrawerRef } from "./EditorPageDockDrawer";
-import { SyncAltIcon } from "@patternfly/react-icons/dist/js/icons/sync-alt-icon";
-import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
-import { UrlType, useImportableUrl } from "../importFromUrl/ImportableUrlHooks";
-import { Location } from "history";
-import { ExternalLinkAltIcon } from "@patternfly/react-icons/dist/js/icons/external-link-alt-icon";
-import { useEditorEnvelopeLocator } from "../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
-import { useCancelableEffect } from "@kie-tools-core/react-hooks/dist/useCancelableEffect";
-import type { RestEndpointMethodTypes as OctokitRestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types";
-import { useSharedValue } from "@kie-tools-core/envelope-bus/dist/hooks";
-import { WorkspaceStatusIndicator } from "../workspace/components/WorkspaceStatusIndicator";
-import { ResponsiveDropdown } from "../ResponsiveDropdown/ResponsiveDropdown";
-import { ResponsiveDropdownToggle } from "../ResponsiveDropdown/ResponsiveDropdownToggle";
-import { useAuthSession } from "../authSessions/AuthSessionsContext";
-import { AuthSessionSelect, AuthSessionSelectFilter } from "../authSessions/AuthSessionSelect";
-import { useAuthProvider } from "../authProviders/AuthProvidersContext";
-import { useGitHubClient } from "../github/Hooks";
-import { AccountsDispatchActionKind, useAccountsDispatch } from "../accounts/AccountsContext";
-import { SelectPosition } from "@patternfly/react-core/dist/js/components/Select";
-import {
- authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain,
- authSessionsSelectFilterCompatibleWithGitUrlDomain,
- gitAuthSessionSelectFilter,
-} from "../authSessions/CompatibleAuthSessions";
-import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
-import { useGlobalAlert, useGlobalAlertsDispatchContext } from "../alerts";
-import {
- AuthProviderGroup,
- GistEnabledAuthProviderType,
- GitAuthProviderType,
- isGistEnabledAuthProviderType,
-} from "../authProviders/AuthProvidersApi";
-import { CreateGitRepositoryModal, CreateRepositoryResponse } from "./CreateGitRepositoryModal";
-import { BitbucketIcon, GitIcon, GithubIcon } from "@patternfly/react-icons";
-import { useBitbucketClient } from "../bitbucket/Hooks";
-import {
- isGistLikeWorkspaceKind,
- WorkspaceKindGistLike,
- WorkspaceKindGitBased,
-} from "@kie-tools-core/workspaces-git-fs/src/worker/api/WorkspaceOrigin";
-import { switchExpression } from "../switchExpression/switchExpression";
-import { CreateGistOrSnippetModal } from "./CreateGistOrSnippetModal";
-
-export interface Props {
- editor: EmbeddedEditorRef | undefined;
- workspaceFile: WorkspaceFile;
- editorPageDock: EditorPageDockDrawerRef | undefined;
-}
-
-const showWhenSmall: ToolbarItemProps["visibility"] = {
- default: "visible",
- "2xl": "hidden",
- xl: "hidden",
- lg: "visible",
- md: "visible",
-};
-
-const hideWhenSmall: ToolbarItemProps["visibility"] = {
- default: "hidden",
- "2xl": "visible",
- xl: "visible",
- lg: "hidden",
- md: "hidden",
-};
-
-const hideWhenTiny: ToolbarItemProps["visibility"] = {
- default: "hidden",
- "2xl": "visible",
- xl: "visible",
- lg: "visible",
- md: "hidden",
-};
-
-export function EditorToolbar(props: Props) {
- const routes = useRoutes();
- const editorEnvelopeLocator = useEditorEnvelopeLocator();
- const history = useHistory();
- const workspaces = useWorkspaces();
- const accountsDispatch = useAccountsDispatch();
- const [isShareDropdownOpen, setShareDropdownOpen] = useState(false);
- const [isSyncGistOrSnippetDropdownOpen, setSyncGistOrSnippetDropdownOpen] = useState(false);
- const [isSyncGitRepositoryDropdownOpen, setSyncGitRepositoryDropdownOpen] = useState(false);
- const [isLargeKebabOpen, setLargeKebabOpen] = useState(false);
- const [isSmallKebabOpen, setSmallKebabOpen] = useState(false);
- const [isEmbedModalOpen, setEmbedModalOpen] = useState(false);
- const { i18n } = useOnlineI18n();
- const alertsDispatch = useGlobalAlertsDispatchContext();
- const isEdited = useDirtyState(props.editor);
- const downloadRef = useRef<HTMLAnchorElement>(null);
- const downloadAllRef = useRef<HTMLAnchorElement>(null);
- const downloadPreviewRef = useRef<HTMLAnchorElement>(null);
- const copyContentTextArea = useRef<HTMLTextAreaElement>(null);
- const [isNewFileDropdownMenuOpen, setNewFileDropdownMenuOpen] = useState(false);
- const workspacePromise = useWorkspacePromise(props.workspaceFile.workspaceId);
- const [isGistOrSnippetLoading, setGistOrSnippetLoading] = useState(false);
- const [gitHubGist, setGitHubGist] =
- useState<OctokitRestEndpointMethodTypes["gists"]["get"]["response"]["data"] | undefined>(undefined);
- const [bitbucketSnippet, setBitbucketSnippet] = useState<any>(undefined);
- const workspaceImportableUrl = useImportableUrl(workspacePromise.data?.descriptor.origin.url?.toString());
-
- const { authSession, authInfo, gitConfig } = useAuthSession(workspacePromise.data?.descriptor.gitAuthSessionId);
- const authProvider = useAuthProvider(authSession);
-
- const gitHubClient = useGitHubClient(authSession);
- const bitbucketClient = useBitbucketClient(authSession);
-
- const canPushToGitRepository = useMemo(
- () => authSession?.type === "git" && !!authProvider,
- [authProvider, authSession?.type]
- );
- const navigationBlockersBypass = useNavigationBlockersBypass();
-
- const [flushes] = useSharedValue(
- workspaces.workspacesSharedWorker.workspacesWorkerBus.clientApi.shared.kieSandboxWorkspacesStorage_flushes
- );
-
- const authSessionSelectFilter = useMemo(() => {
- if (!workspacePromise.data) {
- return gitAuthSessionSelectFilter();
- }
-
- if (workspacePromise.data.descriptor.origin.kind === WorkspaceKind.LOCAL) {
- return gitAuthSessionSelectFilter();
- }
-
- if (workspacePromise.data.descriptor.origin.kind === WorkspaceKind.GIT) {
- return authSessionsSelectFilterCompatibleWithGitUrlDomain(
- new URL(workspacePromise.data.descriptor.origin.url).hostname
- );
- }
-
- if (workspacePromise.data.descriptor.origin.kind === WorkspaceKind.GITHUB_GIST) {
- return authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain(
- new URL(workspacePromise.data.descriptor.origin.url).hostname,
- gitHubGist?.owner?.login
- );
- }
-
- if (workspacePromise.data.descriptor.origin.kind === WorkspaceKind.BITBUCKET_SNIPPET) {
- return authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain(
- new URL(workspacePromise.data.descriptor.origin.url).hostname,
- bitbucketSnippet?.owner?.login
- );
- }
-
- return gitAuthSessionSelectFilter();
- }, [bitbucketSnippet?.owner?.login, gitHubGist?.owner?.login, workspacePromise.data]);
-
- const isSaved = useMemo(() => {
- return !isEdited && flushes && !flushes.some((f) => f.includes(props.workspaceFile.workspaceId));
- }, [isEdited, flushes, props.workspaceFile.workspaceId]);
-
- // Prevent from closing without flushing before.
- useEffect(() => {
- if (isSaved) {
- return;
- }
-
- window.onbeforeunload = () => "Some changes are not written to disk yet.";
- return () => {
- window.onbeforeunload = null;
- };
- }, [isSaved]);
-
- useCancelableEffect(
- useCallback(
- ({ canceled }) => {
- if (gitHubGist || workspaceImportableUrl.type !== UrlType.GIST_DOT_GITHUB_DOT_COM) {
- return;
- }
-
- const { gistId } = workspaceImportableUrl;
-
- if (!gistId) {
- return;
- }
-
- gitHubClient.gists.get({ gist_id: gistId }).then(({ data: gist }) => {
- if (canceled.get()) {
- return;
- }
-
- if (gist) {
- setGitHubGist(gist);
- }
- });
- },
- [gitHubGist, workspaceImportableUrl, gitHubClient.gists]
- )
- );
-
- useCancelableEffect(
- useCallback(
- ({ canceled }) => {
- if (bitbucketSnippet || workspaceImportableUrl.type !== UrlType.BITBUCKET_DOT_ORG_SNIPPET) {
- return;
- }
-
- const { snippetId, org } = workspaceImportableUrl;
-
- if (!snippetId || !org) {
- return;
- }
-
- bitbucketClient.getSnippet({ workspace: org, snippetId }).then(async (response) => {
- if (canceled.get()) {
- return;
- }
- const json = await response.json();
- setBitbucketSnippet(json);
- });
- },
- [bitbucketSnippet, workspaceImportableUrl, bitbucketClient]
- )
- );
-
- const successfullyCreatedGistOrSnippetAlert = useGlobalAlert(
- useCallback(
- ({ close }) => {
- if (!isGistLikeWorkspaceKind(workspacePromise.data?.descriptor.origin.kind)) {
- return <></>;
- }
-
- const gistOrSnippetUrl = workspacePromise.data?.descriptor.origin.url;
- return (
- <Alert
- variant="success"
- title={switchExpression(workspacePromise.data?.descriptor.origin.kind, {
- GITHUB_GIST: i18n.editorPage.alerts.createGist,
- BITBUCKET_SNIPPET: i18n.editorPage.alerts.createSnippet,
- })}
- actionClose={<AlertActionCloseButton onClose={close} />}
- actionLinks={
- <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
- {gistOrSnippetUrl}
- </AlertActionLink>
- }
- />
- );
- },
- [
- i18n.editorPage.alerts.createGist,
- i18n.editorPage.alerts.createSnippet,
- workspacePromise.data?.descriptor.origin.kind,
- workspacePromise.data?.descriptor.origin.url,
- ]
- ),
- { durationInSeconds: 4 }
- );
-
- const loadingGistOrSnippetAlert = useGlobalAlert(
- useCallback(
- ({ close }) => {
- if (!isGistLikeWorkspaceKind(workspacePromise.data?.descriptor.origin.kind)) {
- return <></>;
- }
-
- const gistOrSnippetUrl = workspacePromise.data?.descriptor.origin.url;
- return (
- <Alert
- variant="info"
- title={
- <>
- <Spinner size={"sm"} />
- Updating{" "}
- {switchExpression(workspacePromise.data?.descriptor.origin.kind, {
- BITBUCKET_SNIPPET: "Snippet",
- GITHUB_GIST: "Gist",
- })}
- ...
- </>
- }
- actionClose={<AlertActionCloseButton onClose={close} />}
- actionLinks={
- <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
- {gistOrSnippetUrl}
- </AlertActionLink>
- }
- />
- );
- },
- [workspacePromise.data?.descriptor.origin.kind, workspacePromise.data?.descriptor.origin.url]
- )
- );
-
- useEffect(() => {
- if (isGistOrSnippetLoading) {
- loadingGistOrSnippetAlert.show();
- } else {
- loadingGistOrSnippetAlert.close();
- }
- }, [isGistOrSnippetLoading, loadingGistOrSnippetAlert]);
-
- const successfullyUpdatedGistOrSnippetAlert = useGlobalAlert(
- useCallback(
- ({ close }) => {
- if (!isGistLikeWorkspaceKind(workspacePromise.data?.descriptor.origin.kind)) {
- return <></>;
- }
-
- const gistOrSnippetUrl = workspacePromise.data?.descriptor.origin.url;
- return (
- <Alert
- variant="success"
- title={switchExpression(workspacePromise.data?.descriptor.origin.kind, {
- GITHUB_GIST: i18n.editorPage.alerts.updateGist,
- BITBUCKET_SNIPPET: i18n.editorPage.alerts.updateSnippet,
- })}
- actionClose={<AlertActionCloseButton onClose={close} />}
- actionLinks={
- <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
- {gistOrSnippetUrl}
- </AlertActionLink>
- }
- />
- );
- },
- [
- i18n.editorPage.alerts.updateGist,
- i18n.editorPage.alerts.updateSnippet,
- workspacePromise.data?.descriptor.origin.kind,
- workspacePromise.data?.descriptor.origin.url,
- ]
- ),
- { durationInSeconds: 4 }
- );
-
- const errorAlert = useGlobalAlert(
- useCallback(
- ({ close }) => (
- <Alert
- variant="danger"
- title={i18n.editorPage.alerts.error}
- actionClose={<AlertActionCloseButton onClose={close} />}
- />
- ),
- [i18n]
- )
- );
-
- const shouldIncludeDownloadSvgDropdownItem = useMemo(() => {
- return props.workspaceFile.extension.toLowerCase() !== "pmml";
- }, [props.workspaceFile]);
-
- const shouldIncludeEmbedDropdownItem = useMemo(() => {
- return props.workspaceFile.extension.toLowerCase() !== "pmml";
- }, [props.workspaceFile]);
-
- const onDownload = useCallback(() => {
- props.editor?.getStateControl().setSavedCommand();
- alertsDispatch.closeAll();
- props.workspaceFile.getFileContents().then((content) => {
- if (downloadRef.current) {
- const fileBlob = new Blob([content], { type: "text/plain" });
- downloadRef.current.href = URL.createObjectURL(fileBlob);
- downloadRef.current.click();
- }
- });
- }, [props.editor, props.workspaceFile, alertsDispatch]);
-
- const downloadWorkspaceZip = useCallback(async () => {
- if (!props.editor) {
- return;
- }
-
- const zipBlob = await workspaces.prepareZip({ workspaceId: props.workspaceFile.workspaceId });
- if (downloadAllRef.current) {
- downloadAllRef.current.href = URL.createObjectURL(zipBlob);
- downloadAllRef.current.click();
- }
- if (workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.LOCAL) {
- await workspaces.createSavePoint({ workspaceId: props.workspaceFile.workspaceId, gitConfig });
- }
- }, [
- props.editor,
- props.workspaceFile.workspaceId,
- workspaces,
- workspacePromise.data?.descriptor.origin.kind,
- gitConfig,
- ]);
-
- const downloadSvg = useCallback(() => {
- props.editor?.getPreview().then((previewSvg) => {
- if (downloadPreviewRef.current && previewSvg) {
- const fileBlob = new Blob([previewSvg], { type: "image/svg+xml" });
- downloadPreviewRef.current.href = URL.createObjectURL(fileBlob);
- downloadPreviewRef.current.click();
- }
- });
- }, [props.editor]);
-
- const forceUpdateGistOrSnippet = useCallback(async () => {
- try {
- if (!authInfo || !workspacePromise.data) {
- return;
- }
-
- setGistOrSnippetLoading(true);
-
- await workspaces.push({
- workspaceId: props.workspaceFile.workspaceId,
- remote: GIST_ORIGIN_REMOTE_NAME,
- ref: workspacePromise.data.descriptor.origin.branch,
- remoteRef: `refs/heads/${workspacePromise.data.descriptor.origin.branch}`,
- force: true,
- authInfo,
- });
-
- await workspaces.pull({
- workspaceId: props.workspaceFile.workspaceId,
- authInfo,
- });
- } catch (e) {
- errorAlert.show();
- } finally {
- setGistOrSnippetLoading(false);
- setSyncGistOrSnippetDropdownOpen(false);
- }
-
- successfullyUpdatedGistOrSnippetAlert.show();
- }, [
- successfullyUpdatedGistOrSnippetAlert,
- authInfo,
- workspacePromise.data,
- workspaces,
- props.workspaceFile.workspaceId,
- errorAlert,
- ]);
-
- const errorPushingGistOrSnippet = useGlobalAlert(
- useCallback(
- ({ close }) => {
- return (
- <Alert
- variant="danger"
- title={switchExpression(workspacePromise.data?.descriptor.origin.kind as WorkspaceKindGistLike, {
- GITHUB_GIST: i18n.editorPage.alerts.errorPushingGist,
- BITBUCKET_SNIPPET: i18n.editorPage.alerts.errorPushingSnippet,
- })}
- actionLinks={[
- <AlertActionLink
- key="force"
- onClick={() => {
- close();
- forceUpdateGistOrSnippet();
- }}
- >
- Push forcefully
- </AlertActionLink>,
- <AlertActionLink key="dismiss" onClick={close}>
- Dismiss
- </AlertActionLink>,
- ]}
- actionClose={<AlertActionCloseButton onClose={close} />}
- >
- <b>{i18n.editorPage.alerts.forcePushWarning}</b>
- </Alert>
- );
- },
- [
- workspacePromise.data?.descriptor.origin.kind,
- i18n.editorPage.alerts.errorPushingGist,
- i18n.editorPage.alerts.errorPushingSnippet,
- i18n.editorPage.alerts.forcePushWarning,
- forceUpdateGistOrSnippet,
- ]
- )
- );
-
- const updateGistOrSnippet = useCallback(async () => {
- try {
- if (
- !authInfo ||
- !workspacePromise.data ||
- !isGistEnabledAuthProviderType(authProvider?.type) ||
- !isGistLikeWorkspaceKind(workspacePromise.data.descriptor.origin.kind)
- ) {
- return;
- }
- setGistOrSnippetLoading(true);
-
- await workspaces.createSavePoint({
- workspaceId: props.workspaceFile.workspaceId,
- gitConfig,
- });
-
- await workspaces.push({
- workspaceId: props.workspaceFile.workspaceId,
- remote: GIST_ORIGIN_REMOTE_NAME,
- ref: workspacePromise.data.descriptor.origin.branch,
- remoteRef: `refs/heads/${workspacePromise.data.descriptor.origin.branch}`,
- force: false,
- authInfo,
- });
-
- await workspaces.pull({
- workspaceId: props.workspaceFile.workspaceId,
- authInfo,
- });
- } catch (e) {
- errorPushingGistOrSnippet.show();
- throw e;
- } finally {
- setGistOrSnippetLoading(false);
- setSyncGistOrSnippetDropdownOpen(false);
- }
-
- successfullyUpdatedGistOrSnippetAlert.show();
- }, [
- successfullyUpdatedGistOrSnippetAlert,
- authInfo,
- workspacePromise.data,
- authProvider?.type,
- workspaces,
- props.workspaceFile.workspaceId,
- gitConfig,
- errorPushingGistOrSnippet,
- ]);
-
- const forkGitHubGist = useCallback(async () => {
- try {
- if (!authSession || !authInfo || !gitHubGist?.id || !workspacePromise.data) {
- return;
- }
-
- setGistOrSnippetLoading(true);
-
- // Fork Gist
- const gist = await gitHubClient.gists.fork({
- gist_id: gitHubGist.id,
- });
-
- const remoteName = gist.data.id;
-
- // Adds forked gist remote to current one
- await workspaces.addRemote({
- workspaceId: props.workspaceFile.workspaceId,
- url: gist.data.git_push_url,
- name: remoteName,
- force: true,
- });
-
- // Commit
- await workspaces.createSavePoint({
- workspaceId: props.workspaceFile.workspaceId,
- gitConfig,
- });
-
- // Push to forked gist remote
- await workspaces.push({
- workspaceId: props.workspaceFile.workspaceId,
- remote: remoteName,
- ref: workspacePromise.data.descriptor.origin.branch,
- remoteRef: `refs/heads/${workspacePromise.data.descriptor.origin.branch}`,
- force: true,
- authInfo,
- });
-
- // Redirect to import workspace
- navigationBlockersBypass.execute(() => {
- history.push({
- pathname: routes.import.path({}),
- search: routes.import.queryString({
- url: gist.data.html_url,
- authSessionId: authSession.id,
- }),
- });
- });
- } catch (err) {
- errorAlert.show();
- throw err;
- } finally {
- setGistOrSnippetLoading(false);
- }
- }, [
- authSession,
- authInfo,
- gitHubGist,
- workspacePromise.data,
- gitHubClient.gists,
- workspaces,
- props.workspaceFile.workspaceId,
- gitConfig,
- navigationBlockersBypass,
- history,
- routes.import,
- errorAlert,
- ]);
-
- const openEmbedModal = useCallback(() => {
- setEmbedModalOpen(true);
- }, []);
-
- const workspaceHasNestedDirectories = useMemo(
- () => workspacePromise.data?.files.filter((f) => f.relativePath !== f.name).length !== 0,
- [workspacePromise]
- );
-
- const isGitHubGistOwner = useMemo(() => {
- return authInfo?.username && gitHubGist?.owner?.login === authInfo.username;
- }, [authInfo, gitHubGist]);
-
- const isBitbucketSnippetOwner = useMemo(() => {
- return authInfo?.uuid && bitbucketSnippet?.creator.uuid === authInfo.uuid;
- }, [authInfo, bitbucketSnippet]);
-
- const isGistOrSnippetOwner = isGitHubGistOwner || isBitbucketSnippetOwner;
-
- const canCreateGitHubRepository = useMemo(() => authProvider?.type === "github", [authProvider?.type]);
- const canCreateBitbucketRepository = useMemo(() => authProvider?.type === "bitbucket", [authProvider?.type]);
- const canCreateGitRepository = useMemo(
- () => canCreateGitHubRepository || canCreateBitbucketRepository,
- [canCreateGitHubRepository, canCreateBitbucketRepository]
- );
-
- const canCreateGistOrSnippet = useMemo(
- () =>
- authProvider &&
- isGistEnabledAuthProviderType(authProvider?.type) &&
- workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.LOCAL &&
- !workspaceHasNestedDirectories,
- [authProvider, workspacePromise.data?.descriptor.origin.kind, workspaceHasNestedDirectories]
- );
-
- const canUpdateGistOrSnippet = useMemo(
- () =>
- authProvider &&
- isGistEnabledAuthProviderType(authProvider?.type) &&
- !!isGistOrSnippetOwner &&
- workspacePromise.data &&
- isGistLikeWorkspaceKind(workspacePromise.data?.descriptor.origin.kind) &&
- !workspaceHasNestedDirectories,
- [authProvider, isGistOrSnippetOwner, workspacePromise.data, workspaceHasNestedDirectories]
- );
-
- const canForkGitHubGist = useMemo(
- () =>
- authProvider?.type === "github" &&
- !isGistOrSnippetOwner &&
- workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.GITHUB_GIST &&
- !workspaceHasNestedDirectories,
- [
- authProvider?.type,
- isGistOrSnippetOwner,
- workspacePromise.data?.descriptor.origin.kind,
- workspaceHasNestedDirectories,
- ]
- );
-
- const changeGitAuthSessionId = useCallback(
- (newGitAuthSessionId: React.SetStateAction<string | undefined>, lastAuthSessionId: string | undefined) => {
- workspaces.changeGitAuthSessionId({
- workspaceId: props.workspaceFile.workspaceId,
- gitAuthSessionId:
- typeof newGitAuthSessionId === "function" ? newGitAuthSessionId(lastAuthSessionId) : newGitAuthSessionId,
- });
- },
- [props.workspaceFile.workspaceId, workspaces]
- );
-
- const [isCreateGitRepositoryModalOpen, setCreateGitRepositoryModalOpen] = useState(false);
- const [isCreateGistOrSnippetModalOpen, setCreateGistOrSnippetModalOpen] = useState(false);
-
- const shareDropdownItems = useMemo(
- () => [
- <DropdownGroup key={"download-group"} label="Download">
- <DropdownItem
- onClick={onDownload}
- key={"download-file-item"}
- description={`${props.workspaceFile.name} will be downloaded`}
- icon={<DownloadIcon />}
- ouiaId="download-file-dropdown-button"
- >
- Current file
- </DropdownItem>
- {shouldIncludeDownloadSvgDropdownItem && (
- <DropdownItem
- key={`dropdown-download-svg`}
- data-testid="dropdown-download-svg"
- component="button"
- onClick={downloadSvg}
- description={`Image of ${props.workspaceFile.name} will be downloaded in SVG format`}
- icon={<ImageIcon />}
- >
- {"Current file's SVG"}
- </DropdownItem>
- )}
- <DropdownItem
- onClick={downloadWorkspaceZip}
- key={"download-zip-item"}
- description={`A zip file including all files will be downloaded`}
- icon={<FolderIcon />}
- >
- All files
- </DropdownItem>
- </DropdownGroup>,
- ...(shouldIncludeEmbedDropdownItem
- ? [
- <Divider key={"divider-other-group"} />,
- <DropdownGroup key={"other-group"} label="Other">
- <DropdownItem
- key={`dropdown-embed`}
- data-testid="dropdown-embed"
- component="button"
- onClick={openEmbedModal}
- icon={<ColumnsIcon />}
- >
- {i18n.editorToolbar.embed}...
- </DropdownItem>
- </DropdownGroup>,
- ]
- : []),
- ...(workspacePromise.data &&
- (workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.LOCAL ||
- isGistLikeWorkspaceKind(workspacePromise.data?.descriptor.origin.kind))
- ? [
- <DropdownGroup key={"git-group"} label={i18n.names.git}>
- <Alert
- isInline={true}
- variant={"info"}
- title={"Authentication source"}
- actionLinks={
- <AuthSessionSelect
- title={`Select Git authentication for '${workspacePromise.data.descriptor.name}'...`}
- position={SelectPosition.right}
- isPlain={false}
- authSessionId={workspacePromise.data.descriptor.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
- setAuthSessionId={(newAuthSessionId) => {
- changeGitAuthSessionId(newAuthSessionId, workspacePromise.data?.descriptor.gitAuthSessionId);
- setTimeout(() => {
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- setShareDropdownOpen(true);
- setSmallKebabOpen(true);
- }, 0);
- }}
- filter={authSessionSelectFilter}
- />
- }
- >
- {`Manage authentication sources for '${workspacePromise.data.descriptor.name}' to be able to create Repository, GitHub Gist or Bitbucket Snippet.`}
- </Alert>
- {canCreateGitRepository && (
- <Tooltip
- data-testid={"create-git-repository-tooltip"}
- key={`dropdown-create-git-repository`}
- content={<div>{"Click to create a repository."}</div>}
- trigger={!canCreateGitRepository ? "mouseenter click" : ""}
- position="left"
- >
- <DropdownItem
- icon={switchExpression(authProvider?.type as GitAuthProviderType, {
- bitbucket: <BitbucketIcon />,
- github: <GithubIcon />,
- default: <GitIcon />,
- })}
- data-testid={"create-git-repository-button"}
- component="button"
- onClick={() => setCreateGitRepositoryModalOpen(true)}
- isDisabled={!canCreateGitRepository}
- >
- Create{" "}
- {switchExpression(authProvider?.type as GitAuthProviderType, {
- github: "GitHub repository",
- bitbucket: "Bitbucket repository",
- default: "Git repository",
- })}
- ...
- </DropdownItem>
- </Tooltip>
- )}
- {canCreateGistOrSnippet && (
- <Tooltip
- data-testid={"create-gist-or-snippet-tooltip"}
- key={`dropdown-create-gist-or-snippet`}
- content={
- <div>
- {switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
- github: i18n.editorToolbar.cantCreateGistTooltip,
- bitbucket: i18n.editorToolbar.cantCreateSnippetTooltip,
- })}
- </div>
- }
- trigger={!canCreateGistOrSnippet ? "mouseenter click" : ""}
- position="left"
- >
- <DropdownItem
- icon={switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
- bitbucket: <BitbucketIcon />,
- github: <GithubIcon />,
- })}
- data-testid={"create-gist-or-snippet-button"}
- component="button"
- onClick={() => setCreateGistOrSnippetModalOpen(true)}
- isDisabled={!canCreateGistOrSnippet}
- >
- {switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
- github: i18n.editorToolbar.createGist,
- bitbucket: i18n.editorToolbar.createSnippet,
- })}
- </DropdownItem>
- </Tooltip>
- )}
- </DropdownGroup>,
- ]
- : []),
- ],
- [
- onDownload,
- props.workspaceFile.name,
- shouldIncludeDownloadSvgDropdownItem,
- downloadSvg,
- downloadWorkspaceZip,
- shouldIncludeEmbedDropdownItem,
- openEmbedModal,
- i18n.editorToolbar.embed,
- i18n.editorToolbar.cantCreateGistTooltip,
- i18n.editorToolbar.cantCreateSnippetTooltip,
- i18n.editorToolbar.createGist,
- i18n.editorToolbar.createSnippet,
- i18n.names.git,
- workspacePromise.data,
- authSessionSelectFilter,
- canCreateGitRepository,
- authProvider?.type,
- canCreateGistOrSnippet,
- changeGitAuthSessionId,
- accountsDispatch,
- ]
- );
-
- useEffect(() => {
- if (!workspacePromise.data) {
- return;
- }
- if (downloadRef.current) {
- downloadRef.current.download = `${props.workspaceFile.name}`;
- }
- if (downloadAllRef.current) {
- downloadAllRef.current.download = `${workspacePromise.data.descriptor.name}.zip`;
- }
- if (downloadPreviewRef.current) {
- downloadPreviewRef.current.download = `${props.workspaceFile.name}.svg`;
- }
- }, [props.workspaceFile, workspacePromise.data]);
-
- const deleteWorkspaceFile = useCallback(async () => {
- if (!workspacePromise.data) {
- return;
- }
-
- if (workspacePromise.data.files.length === 1) {
- await workspaces.deleteWorkspace({ workspaceId: props.workspaceFile.workspaceId });
- history.push({ pathname: routes.home.path({}) });
- return;
- }
-
- const nextFile = workspacePromise.data.files
- .filter((f) => {
- return (
- f.relativePath !== props.workspaceFile.relativePath && editorEnvelopeLocator.hasMappingFor(f.relativePath)
- );
- })
- .pop();
-
- await workspaces.deleteFile({
- file: props.workspaceFile,
- });
-
- if (!nextFile) {
- history.push({ pathname: routes.home.path({}) });
- return;
- }
-
- history.push({
- pathname: routes.workspaceWithFilePath.path({
- workspaceId: nextFile.workspaceId,
- fileRelativePath: nextFile.relativePathWithoutExtension,
- extension: nextFile.extension,
- }),
- });
- }, [routes, history, workspacePromise.data, props.workspaceFile, workspaces, editorEnvelopeLocator]);
-
- const workspaceNameRef = useRef<HTMLInputElement>(null);
-
- const resetWorkspaceName = useCallback(() => {
- if (workspaceNameRef.current && workspacePromise.data) {
- workspaceNameRef.current.value = workspacePromise.data.descriptor.name;
- }
- }, [workspacePromise.data]);
-
- useEffect(resetWorkspaceName, [resetWorkspaceName]);
-
- const onRenameWorkspace = useCallback(
- async (newName: string | undefined) => {
- if (!newName) {
- resetWorkspaceName();
- return;
- }
-
- if (!workspacePromise.data || newName === workspacePromise.data.descriptor.name) {
- return;
- }
-
- await workspaces.renameWorkspace({
- workspaceId: workspacePromise.data.descriptor.workspaceId,
- newName: newName.trim(),
- });
- },
- [workspacePromise.data, workspaces, resetWorkspaceName]
- );
-
- const onWorkspaceNameKeyDown = useCallback(
- (e: React.KeyboardEvent<HTMLInputElement>) => {
- e.stopPropagation();
- if (e.keyCode === 13 /* Enter */) {
- e.currentTarget.blur();
- } else if (e.keyCode === 27 /* ESC */) {
- resetWorkspaceName();
- e.currentTarget.blur();
- }
- },
- [resetWorkspaceName]
- );
-
- const deleteFileDropdownItem = useMemo(() => {
- return (
- <DropdownItem key={"delete-dropdown-item"} onClick={deleteWorkspaceFile}>
- <Flex flexWrap={{ default: "nowrap" }}>
- <FlexItem>
- <TrashIcon />
- Delete <b>{`"${props.workspaceFile.nameWithoutExtension}"`}</b>
- </FlexItem>
- <FlexItem>
- <b>
- <FileLabel extension={props.workspaceFile.extension} />
- </b>
- </FlexItem>
- </Flex>
- </DropdownItem>
- );
- }, [deleteWorkspaceFile, props.workspaceFile]);
-
- const pushingAlert = useGlobalAlert(
- useCallback(() => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return (
- <Alert
- variant="info"
- title={
- <>
- <Spinner size={"sm"} />
- {`Pushing to '${workspacePromise.data?.descriptor.origin.url}'...`}
- </>
- }
- />
- );
- }, [workspacePromise])
- );
-
- const comittingAlert = useGlobalAlert(
- useCallback(() => {
- return (
- <Alert
- variant="info"
- title={
- <>
- <Spinner size={"sm"} />
- {`Creating commit...`}
- </>
- }
- />
- );
- }, [])
- );
-
- const commitSuccessAlert = useGlobalAlert(
- useCallback(() => {
- return <Alert variant="success" title={`Commit created.`} />;
- }, []),
- { durationInSeconds: 2 }
- );
-
- const commitFailAlert = useGlobalAlert(
- useCallback(({ close }, staticArgs: { reason: string }) => {
- return (
- <Alert
- variant="danger"
- title={`Failed to commit: ${staticArgs.reason}`}
- actionClose={<AlertActionCloseButton onClose={close} />}
- />
- );
- }, [])
- );
-
- const nothingToCommitAlert = useGlobalAlert(
- useCallback(() => {
- return <Alert variant="info" title={"Nothing to commit."} />;
- }, []),
- { durationInSeconds: 2 }
- );
-
- const createSavePointDropdownItem = useMemo(() => {
- return (
- <DropdownItem
- key={"commit-dropdown-item"}
- icon={<SaveIcon />}
- onClick={async () => {
- if (!(await workspaces.hasLocalChanges({ workspaceId: props.workspaceFile.workspaceId }))) {
- nothingToCommitAlert.show();
- return;
- }
- comittingAlert.show();
- try {
- await workspaces.createSavePoint({
- workspaceId: props.workspaceFile.workspaceId,
- gitConfig,
- });
- comittingAlert.close();
- commitSuccessAlert.show();
- } catch (e) {
- comittingAlert.close();
- commitFailAlert.show({ reason: e.message });
- }
- }}
- description={"Create a save point"}
- >
- Commit
- </DropdownItem>
- );
- }, [
- workspaces,
- props.workspaceFile.workspaceId,
- comittingAlert,
- nothingToCommitAlert,
- gitConfig,
- commitSuccessAlert,
- commitFailAlert,
- ]);
-
- const pushSuccessAlert = useGlobalAlert(
- useCallback(() => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return <Alert variant="success" title={`Pushed to '${workspacePromise.data?.descriptor.origin.url}'`} />;
- }, [workspacePromise]),
- { durationInSeconds: 4 }
- );
-
- const pushErrorAlert = useGlobalAlert(
- useCallback(
- ({ close }) => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return (
- <Alert
- variant="danger"
- title={`Error Pushing to '${workspacePromise.data?.descriptor.origin.url}'`}
- actionClose={<AlertActionCloseButton onClose={close} />}
- />
- );
- },
- [workspacePromise]
- )
- );
-
- const pullingAlert = useGlobalAlert(
- useCallback(() => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return (
- <Alert
- variant="info"
- title={
- <>
- <Spinner size={"sm"} />
- {`Pulling from '${workspacePromise.data?.descriptor.origin.url}'...`}
- </>
- }
- />
- );
- }, [workspacePromise])
- );
-
- const pullSuccessAlert = useGlobalAlert(
- useCallback(() => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return <Alert variant="success" title={`Pulled from '${workspacePromise.data?.descriptor.origin.url}'`} />;
- }, [workspacePromise]),
- { durationInSeconds: 4 }
- );
-
- const pushNewBranch = useCallback(
- async (newBranchName: string) => {
- if (!authInfo || !workspacePromise.data) {
- return;
- }
-
- try {
- pushingAlert.show();
-
- await workspaces.createSavePoint({
- workspaceId: props.workspaceFile.workspaceId,
- gitConfig,
- });
-
- await workspaces.branch({ workspaceId: props.workspaceFile.workspaceId, checkout: false, name: newBranchName });
-
- await workspaces.push({
- workspaceId: props.workspaceFile.workspaceId,
- remote: GIT_ORIGIN_REMOTE_NAME,
- remoteRef: `refs/heads/${newBranchName}`,
- ref: newBranchName,
- force: false,
- authInfo,
- });
-
- history.push({
- pathname: routes.import.path({}),
- search: routes.import.queryString({
- url: `${workspacePromise.data.descriptor.origin.url}`,
- branch: newBranchName,
- authSessionId: workspacePromise.data.descriptor.gitAuthSessionId,
- }),
- });
- } catch (e) {
- pushErrorAlert.show();
- } finally {
- pushingAlert.close();
- }
- },
- [
- authInfo,
- workspacePromise.data,
- pushingAlert,
- workspaces,
- props.workspaceFile.workspaceId,
- gitConfig,
- history,
- routes.import,
- pushErrorAlert,
- ]
- );
-
- const pullErrorAlert = useGlobalAlert<{ newBranchName: string; onTryAgain: () => any }>(
- useCallback(
- ({ close }, { newBranchName }) => {
- if (workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.GIT) {
- return <></>;
- }
-
- return (
- <Alert
- variant="danger"
- title={`Error Pulling from '${workspacePromise.data?.descriptor.origin.url}'`}
- actionClose={<AlertActionCloseButton onClose={close} />}
- actionLinks={<></>}
- >
- {`This usually happens when your branch has conflicts with the upstream branch or you don't have permission to Pull.`}
- <br />
- <br />
- {`You can save your work to a new branch.`}
- <br />
- <br />
- <Tooltip
- data-testid={"gist-it-tooltip"}
- content={<div>{`You need select an authentication source to be able to Push to a new branch.`}</div>}
- trigger={!canPushToGitRepository ? "mouseenter click" : ""}
- position="left"
- >
- <Button
- onClick={() => pushNewBranch(newBranchName)}
- variant={ButtonVariant.link}
- style={{ paddingLeft: 0 }}
- isSmall={true}
- isDisabled={!canPushToGitRepository}
- >
- {`Switch to '${newBranchName}'`}
- </Button>
- </Tooltip>
- <br />
- <br />
-
- {`Or change the authentication source for '${workspacePromise.data?.descriptor.name}' and try again`}
- <br />
- </Alert>
- );
- },
- [canPushToGitRepository, pushNewBranch, workspacePromise.data]
- )
- );
-
- const pullFromGitRepository = useCallback(
- async (args: { showAlerts: boolean }) => {
- pullingAlert.close();
- pullErrorAlert.close();
- pullSuccessAlert.close();
-
- if (args.showAlerts) {
- pullingAlert.show();
- }
- await workspaces.createSavePoint({
- workspaceId: props.workspaceFile.workspaceId,
- gitConfig,
- });
-
- try {
- await workspaces.pull({
- workspaceId: props.workspaceFile.workspaceId,
- authInfo,
- });
-
- if (args.showAlerts) {
- pullSuccessAlert.show();
- }
- } catch (e) {
- console.error(e);
- if (args.showAlerts) {
- const randomString = (Math.random() + 1).toString(36).substring(7);
- const newBranchName = `${workspacePromise.data?.descriptor.origin.branch}-${randomString}`;
- pullErrorAlert.show({ newBranchName, onTryAgain: () => pullFromGitRepository({ showAlerts: true }) });
- }
- } finally {
- if (args.showAlerts) {
- pullingAlert.close();
- }
- }
- },
- [
- pullingAlert,
- pullErrorAlert,
- pullSuccessAlert,
- workspaces,
- props.workspaceFile.workspaceId,
- gitConfig,
- authInfo,
- workspacePromise.data?.descriptor.origin.branch,
- ]
- );
-
- const pushToGitRepository = useCallback(async () => {
- pushingAlert.close();
- pushErrorAlert.close();
- pushSuccessAlert.close();
-
- if (!authInfo) {
- return;
- }
-
- pushingAlert.show();
- try {
- const workspaceId = props.workspaceFile.workspaceId;
- await workspaces.createSavePoint({
- workspaceId: workspaceId,
- gitConfig,
- });
-
- const workspace = await workspaces.getWorkspace({ workspaceId });
- await workspaces.push({
- workspaceId: props.workspaceFile.workspaceId,
- ref: workspace.origin.branch,
- remote: GIST_ORIGIN_REMOTE_NAME,
- remoteRef: `refs/heads/${workspace.origin.branch}`,
- force: false,
- authInfo,
- });
- await pullFromGitRepository({ showAlerts: false });
- pushSuccessAlert.show();
- } catch (e) {
- console.error(e);
- pushErrorAlert.show();
- } finally {
- pushingAlert.close();
- }
- }, [
- pushingAlert,
- pushErrorAlert,
- pushSuccessAlert,
- authInfo,
- props.workspaceFile.workspaceId,
- workspaces,
- gitConfig,
- pullFromGitRepository,
- ]);
-
- const navigationStatus = useNavigationStatus();
- const navigationStatusToggle = useNavigationStatusToggle();
- const confirmNavigationAlert = useGlobalAlert<{ lastBlockedLocation: Location }>(
- useCallback(
- (_, { lastBlockedLocation }) => (
- <Alert
- data-testid="unsaved-alert"
- variant="warning"
- title={
- workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.LOCAL
- ? i18n.editorPage.alerts.unsaved.titleLocal
- : i18n.editorPage.alerts.unsaved.titleGit
- }
- actionClose={
- <AlertActionCloseButton data-testid="unsaved-alert-close-button" onClose={navigationStatusToggle.unblock} />
- }
- actionLinks={
- <>
- <Divider inset={{ default: "insetMd" }} />
- <br />
- {(workspacePromise.data?.descriptor.origin.kind === WorkspaceKind.LOCAL && (
- <AlertActionLink
- data-testid="unsaved-alert-save-button"
- onClick={() => {
- navigationStatusToggle.unblock();
- return downloadWorkspaceZip();
- }}
- style={{ fontWeight: "bold" }}
- >
- {`${i18n.terms.download} '${workspacePromise.data?.descriptor.name}'`}
- </AlertActionLink>
- )) || (
- <PushToGitAlertActionLinks
- changeGitAuthSessionId={changeGitAuthSessionId}
- workspaceDescriptor={workspacePromise.data?.descriptor}
- canPush={switchExpression(workspacePromise.data?.descriptor.origin.kind as WorkspaceKindGitBased, {
- BITBUCKET_SNIPPET: canUpdateGistOrSnippet,
- GITHUB_GIST: canUpdateGistOrSnippet,
- GIT: canPushToGitRepository,
- })}
- remoteRef={`${GIT_ORIGIN_REMOTE_NAME}/${workspacePromise.data?.descriptor.origin.branch}`}
- authSessionSelectFilter={authSessionSelectFilter}
- onPush={() => {
- navigationStatusToggle.unblock();
- switchExpression(workspacePromise.data?.descriptor.origin.kind as WorkspaceKindGitBased, {
- BITBUCKET_SNIPPET: updateGistOrSnippet,
- GITHUB_GIST: updateGistOrSnippet,
- GIT: pushToGitRepository,
- })();
- }}
- />
- )}
- <br />
- <br />
- <AlertActionLink
- data-testid="unsaved-alert-close-without-save-button"
- onClick={() =>
- navigationBlockersBypass.execute(() => {
- history.push(lastBlockedLocation);
- })
- }
- >
- {i18n.editorPage.alerts.unsaved.proceedAnyway}
- </AlertActionLink>
- <br />
- <br />
- </>
- }
- >
- <br />
- <p>{i18n.editorPage.alerts.unsaved.message}</p>
- </Alert>
- ),
- [
- workspacePromise.data?.descriptor,
- i18n.editorPage.alerts.unsaved.titleLocal,
- i18n.editorPage.alerts.unsaved.titleGit,
- i18n.editorPage.alerts.unsaved.proceedAnyway,
- i18n.editorPage.alerts.unsaved.message,
- i18n.terms.download,
- navigationStatusToggle,
- changeGitAuthSessionId,
- canUpdateGistOrSnippet,
- canPushToGitRepository,
- authSessionSelectFilter,
- downloadWorkspaceZip,
- updateGistOrSnippet,
- pushToGitRepository,
- navigationBlockersBypass,
- history,
- ]
- )
- );
-
- useEffect(() => {
- if (navigationStatus.lastBlockedLocation) {
- confirmNavigationAlert.show({ lastBlockedLocation: navigationStatus.lastBlockedLocation });
- } else {
- confirmNavigationAlert.close();
- }
- }, [confirmNavigationAlert, navigationStatus]);
-
- const [isVsCodeDropdownOpen, setVsCodeDropdownOpen] = useState(false);
-
- const createRepositorySuccessAlert = useGlobalAlert<{ url: string }>(
- useCallback(({ close }, { url }) => {
- return (
- <Alert
- variant="success"
- title={`GitHub repository created.`}
- actionClose={<AlertActionCloseButton onClose={close} />}
- actionLinks={<AlertActionLink onClick={() => window.open(url, "_blank")}>{url}</AlertActionLink>}
- />
- );
- }, [])
- );
-
- const canSeeWorkspaceToolbar = useMemo(
- () =>
- workspacePromise.data?.descriptor.origin.kind !== WorkspaceKind.LOCAL || workspacePromise.data?.files.length > 1,
- [workspacePromise.data?.descriptor.origin.kind, workspacePromise.data?.files.length]
- );
-
- return (
- <PromiseStateWrapper
- promise={workspacePromise}
- resolved={(workspace) => {
- return (
- <>
- <PageSection type={"nav"} variant={"light"} padding={{ default: "noPadding" }}>
- {workspace && canSeeWorkspaceToolbar && (
- <Flex
- justifyContent={{ default: "justifyContentSpaceBetween" }}
- flexWrap={{ default: "nowrap" }}
- spaceItems={{ default: "spaceItemsMd" }}
- >
- <FlexItem style={{ minWidth: 0 }}>
- <Flex
- justifyContent={{ default: "justifyContentFlexStart" }}
- flexWrap={{ default: "nowrap" }}
- spaceItems={{ default: "spaceItemsSm" }}
- alignItems={{ default: "alignItemsCenter" }}
- >
- <FlexItem>
- <Button
- className={"kie-tools--masthead-hoverable"}
- variant={ButtonVariant.plain}
- onClick={() => history.push({ pathname: routes.home.path({}) })}
- >
- <AngleLeftIcon />
- </Button>
- </FlexItem>
- <FlexItem>
- <AuthSessionSelect
- title={`Select Git authentication for '${workspace.descriptor.name}'...`}
- isPlain={true}
- authSessionId={workspace.descriptor.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
- setAuthSessionId={(newAuthSessionId) => {
- changeGitAuthSessionId(newAuthSessionId, workspace.descriptor.gitAuthSessionId);
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- }}
- filter={authSessionSelectFilter}
- />
- </FlexItem>
- <FlexItem>
- <WorkspaceLabel descriptor={workspace.descriptor} />
- </FlexItem>
- <FlexItem
- style={{ minWidth: 0, padding: "0 8px 0 8px", flexShrink: 0 }}
- className={"kie-tools--masthead-hoverable"}
- >
- <FolderIcon style={{ marginRight: "8px", verticalAlign: "middle" }} />
- <div
- data-testid={"toolbar-title-workspace"}
- className={"kogito--editor__toolbar-name-container"}
- style={{ display: "inline-block", verticalAlign: "middle" }}
- >
- <Title
- aria-label={"EmbeddedEditorFile name"}
- headingLevel={"h3"}
- size={"md"}
- style={{
- fontStyle: "italic",
- }}
- >
- {workspace.descriptor.name}
- </Title>
- <TextInput
- ref={workspaceNameRef}
- type={"text"}
- aria-label={"Edit workspace name"}
- onKeyDown={onWorkspaceNameKeyDown}
- className={"kogito--editor__toolbar-subtitle"}
- onBlur={(e) => onRenameWorkspace(e.target.value)}
- style={{ fontStyle: "italic", top: "4px", height: "calc(100% - 8px)" }}
- />
- </div>
- </FlexItem>
- <FlexItem>
- <WorkspaceStatusIndicator workspace={workspace} />
- </FlexItem>
- </Flex>
- </FlexItem>
- {/*<Divider inset={{ default: "insetMd" }} isVertical={true} />*/}
- {workspace.descriptor.origin.kind === WorkspaceKind.GIT &&
- workspaceImportableUrl.type === UrlType.GITHUB_DOT_COM && (
- <FlexItem
- style={{
- minWidth: "137px",
- }}
- >
- <Dropdown
- className={"kie-tools--masthead-hoverable"}
- isPlain={true}
- onSelect={() => setVsCodeDropdownOpen(false)}
- isOpen={isVsCodeDropdownOpen}
- position={"right"}
- toggle={
- <DropdownToggle toggleIndicator={null} onToggle={setVsCodeDropdownOpen}>
- <Flex flexWrap={{ default: "nowrap" }}>
- <FlexItem
- style={{
- minWidth: 0 /* This is to make the flex parent not overflow horizontally */,
- }}
- >
- <Tooltip distance={5} position={"top-start"} content={workspace.descriptor.name}>
- <TextContent>
- <Text
- component={TextVariants.small}
- style={{
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- }}
- >
- <img
- style={{
- minWidth: "14px",
- maxWidth: "14px",
- marginTop: "-2px",
- verticalAlign: "middle",
- }}
- alt="vscode-logo-blue"
- src={routes.static.images.vscodeLogoBlue.path({})}
- />
-
- {`Open "${workspace.descriptor.name}"`}
- </Text>
- </TextContent>
- </Tooltip>
- </FlexItem>
- <FlexItem>
- <CaretDownIcon />
- </FlexItem>
- </Flex>
- </DropdownToggle>
- }
- dropdownItems={[
- <DropdownGroup key={"open-in-vscode"}>
- {navigationStatus.shouldBlockNavigationTo({ pathname: "__external" }) && (
- <>
- <Alert
- isInline={true}
- variant={"warning"}
- title={"You have new changes to Push"}
- actionLinks={
- <PushToGitAlertActionLinks
- changeGitAuthSessionId={changeGitAuthSessionId}
- workspaceDescriptor={workspace.descriptor}
- canPush={canPushToGitRepository}
- authSessionSelectFilter={authSessionSelectFilter}
- remoteRef={`${GIT_ORIGIN_REMOTE_NAME}/${workspace.descriptor.origin.branch}`}
- onPush={pushToGitRepository}
- />
- }
- >
- {`Opening '${workspace.descriptor.name}' on vscode.dev won't show your latest changes.`}
- </Alert>
- <Divider />
- </>
- )}
- <DropdownItem
- style={{ minWidth: "400px" }}
- href={`https://vscode.dev/github${
- new URL(workspace.descriptor.origin.url).pathname.endsWith(".git")
- ? new URL(workspace.descriptor.origin.url).pathname.replace(".git", "")
- : new URL(workspace.descriptor.origin.url).pathname
- }/tree/${workspace.descriptor.origin.branch}`}
- target={"_blank"}
- icon={<ExternalLinkAltIcon />}
- description={`The '${workspace.descriptor.origin.branch}' branch will be opened.`}
- >
- vscode.dev
- </DropdownItem>
- <Divider />
- <DropdownItem
- href={`vscode://vscode.git/clone?url=${workspace.descriptor.origin.url.toString()}`}
- target={"_blank"}
- icon={<ExternalLinkAltIcon />}
- description={"The default branch will be opened."}
- >
- VS Code Desktop
- </DropdownItem>
- </DropdownGroup>,
- ]}
- />
- </FlexItem>
- )}
- </Flex>
- )}
- </PageSection>
- <PageSection type={"nav"} variant={"light"} style={{ paddingTop: 0, paddingBottom: "16px" }}>
- <Flex
- justifyContent={{ default: "justifyContentSpaceBetween" }}
- alignItems={{ default: "alignItemsCenter" }}
- flexWrap={{ default: "nowrap" }}
- >
- <FlexItem style={{ minWidth: 0 }}>
- <PageHeaderToolsItem visibility={{ default: "visible" }}>
- <Flex flexWrap={{ default: "nowrap" }} alignItems={{ default: "alignItemsCenter" }}>
- <FlexItem style={{ minWidth: 0 }}>
- <FileSwitcher workspace={workspace} workspaceFile={props.workspaceFile} />
- </FlexItem>
- <FlexItem>
- {(isEdited && (
- <Tooltip content={"Saving in memory..."} position={"bottom"}>
- <TextContent
- style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}
- >
- <Text
- style={{ display: "flex" }}
- aria-label={"Saving in memory..."}
- data-testid="is-saving-in-memory-indicator"
- component={TextVariants.small}
- >
- <OutlinedClockIcon size={"sm"} style={{ margin: 0 }} />
- </Text>
- </TextContent>
- </Tooltip>
- )) || (
- <Tooltip content={"File is in memory."} position={"bottom"}>
- <TextContent
- style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}
- >
- <Text
- style={{ display: "flex" }}
- aria-label={"File is in memory."}
- data-testid="is-saved-in-memory-indicator"
- component={TextVariants.small}
- >
- <DesktopIcon size={"sm"} style={{ margin: 0 }} />
- </Text>
- </TextContent>
- </Tooltip>
- )}
- </FlexItem>
- <FlexItem>
- {(!isSaved && (
- <Tooltip content={"Writing file..."} position={"bottom"}>
- <TextContent
- style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}
- >
- <Text
- style={{ display: "flex" }}
- aria-label={"Writing file..."}
- data-testid="is-writing-indicator"
- component={TextVariants.small}
- >
- <OutlinedClockIcon size={"sm"} style={{ margin: 0 }} />
- </Text>
- </TextContent>
- </Tooltip>
- )) || (
- <Tooltip content={"File is written on disk."} position={"bottom"}>
- <TextContent
- style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}
- >
- <Text
- style={{ display: "flex" }}
- aria-label={"File is written on disk."}
- data-testid="is-written-indicator"
- component={TextVariants.small}
- >
- <OutlinedHddIcon size={"sm"} style={{ margin: 0 }} />
- </Text>
- </TextContent>
- </Tooltip>
- )}
- </FlexItem>
- </Flex>
- </PageHeaderToolsItem>
- </FlexItem>
- <FlexItem>
- <Toolbar>
- <ToolbarContent style={{ paddingRight: 0 }}>
- <ToolbarGroup>
- <ToolbarItem>
- <ResponsiveDropdown
- title={"Add file"}
- onClose={() => setNewFileDropdownMenuOpen(false)}
- position={"right"}
- isOpen={isNewFileDropdownMenuOpen}
- toggle={
- <ResponsiveDropdownToggle
- onToggle={() => setNewFileDropdownMenuOpen((prev) => !prev)}
- isPrimary={true}
- toggleIndicator={CaretDownIcon}
- >
- <PlusIcon />
- New file
- </ResponsiveDropdownToggle>
- }
- >
- <NewFileDropdownMenu
- workspaceDescriptor={workspace.descriptor}
- destinationDirPath={props.workspaceFile.relativeDirPath}
- onAddFile={async (file) => {
- setNewFileDropdownMenuOpen(false);
- if (!file) {
- return;
- }
-
- history.push({
- pathname: routes.workspaceWithFilePath.path({
- workspaceId: file.workspaceId,
- fileRelativePath: file.relativePathWithoutExtension,
- extension: file.extension,
- }),
- });
- }}
- />
- </ResponsiveDropdown>
- </ToolbarItem>
- <ToolbarItem visibility={hideWhenSmall}>
- {props.workspaceFile.extension === "dmn" && (
- <ToolbarGroup>
- <KieSandboxExtendedServicesButtons
- workspace={workspace}
- workspaceFile={props.workspaceFile}
- editorPageDock={props.editorPageDock}
- />
- </ToolbarGroup>
- )}
- </ToolbarItem>
- {isGistLikeWorkspaceKind(workspace.descriptor.origin.kind) && (
- <ToolbarItem>
- <Dropdown
- onSelect={() => setSyncGistOrSnippetDropdownOpen(false)}
- isOpen={isSyncGistOrSnippetDropdownOpen}
- position={DropdownPosition.right}
- toggle={
- <DropdownToggle
- id={"sync-dropdown"}
- data-testid={"sync-dropdown"}
- onToggle={(isOpen) => setSyncGistOrSnippetDropdownOpen(isOpen)}
- >
- Sync
- </DropdownToggle>
- }
- dropdownItems={[
- <DropdownGroup key={"sync-gist-or-snippet-dropdown-group"}>
- <Tooltip
- data-testid={"gist-or-snippet-it-tooltip"}
- content={
- <div>
- {switchExpression(workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
- GITHUB_GIST: i18n.editorToolbar.cantUpdateGistTooltip,
- BITBUCKET_SNIPPET: i18n.editorToolbar.cantUpdateSnippetTooltip,
- })}
- </div>
- }
- trigger={!canUpdateGistOrSnippet ? "mouseenter click" : ""}
- position="left"
- >
- <>
- <DropdownItem
- style={{ minWidth: "300px" }}
- icon={switchExpression(
- workspace.descriptor.origin.kind as WorkspaceKindGistLike,
- {
- BITBUCKET_SNIPPET: <BitbucketIcon />,
- GITHUB_GIST: <GithubIcon />,
- }
- )}
- onClick={updateGistOrSnippet}
- isDisabled={!canUpdateGistOrSnippet}
- >
- Update{" "}
- {switchExpression(workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
- BITBUCKET_SNIPPET: "Bitbucket Snippet",
- GITHUB_GIST: "GitHub Gist",
- })}
- </DropdownItem>
- {canForkGitHubGist && (
- <>
- <Divider />
- <li role="menuitem">
- <Alert
- isInline={true}
- variant={"default"}
- title={
- <span style={{ whiteSpace: "nowrap" }}>
- {"Can't update Gists you don't own"}
- </span>
- }
- >
- <br />
- {`You can create a fork of '${workspace.descriptor.name}' to save your updates.`}
- <br />
- <br />
- <Button
- onClick={forkGitHubGist}
- variant={ButtonVariant.link}
- isSmall={true}
- style={{ paddingLeft: 0 }}
- >
- {`Fork Gist`}
- </Button>
- <br />
- <br />
- {`Or you can change the authentication source for '${workspace.descriptor.name}' to be able to Update Gist.`}
- <br />
- <br />
- <AuthSessionSelect
- title={`Select Git authentication for '${workspace.descriptor.name}'...`}
- isPlain={false}
- authSessionId={workspace.descriptor.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={
- AuthProviderGroup.GIT
- }
- setAuthSessionId={(newAuthSessionId) => {
- changeGitAuthSessionId(
- newAuthSessionId,
- workspace.descriptor.gitAuthSessionId
- );
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- setTimeout(() => {
- setSyncGistOrSnippetDropdownOpen(true);
- }, 0);
- }}
- filter={authSessionSelectFilter}
- />
- </Alert>
- </li>
- </>
- )}
- {!canPushToGitRepository && (
- <>
- <Divider />
- <Alert
- isInline={true}
- variant={"default"}
- title={`Can't Update ${switchExpression(
- authProvider?.type as GitAuthProviderType,
- {
- github: "GitHub repository",
- bitbucket: "Bitbucket repository",
- default: "Git repository",
- }
- )} without selecting a matching authentication source`}
- actionLinks={
- <AuthSessionSelect
- title={`Select Git authentication for '${workspace.descriptor.name}'...`}
- isPlain={false}
- authSessionId={workspace.descriptor.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={
- AuthProviderGroup.GIT
- }
- setAuthSessionId={(newAuthSessionId) => {
- changeGitAuthSessionId(
- newAuthSessionId,
- workspace.descriptor.gitAuthSessionId
- );
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- setTimeout(() => {
- setSyncGistOrSnippetDropdownOpen(true);
- }, 0);
- }}
- filter={authSessionSelectFilter}
- />
- }
- >
- {`Select an authentication source for '${
- workspace.descriptor.name
- }' to be able to Update ${switchExpression(
- workspace.descriptor.origin.kind as WorkspaceKindGistLike,
- {
- GITHUB_GIST: "Github Gist",
- BITBUCKET_SNIPPET: "Bitbucket Snippet",
- }
- )}.`}
- </Alert>
- </>
- )}
- </>
- </Tooltip>
- </DropdownGroup>,
- ]}
- />
- </ToolbarItem>
- )}
- {workspace.descriptor.origin.kind === WorkspaceKind.GIT && (
- <ToolbarItem>
- <Dropdown
- onSelect={() => setSyncGitRepositoryDropdownOpen(false)}
- isOpen={isSyncGitRepositoryDropdownOpen}
- position={DropdownPosition.right}
- toggle={
- <DropdownToggle
- id={"sync-dropdown"}
- data-testid={"sync-dropdown"}
- onToggle={(isOpen) => setSyncGitRepositoryDropdownOpen(isOpen)}
- >
- Sync
- </DropdownToggle>
- }
- dropdownItems={[
- <DropdownGroup key={"sync-git-dropdown-group"}>
- <DropdownItem
- icon={<SyncAltIcon />}
- onClick={() => pullFromGitRepository({ showAlerts: true })}
- description={`Get new changes made upstream at '${GIT_ORIGIN_REMOTE_NAME}/${workspace.descriptor.origin.branch}'.`}
- >
- Pull
- </DropdownItem>
- <Tooltip
- data-testid={"git-it-tooltip"}
- content={
- <div>{`You need to select an authentication source to Push to this repository.`}</div>
- }
- trigger={!canPushToGitRepository ? "mouseenter click" : ""}
- position="left"
- >
- <>
- <DropdownItem
- icon={<ArrowCircleUpIcon />}
- onClick={pushToGitRepository}
- isDisabled={!canPushToGitRepository}
- description={`Send your changes upstream to '${GIT_ORIGIN_REMOTE_NAME}/${workspace.descriptor.origin.branch}'.`}
- >
- Push
- </DropdownItem>
- {!canPushToGitRepository && (
- <>
- <Alert
- isInline={true}
- variant={"default"}
- title={"Can't Push without selecting an authentication source"}
- actionLinks={
- <AuthSessionSelect
- title={`Select Git authentication for '${workspace.descriptor.name}'...`}
- isPlain={false}
- authSessionId={workspace.descriptor.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={
- AuthProviderGroup.GIT
- }
- setAuthSessionId={(newAuthSessionId) => {
- changeGitAuthSessionId(
- newAuthSessionId,
- workspace.descriptor.gitAuthSessionId
- );
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- setTimeout(() => {
- setSyncGitRepositoryDropdownOpen(true);
- });
- }}
- filter={authSessionSelectFilter}
- />
- }
- >
- {`Select an authentication source for '${workspace.descriptor.name}' to be able to Push.`}
- </Alert>
- </>
- )}
- </>
- </Tooltip>
- </DropdownGroup>,
- ]}
- />
- </ToolbarItem>
- )}
- <ToolbarItem visibility={hideWhenSmall}>
- <Dropdown
- onSelect={() => setShareDropdownOpen(false)}
- isOpen={isShareDropdownOpen}
- dropdownItems={shareDropdownItems}
- position={DropdownPosition.right}
- toggle={
- <DropdownToggle
- id={"share-dropdown"}
- data-testid={"share-dropdown"}
- onToggle={(isOpen) => setShareDropdownOpen(isOpen)}
- >
- {i18n.editorToolbar.share}
- </DropdownToggle>
- }
- />
- </ToolbarItem>
- <ToolbarItem visibility={hideWhenSmall} style={{ marginRight: 0 }}>
- <KebabDropdown
- id={"kebab-lg"}
- state={[isLargeKebabOpen, setLargeKebabOpen]}
- items={[deleteFileDropdownItem, <Divider key={"divider-0"} />, createSavePointDropdownItem]}
- />
- </ToolbarItem>
- <ToolbarItem visibility={showWhenSmall} style={{ marginRight: 0 }}>
- <KebabDropdown
- id={"kebab-sm"}
- state={[isSmallKebabOpen, setSmallKebabOpen]}
- items={[
- deleteFileDropdownItem,
- <Divider key={"divider-0"} />,
- createSavePointDropdownItem,
- <Divider key={"divider-1"} />,
- ...shareDropdownItems,
- ...(props.workspaceFile.extension !== "dmn"
- ? []
- : [
- <Divider key={"divider-2"} />,
- <KieSandboxExtendedServicesDropdownGroup
- workspace={workspace}
- key="kie-sandbox-extended-services-group"
- />,
- ]),
- ]}
- />
- </ToolbarItem>
- </ToolbarGroup>
- </ToolbarContent>
- </Toolbar>
- </FlexItem>
- </Flex>
- </PageSection>
- <EmbedModal
- workspace={workspace.descriptor}
- workspaceFile={props.workspaceFile}
- isOpen={isEmbedModalOpen}
- onClose={() => setEmbedModalOpen(false)}
- />
- <CreateGitRepositoryModal
- workspace={workspace.descriptor}
- isOpen={isCreateGitRepositoryModalOpen}
- onClose={() => setCreateGitRepositoryModalOpen(false)}
- onSuccess={({ url }) => {
- createRepositorySuccessAlert.show({ url });
- }}
- />
- <CreateGistOrSnippetModal
- workspace={workspace.descriptor}
- isOpen={isCreateGistOrSnippetModalOpen}
- onClose={() => setCreateGistOrSnippetModalOpen(false)}
- onSuccess={({ url }) => {
- successfullyCreatedGistOrSnippetAlert.show({ url });
- }}
- onError={() => {
- errorAlert.show();
- }}
- />
- <textarea ref={copyContentTextArea} style={{ height: 0, position: "absolute", zIndex: -1 }} />
- <a ref={downloadRef} />
- <a ref={downloadAllRef} />
- <a ref={downloadPreviewRef} />
- </>
- );
- }}
- />
- );
-}
-
-export function PushToGitAlertActionLinks(props: {
- onPush: () => void;
- canPush?: boolean;
- remoteRef?: string;
- workspaceDescriptor: WorkspaceDescriptor | undefined;
- changeGitAuthSessionId: (a: React.SetStateAction<string | undefined>, b: string | undefined) => void;
- authSessionSelectFilter: AuthSessionSelectFilter;
-}) {
- const accountsDispatch = useAccountsDispatch();
- if (props.workspaceDescriptor?.origin.kind === WorkspaceKind.GIT && !props.remoteRef) {
- throw new Error("Should specify remoteRef for GIT workspaces");
- }
-
- const pushButton = useMemo(
- () => (
- <AlertActionLink onClick={props.onPush} style={{ fontWeight: "bold" }} isDisabled={!props.canPush}>
- {props.workspaceDescriptor
- ? switchExpression(props.workspaceDescriptor.origin.kind, {
- GIT: `Push to '${props.remoteRef}'`,
- GITHUB_GIST: "Update Gist",
- BITBUCKET_SNIPPET: "Update Snippet",
- default: "",
- })
- : ""}
- </AlertActionLink>
- ),
- [props]
- );
-
- return (
- <>
- {!props.canPush && (
- <Alert
- isInline={true}
- variant={"default"}
- title={"Can't Push without selecting an authentication source"}
- actionLinks={
- <>
- <AuthSessionSelect
- title={`Select Git authentication for '${props.workspaceDescriptor?.name}'...`}
- position={SelectPosition.right}
- isPlain={false}
- authSessionId={props.workspaceDescriptor?.gitAuthSessionId}
- showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
- setAuthSessionId={(newAuthSessionId) => {
- props.changeGitAuthSessionId(newAuthSessionId, props.workspaceDescriptor?.gitAuthSessionId);
- accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
- }}
- filter={props.authSessionSelectFilter}
- />
- <br />
- <br />
- {pushButton}
- </>
- }
- >
- {`Select an authentication source for '${props.workspaceDescriptor?.name}' to be able to Push.`}
- <br />
- </Alert>
- )}
- {props.canPush && pushButton}
- </>
- );
-}
-
-export function KebabDropdown(props: {
- id: string;
- items: React.ReactNode[];
- state: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
-}) {
- return (
- <Dropdown
- className={"kie-tools--masthead-hoverable"}
- isOpen={props.state[0]}
- isPlain={true}
- position={DropdownPosition.right}
- onSelect={() => props.state[1](false)}
- toggle={
- <DropdownToggle
- id={props.id}
- toggleIndicator={null}
- onToggle={(isOpen) => props.state[1](isOpen)}
- ouiaId={props.id}
- >
- <EllipsisVIcon />
- </DropdownToggle>
- }
- dropdownItems={props.items}
- />
- );
-}
diff --git a/packages/serverless-workflow-diagram-editor/sw-editor/sw-editor-client/src/main/java/org/kie/workbench/common/stunner/sw/client/shapes/icons/IconPosition.java b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIcon.tsx
similarity index 58%
copy from packages/serverless-workflow-diagram-editor/sw-editor/sw-editor-client/src/main/java/org/kie/workbench/common/stunner/sw/client/shapes/icons/IconPosition.java
copy to packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIcon.tsx
index c76fe691f8..568f1ecb9d 100644
--- a/packages/serverless-workflow-diagram-editor/sw-editor/sw-editor-client/src/main/java/org/kie/workbench/common/stunner/sw/client/shapes/icons/IconPosition.java
+++ b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIcon.tsx
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,12 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.kie.workbench.common.stunner.sw.client.shapes.icons;
-import com.ait.lienzo.client.core.types.Point2D;
+import React from "react";
+import { Icon } from "@patternfly/react-core/dist/js/components/Icon";
-public interface IconPosition {
+type Props = {
+ iconUrl?: string;
+};
- Point2D RIGHT_TOP_CORNER = new Point2D(224, 6);
- Point2D LEFT_FROM_RIGHT_TOP_CORNER = new Point2D(202, 6);
+export function AcceleratorIcon(props: Props) {
+ return props.iconUrl ? (
+ <Icon isInline style={{ verticalAlign: "middle" }}>
+ <img src={props.iconUrl} />
+ </Icon>
+ ) : (
+ <span>🚀</span>
+ );
}
diff --git a/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIndicator.tsx b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIndicator.tsx
new file mode 100644
index 0000000000..ee6b795d7f
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorIndicator.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState } from "react";
+import { useCurrentAccelerator } from "../../../accelerators/AcceleratorsHooks";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
+import { AcceleratorModal } from "./AcceleratorModal";
+import { AcceleratorIcon } from "./AcceleratorIcon";
+
+type Props = {
+ workspaceId: string;
+};
+
+export function AcceleratorIndicator(props: Props) {
+ const [isAcceleratorDetailsModalOpen, setAcceleratorDetailsModalOpen] = useState(false);
+ const currentAccelerator = useCurrentAccelerator(props.workspaceId);
+
+ if (!currentAccelerator) {
+ return <></>;
+ }
+
+ return (
+ <Tooltip
+ position={"right"}
+ content={
+ <>
+ <AcceleratorIcon iconUrl={currentAccelerator.iconUrl} />
+
+ {currentAccelerator.name} Accelerator
+ </>
+ }
+ >
+ <>
+ <Button
+ variant={ButtonVariant.plain}
+ onClick={() => setAcceleratorDetailsModalOpen(true)}
+ className={"kie-tools--masthead-hoverable"}
+ >
+ <AcceleratorIcon iconUrl={currentAccelerator.iconUrl} />
+ </Button>
+ <AcceleratorModal
+ isOpen={isAcceleratorDetailsModalOpen}
+ onClose={() => setAcceleratorDetailsModalOpen(false)}
+ accelerator={currentAccelerator}
+ />
+ </>
+ </Tooltip>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorModal.tsx b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorModal.tsx
new file mode 100644
index 0000000000..b0f3982563
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorModal.tsx
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import { AcceleratorAppliedConfig } from "../../../accelerators/AcceleratorsApi";
+import { useOnlineI18n } from "../../../i18n";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
+import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid";
+import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import { AcceleratorIcon } from "./AcceleratorIcon";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+
+type Props = {
+ accelerator: AcceleratorAppliedConfig;
+ isOpen: boolean;
+ onClose: () => any;
+ isApplying?: boolean;
+ onApplyAccelerator?: () => void;
+};
+
+export function AcceleratorModal(props: Props) {
+ const { i18n } = useOnlineI18n();
+
+ return (
+ <Modal
+ isOpen={props.isOpen}
+ onClose={props.onClose}
+ aria-label="Accelerator"
+ variant={ModalVariant.medium}
+ actions={
+ props.isApplying && [
+ <Button key="apply" variant="primary" onClick={props.onApplyAccelerator}>
+ {i18n.terms.apply}
+ </Button>,
+ <Button key="cancel" variant="link" onClick={props.onClose}>
+ {i18n.terms.cancel}
+ </Button>,
+ ]
+ }
+ >
+ <Title headingLevel="h1">
+ <AcceleratorIcon iconUrl={props.accelerator.iconUrl} />
+
+ {props.accelerator.name} Accelerator
+ </Title>
+ <Grid style={{ margin: "1rem 0" }} hasGutter>
+ {props.isApplying && (
+ <GridItem span={12}>
+ <i>{i18n.accelerators.acceleratorDescription}</i>
+ </GridItem>
+ )}
+ <GridItem span={12}>
+ <p>
+ {i18n.accelerators.acceleratorDetails}
+
+ <a href={props.accelerator.gitRepositoryUrl} target="__blank">
+ {props.accelerator.gitRepositoryUrl}
+ </a>
+ </p>
+ Git ref:{" "}
+ <small>
+ <pre style={{ display: "inline" }}>@{props.accelerator.gitRepositoryGitRef}</pre>
+ </small>
+ </GridItem>
+ <Divider />
+ <GridItem span={6}>
+ {props.isApplying ? i18n.accelerators.dmnFilesMove : i18n.accelerators.dmnFilesLocation}
+ </GridItem>
+ <GridItem span={6}>
+ <pre>{props.accelerator.dmnDestinationFolder}</pre>
+ </GridItem>
+ <GridItem span={6}>
+ {props.isApplying ? i18n.accelerators.pmmlFilesMove : i18n.accelerators.pmmlFilesLocation}
+ </GridItem>
+ <GridItem span={6}>
+ <pre>{props.accelerator.dmnDestinationFolder}</pre>
+ </GridItem>
+ <GridItem span={6}>
+ {props.isApplying ? i18n.accelerators.bpmnFilesMove : i18n.accelerators.bpmnFilesLocation}
+ </GridItem>
+ <GridItem span={6}>
+ <pre>{props.accelerator.bpmnDestinationFolder}</pre>
+ </GridItem>
+ <GridItem span={6}>
+ {props.isApplying ? i18n.accelerators.otherFilesMove : i18n.accelerators.otherFilesLocation}
+ </GridItem>
+ <GridItem span={6}>
+ <pre>{props.accelerator.otherFilesDestinationFolder}</pre>
+ </GridItem>
+ {!props.isApplying && props.accelerator.appliedAt && (
+ <>
+ <Divider />
+ <GridItem span={12}>
+ <i>
+ {i18n.accelerators.appliedAt} {new Date(props.accelerator.appliedAt).toLocaleString()}
+ </i>
+ </GridItem>
+ </>
+ )}
+ {props.isApplying && (
+ <>
+ <Divider />
+ <GridItem span={12}>
+ <i>{i18n.accelerators.applyDisclaimer}</i>
+ </GridItem>
+ </>
+ )}
+ </Grid>
+ </Modal>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorsDropdown.tsx b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorsDropdown.tsx
new file mode 100644
index 0000000000..8c535c31a1
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Accelerators/AcceleratorsDropdown.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useState } from "react";
+import { ResponsiveDropdown } from "../../../ResponsiveDropdown/ResponsiveDropdown";
+import { ResponsiveDropdownToggle } from "../../../ResponsiveDropdown/ResponsiveDropdownToggle";
+import { useEditorToolbarContext, useEditorToolbarDispatchContext } from "../EditorToolbarContextProvider";
+import CaretDownIcon from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
+import {
+ useAcceleratorsDispatch,
+ useAvailableAccelerators,
+ useCurrentAccelerator,
+} from "../../../accelerators/AcceleratorsHooks";
+import { DropdownItem } from "@patternfly/react-core/dist/js/components/Dropdown";
+import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { AcceleratorConfig } from "../../../accelerators/AcceleratorsApi";
+import { useOnlineI18n } from "../../../i18n";
+import { AcceleratorModal } from "./AcceleratorModal";
+import { AcceleratorIcon } from "./AcceleratorIcon";
+
+type Props = {
+ workspaceFile: WorkspaceFile;
+};
+
+export function AcceleratorsDropdown(props: Props) {
+ const { i18n } = useOnlineI18n();
+ const { isAcceleratorsDropdownOpen, workspace } = useEditorToolbarContext();
+ const { setAcceleratorsDropdownOpen } = useEditorToolbarDispatchContext();
+ const accelerators = useAvailableAccelerators();
+ const { applyAcceleratorToWorkspace } = useAcceleratorsDispatch(workspace);
+ const [isApplyModalOpen, setApplyModalOpen] = useState(false);
+ const [selectedAccelerator, setSelectedAccelerator] = useState<AcceleratorConfig | undefined>();
+
+ const currentAccelerator = useCurrentAccelerator(props.workspaceFile.workspaceId);
+
+ const onOpenApplyAccelerator = useCallback(
+ (accelerator: AcceleratorConfig) => {
+ setAcceleratorsDropdownOpen(false);
+ setSelectedAccelerator(accelerator);
+ setApplyModalOpen(true);
+ },
+ [setAcceleratorsDropdownOpen]
+ );
+
+ const onApplyAccelerator = useCallback(() => {
+ if (selectedAccelerator) {
+ applyAcceleratorToWorkspace(selectedAccelerator, props.workspaceFile);
+ }
+ setSelectedAccelerator(undefined);
+ setApplyModalOpen(false);
+ }, [applyAcceleratorToWorkspace, props.workspaceFile, selectedAccelerator]);
+
+ if (currentAccelerator) {
+ return <></>;
+ }
+
+ return (
+ <>
+ <ResponsiveDropdown
+ title="Accelerators"
+ className={"kie-tools--masthead-hoverable"}
+ isPlain={true}
+ onClose={() => setAcceleratorsDropdownOpen(false)}
+ position={"right"}
+ isOpen={isAcceleratorsDropdownOpen}
+ toggle={
+ <ResponsiveDropdownToggle
+ onToggle={() => setAcceleratorsDropdownOpen((prev) => !prev)}
+ toggleIndicator={CaretDownIcon}
+ icon={<i>🚀</i>}
+ >
+ {i18n.accelerators.applyAccelerator}
+ </ResponsiveDropdownToggle>
+ }
+ dropdownItems={accelerators.map((accelerator) => (
+ <DropdownItem
+ key={accelerator.name}
+ icon={<AcceleratorIcon iconUrl={accelerator.iconUrl} />}
+ onClick={() => onOpenApplyAccelerator(accelerator)}
+ >
+ {accelerator.name}...
+ </DropdownItem>
+ ))}
+ />
+ {selectedAccelerator && (
+ <AcceleratorModal
+ isOpen={isApplyModalOpen}
+ onClose={() => setApplyModalOpen(false)}
+ onApplyAccelerator={onApplyAccelerator}
+ accelerator={selectedAccelerator}
+ isApplying={true}
+ />
+ )}
+ </>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/EditorToolbar.tsx b/packages/online-editor/src/editor/Toolbar/EditorToolbar.tsx
new file mode 100644
index 0000000000..b173250ace
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/EditorToolbar.tsx
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useEffect, useMemo, useRef } from "react";
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownPosition,
+ DropdownToggle,
+} from "@patternfly/react-core/dist/js/components/Dropdown";
+import {
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem,
+ ToolbarItemProps,
+} from "@patternfly/react-core/dist/js/components/Toolbar";
+import { SaveIcon } from "@patternfly/react-icons/dist/js/icons/save-icon";
+import { useOnlineI18n } from "../../i18n";
+import { KieSandboxExtendedServicesButtons } from "../KieSandboxExtendedServices/KieSandboxExtendedServicesButtons";
+import { useRoutes } from "../../navigation/Hooks";
+import { EmbeddedEditorRef } from "@kie-tools-core/editor/dist/embedded";
+import { useHistory } from "react-router";
+import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { PlusIcon } from "@patternfly/react-icons/dist/js/icons/plus-icon";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { NewFileDropdownMenu } from "./NewFileDropdownMenu";
+import { PageHeaderToolsItem, PageSection } from "@patternfly/react-core/dist/js/components/Page";
+import { FileLabel } from "../../filesList/FileLabel";
+import {
+ useWorkspaceGitStatusPromise,
+ useWorkspacePromise,
+ WorkspaceGitStatusType,
+} from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks";
+import { FileSwitcher } from "./FileSwitcher";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { KieSandboxExtendedServicesDropdownGroup } from "../KieSandboxExtendedServices/KieSandboxExtendedServicesDropdownGroup";
+import { TrashIcon } from "@patternfly/react-icons/dist/js/icons/trash-icon";
+import { CaretDownIcon } from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
+import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { EditorPageDockDrawerRef } from "../EditorPageDockDrawer";
+import { useEditorEnvelopeLocator } from "../../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
+import { ResponsiveDropdown } from "../../ResponsiveDropdown/ResponsiveDropdown";
+import { ResponsiveDropdownToggle } from "../../ResponsiveDropdown/ResponsiveDropdownToggle";
+import { useAuthSession } from "../../authSessions/AuthSessionsContext";
+import { KebabDropdown } from "./KebabDropdown";
+import { VsCodeDropdownMenu } from "./VsCodeDropdownMenu";
+import { DownloadDropdownGroup } from "./Share/DownloadDropdownGroup";
+import { GitDropdownGroup } from "./Share/GitDropdownGroup";
+import { EmbedDropdownGroup } from "./Share/EmbedDropdownGroup";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { useGitIntegration } from "./GitIntegration/GitIntegrationContextProvider";
+import {
+ EditorToolbarContextProvider,
+ useEditorToolbarContext,
+ useEditorToolbarDispatchContext,
+} from "./EditorToolbarContextProvider";
+import { WorkspaceToolbar } from "./Workspace/WorkspaceToolbar";
+import { useWorkspaceNavigationBlocker } from "./Workspace/Hooks";
+import { FileStatus } from "./FileStatus";
+import { SyncDropdownMenu } from "./SyncDropdownMenu";
+import { AcceleratorsDropdown } from "./Accelerators/AcceleratorsDropdown";
+import { listDeletedFiles } from "../../workspace/components/WorkspaceStatusIndicator";
+import { PromiseState } from "@kie-tools-core/react-hooks/dist/PromiseState";
+
+export interface Props {
+ editor: EmbeddedEditorRef | undefined;
+ workspaceFile: WorkspaceFile;
+ editorPageDock: EditorPageDockDrawerRef | undefined;
+}
+
+const showWhenSmall: ToolbarItemProps["visibility"] = {
+ default: "visible",
+ "2xl": "hidden",
+ xl: "hidden",
+ lg: "visible",
+ md: "visible",
+};
+
+const hideWhenSmall: ToolbarItemProps["visibility"] = {
+ default: "hidden",
+ "2xl": "visible",
+ xl: "visible",
+ lg: "hidden",
+ md: "hidden",
+};
+
+const hideWhenTiny: ToolbarItemProps["visibility"] = {
+ default: "hidden",
+ "2xl": "visible",
+ xl: "visible",
+ lg: "visible",
+ md: "hidden",
+};
+
+export function EditorToolbarWithWorkspace(
+ props: Props & { workspace: ActiveWorkspace; workspaceGitStatusPromise: PromiseState<WorkspaceGitStatusType> }
+) {
+ const routes = useRoutes();
+ const editorEnvelopeLocator = useEditorEnvelopeLocator();
+ const history = useHistory();
+ const workspaces = useWorkspaces();
+ const { i18n } = useOnlineI18n();
+ const copyContentTextArea = useRef<HTMLTextAreaElement>(null);
+
+ const { alerts } = useGitIntegration();
+
+ const { isNewFileDropdownMenuOpen, isShareDropdownOpen, isLargeKebabOpen, isSmallKebabOpen } =
+ useEditorToolbarContext();
+
+ const { setNewFileDropdownMenuOpen, setShareDropdownOpen, setLargeKebabOpen, setSmallKebabOpen } =
+ useEditorToolbarDispatchContext();
+
+ useWorkspaceNavigationBlocker(props.workspace);
+
+ const { gitConfig } = useAuthSession(props.workspace.descriptor.gitAuthSessionId);
+
+ const shareDropdownItems = useMemo(
+ () => [
+ <DownloadDropdownGroup
+ editor={props.editor}
+ workspaceFile={props.workspaceFile}
+ workspace={props.workspace}
+ key="download-dropdown-group"
+ />,
+ <EmbedDropdownGroup workspaceFile={props.workspaceFile} workspace={props.workspace} key="embed-dropdown-group" />,
+ <GitDropdownGroup workspace={props.workspace} key="git-dropdown-group" />,
+ ],
+ [props.editor, props.workspaceFile, props.workspace]
+ );
+
+ const handleDeletedWorkspaceFile = useCallback(() => {
+ const nextFile = props.workspace.files
+ .filter((f) => {
+ return (
+ f.relativePath !== props.workspaceFile.relativePath && editorEnvelopeLocator.hasMappingFor(f.relativePath)
+ );
+ })
+ .pop();
+ if (!nextFile) {
+ history.push({ pathname: routes.home.path({}) });
+ return;
+ }
+
+ history.push({
+ pathname: routes.workspaceWithFilePath.path({
+ workspaceId: nextFile.workspaceId,
+ fileRelativePath: nextFile.relativePathWithoutExtension,
+ extension: nextFile.extension,
+ }),
+ });
+ }, [
+ editorEnvelopeLocator,
+ history,
+ props.workspace.files,
+ props.workspaceFile.relativePath,
+ routes.home,
+ routes.workspaceWithFilePath,
+ ]);
+
+ const deleteWorkspaceFile = useCallback(async () => {
+ if (props.workspace.files.length === 1) {
+ await workspaces.deleteWorkspace({ workspaceId: props.workspaceFile.workspaceId });
+ history.push({ pathname: routes.home.path({}) });
+ return;
+ }
+
+ await workspaces.deleteFile({
+ file: props.workspaceFile,
+ });
+
+ handleDeletedWorkspaceFile();
+ }, [props.workspace.files.length, props.workspaceFile, workspaces, handleDeletedWorkspaceFile, history, routes.home]);
+
+ const deleteFileDropdownItem = useMemo(() => {
+ return (
+ <DropdownItem key={"delete-dropdown-item"} onClick={deleteWorkspaceFile}>
+ <Flex flexWrap={{ default: "nowrap" }}>
+ <FlexItem>
+ <TrashIcon />
+ Delete <b>{`"${props.workspaceFile.nameWithoutExtension}"`}</b>
+ </FlexItem>
+ <FlexItem>
+ <b>
+ <FileLabel extension={props.workspaceFile.extension} />
+ </b>
+ </FlexItem>
+ </Flex>
+ </DropdownItem>
+ );
+ }, [deleteWorkspaceFile, props.workspaceFile]);
+
+ const createSavePointDropdownItem = useMemo(() => {
+ return (
+ <DropdownItem
+ key={"commit-dropdown-item"}
+ icon={<SaveIcon />}
+ onClick={async () => {
+ if (!(await workspaces.hasLocalChanges({ workspaceId: props.workspaceFile.workspaceId }))) {
+ alerts.nothingToCommitAlert.show();
+ return;
+ }
+ alerts.comittingAlert.show();
+ try {
+ await workspaces.createSavePoint({
+ workspaceId: props.workspaceFile.workspaceId,
+ gitConfig,
+ });
+ alerts.comittingAlert.close();
+ alerts.commitSuccessAlert.show();
+ } catch (e) {
+ alerts.comittingAlert.close();
+ alerts.commitFailAlert.show({ reason: e.message });
+ }
+ }}
+ description={"Create a save point"}
+ >
+ Commit
+ </DropdownItem>
+ );
+ }, [
+ workspaces,
+ props.workspaceFile.workspaceId,
+ alerts.comittingAlert,
+ alerts.nothingToCommitAlert,
+ alerts.commitSuccessAlert,
+ alerts.commitFailAlert,
+ gitConfig,
+ ]);
+
+ const canSeeWorkspaceToolbar = useMemo(
+ () =>
+ props.workspace.descriptor.origin.kind !== WorkspaceKind.LOCAL ||
+ props.workspace.files.length +
+ listDeletedFiles({
+ workspaceDescriptor: props.workspace.descriptor,
+ workspaceGitStatusPromise: props.workspaceGitStatusPromise,
+ }).length >
+ 1,
+ [props.workspace.descriptor, props.workspace.files.length, props.workspaceGitStatusPromise]
+ );
+
+ return (
+ <>
+ <PageSection type={"nav"} variant={"light"} padding={{ default: "noPadding" }}>
+ {canSeeWorkspaceToolbar && (
+ <Flex
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
+ flexWrap={{ default: "nowrap" }}
+ spaceItems={{ default: "spaceItemsMd" }}
+ >
+ <FlexItem style={{ minWidth: 0 }}>
+ <WorkspaceToolbar
+ workspace={props.workspace}
+ workspaceGitStatusPromise={props.workspaceGitStatusPromise}
+ currentWorkspaceFile={props.workspaceFile}
+ onDeletedWorkspaceFile={handleDeletedWorkspaceFile}
+ />
+ </FlexItem>
+ <VsCodeDropdownMenu workspace={props.workspace} />
+ </Flex>
+ )}
+ </PageSection>
+ <PageSection type={"nav"} variant={"light"} style={{ paddingTop: 0, paddingBottom: "16px" }}>
+ <Flex
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
+ alignItems={{ default: "alignItemsCenter" }}
+ flexWrap={{ default: "nowrap" }}
+ >
+ <FlexItem style={{ minWidth: 0 }}>
+ <PageHeaderToolsItem visibility={{ default: "visible" }}>
+ <Flex flexWrap={{ default: "nowrap" }} alignItems={{ default: "alignItemsCenter" }}>
+ <FlexItem style={{ minWidth: 0 }}>
+ <FileSwitcher
+ workspace={props.workspace}
+ gitStatusProps={
+ canSeeWorkspaceToolbar
+ ? {
+ workspaceDescriptor: props.workspace.descriptor,
+ workspaceGitStatusPromise: props.workspaceGitStatusPromise,
+ }
+ : undefined
+ }
+ workspaceFile={props.workspaceFile}
+ onDeletedWorkspaceFile={handleDeletedWorkspaceFile}
+ />
+ </FlexItem>
+ <FileStatus workspace={props.workspace} workspaceFile={props.workspaceFile} editor={props.editor} />
+ </Flex>
+ </PageHeaderToolsItem>
+ </FlexItem>
+ <FlexItem>
+ <Toolbar>
+ <ToolbarContent style={{ paddingRight: 0 }}>
+ <ToolbarGroup>
+ <ToolbarItem>
+ <AcceleratorsDropdown workspaceFile={props.workspaceFile} />
+ </ToolbarItem>
+ <ToolbarItem>
+ <ResponsiveDropdown
+ title={"Add file"}
+ onClose={() => setNewFileDropdownMenuOpen(false)}
+ position={"right"}
+ isOpen={isNewFileDropdownMenuOpen}
+ toggle={
+ <ResponsiveDropdownToggle
+ onToggle={() => setNewFileDropdownMenuOpen((prev) => !prev)}
+ isPrimary={true}
+ toggleIndicator={CaretDownIcon}
+ >
+ <PlusIcon />
+ New file
+ </ResponsiveDropdownToggle>
+ }
+ >
+ <NewFileDropdownMenu
+ workspaceDescriptor={props.workspace.descriptor}
+ destinationDirPath={props.workspaceFile.relativeDirPath}
+ onAddFile={async (file) => {
+ setNewFileDropdownMenuOpen(false);
+ if (!file) {
+ return;
+ }
+
+ history.push({
+ pathname: routes.workspaceWithFilePath.path({
+ workspaceId: file.workspaceId,
+ fileRelativePath: file.relativePathWithoutExtension,
+ extension: file.extension,
+ }),
+ });
+ }}
+ />
+ </ResponsiveDropdown>
+ </ToolbarItem>
+ <ToolbarItem visibility={hideWhenSmall}>
+ {props.workspaceFile.extension === "dmn" && (
+ <ToolbarGroup>
+ <KieSandboxExtendedServicesButtons
+ workspace={props.workspace}
+ workspaceFile={props.workspaceFile}
+ editorPageDock={props.editorPageDock}
+ />
+ </ToolbarGroup>
+ )}
+ </ToolbarItem>
+ {props.workspace.descriptor.origin.kind !== WorkspaceKind.LOCAL && (
+ <ToolbarItem>
+ <SyncDropdownMenu workspace={props.workspace} />
+ </ToolbarItem>
+ )}
+ <ToolbarItem visibility={hideWhenSmall}>
+ <Dropdown
+ onSelect={() => setShareDropdownOpen(false)}
+ isOpen={isShareDropdownOpen}
+ dropdownItems={shareDropdownItems}
+ position={DropdownPosition.right}
+ toggle={
+ <DropdownToggle
+ id={"share-dropdown"}
+ data-testid={"share-dropdown"}
+ onToggle={(isOpen) => setShareDropdownOpen(isOpen)}
+ >
+ {i18n.editorToolbar.share}
+ </DropdownToggle>
+ }
+ />
+ </ToolbarItem>
+ <ToolbarItem visibility={hideWhenSmall} style={{ marginRight: 0 }}>
+ <KebabDropdown
+ id={"kebab-lg"}
+ state={[isLargeKebabOpen, setLargeKebabOpen]}
+ items={[deleteFileDropdownItem, <Divider key={"divider-0"} />, createSavePointDropdownItem]}
+ />
+ </ToolbarItem>
+ <ToolbarItem visibility={showWhenSmall} style={{ marginRight: 0 }}>
+ <KebabDropdown
+ id={"kebab-sm"}
+ state={[isSmallKebabOpen, setSmallKebabOpen]}
+ items={[
+ deleteFileDropdownItem,
+ <Divider key={"divider-0"} />,
+ createSavePointDropdownItem,
+ <Divider key={"divider-1"} />,
+ ...shareDropdownItems,
+ ...(props.workspaceFile.extension !== "dmn"
+ ? []
+ : [
+ <Divider key={"divider-2"} />,
+ <KieSandboxExtendedServicesDropdownGroup
+ workspace={props.workspace}
+ key="kie-sandbox-extended-services-group"
+ />,
+ ]),
+ ]}
+ />
+ </ToolbarItem>
+ </ToolbarGroup>
+ </ToolbarContent>
+ </Toolbar>
+ </FlexItem>
+ </Flex>
+ </PageSection>
+ <textarea ref={copyContentTextArea} style={{ height: 0, position: "absolute", zIndex: -1 }} />
+ </>
+ );
+}
+
+export function EditorToolbar(props: Props) {
+ const workspacePromise = useWorkspacePromise(props.workspaceFile.workspaceId);
+ const workspaceGitStatusPromise = useWorkspaceGitStatusPromise(workspacePromise.data?.descriptor);
+ if (!workspacePromise.data) {
+ return <></>;
+ }
+
+ return (
+ <EditorToolbarContextProvider {...props} workspace={workspacePromise.data}>
+ <EditorToolbarWithWorkspace
+ {...props}
+ workspace={workspacePromise.data}
+ workspaceGitStatusPromise={workspaceGitStatusPromise}
+ />
+ </EditorToolbarContextProvider>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/EditorToolbarContextProvider.tsx b/packages/online-editor/src/editor/Toolbar/EditorToolbarContextProvider.tsx
new file mode 100644
index 0000000000..f212e59c38
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/EditorToolbarContextProvider.tsx
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, {
+ Dispatch,
+ ReactNode,
+ RefObject,
+ SetStateAction,
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { GitIntegrationContextProvider } from "./GitIntegration/GitIntegrationContextProvider";
+import { WorkspaceFile, useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { CreateGitRepositoryModal } from "./GitIntegration/CreateGitRepositoryModal";
+import { CreateGistOrSnippetModal } from "./GitIntegration/CreateGistOrSnippetModal";
+import { EmbedModal } from "./Share/EmbedModal";
+import { EmbeddedEditorRef } from "@kie-tools-core/editor/dist/embedded";
+import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { useAuthSession } from "../../authSessions/AuthSessionsContext";
+
+export type EditorToolbarContextType = {
+ isEmbedModalOpen: boolean;
+ isCreateGitRepositoryModalOpen: boolean;
+ isCreateGistOrSnippetModalOpen: boolean;
+ isShareDropdownOpen: boolean;
+ isSyncGistOrSnippetDropdownOpen: boolean;
+ isSyncGitRepositoryDropdownOpen: boolean;
+ isLargeKebabOpen: boolean;
+ isSmallKebabOpen: boolean;
+ isNewFileDropdownMenuOpen: boolean;
+ isAcceleratorsDropdownOpen: boolean;
+ downloadRef: RefObject<HTMLAnchorElement>;
+ downloadAllRef: RefObject<HTMLAnchorElement>;
+ downloadPreviewRef: RefObject<HTMLAnchorElement>;
+ downloadWorkspaceZip: () => Promise<void>;
+ workspace: ActiveWorkspace;
+};
+
+export type EditorToolbarDispatchContextType = {
+ setEmbedModalOpen: Dispatch<SetStateAction<boolean>>;
+ setCreateGitRepositoryModalOpen: Dispatch<SetStateAction<boolean>>;
+ setCreateGistOrSnippetModalOpen: Dispatch<SetStateAction<boolean>>;
+ setShareDropdownOpen: Dispatch<SetStateAction<boolean>>;
+ setSyncGistOrSnippetDropdownOpen: Dispatch<SetStateAction<boolean>>;
+ setSyncGitRepositoryDropdownOpen: Dispatch<SetStateAction<boolean>>;
+ setLargeKebabOpen: Dispatch<SetStateAction<boolean>>;
+ setSmallKebabOpen: Dispatch<SetStateAction<boolean>>;
+ setNewFileDropdownMenuOpen: Dispatch<SetStateAction<boolean>>;
+ setAcceleratorsDropdownOpen: Dispatch<SetStateAction<boolean>>;
+};
+
+export const EditorToolbarContext = createContext<EditorToolbarContextType>({} as any);
+export const EditorToolbarDispatchContext = createContext<EditorToolbarDispatchContextType>({} as any);
+
+type Props = {
+ workspace: ActiveWorkspace;
+ workspaceFile: WorkspaceFile;
+ children: ReactNode;
+ editor: EmbeddedEditorRef | undefined;
+};
+
+export function EditorToolbarContextProvider(props: Props) {
+ const [isEmbedModalOpen, setEmbedModalOpen] = useState(false);
+ const [isCreateGitRepositoryModalOpen, setCreateGitRepositoryModalOpen] = useState(false);
+ const [isCreateGistOrSnippetModalOpen, setCreateGistOrSnippetModalOpen] = useState(false);
+ const [isShareDropdownOpen, setShareDropdownOpen] = useState(false);
+ const [isSyncGistOrSnippetDropdownOpen, setSyncGistOrSnippetDropdownOpen] = useState(false);
+ const [isSyncGitRepositoryDropdownOpen, setSyncGitRepositoryDropdownOpen] = useState(false);
+ const [isAcceleratorsDropdownOpen, setAcceleratorsDropdownOpen] = useState(false);
+ const [isLargeKebabOpen, setLargeKebabOpen] = useState(false);
+ const [isSmallKebabOpen, setSmallKebabOpen] = useState(false);
+ const [isNewFileDropdownMenuOpen, setNewFileDropdownMenuOpen] = useState(false);
+ const downloadRef = useRef<HTMLAnchorElement>(null);
+ const downloadAllRef = useRef<HTMLAnchorElement>(null);
+ const downloadPreviewRef = useRef<HTMLAnchorElement>(null);
+ const workspaces = useWorkspaces();
+ const { gitConfig } = useAuthSession(props.workspace.descriptor.gitAuthSessionId);
+
+ const downloadWorkspaceZip = useCallback(async () => {
+ if (!props.editor) {
+ return;
+ }
+
+ const zipBlob = await workspaces.prepareZip({ workspaceId: props.workspaceFile.workspaceId });
+ if (downloadAllRef.current) {
+ downloadAllRef.current.href = URL.createObjectURL(zipBlob);
+ downloadAllRef.current.click();
+ }
+ if (props.workspace.descriptor.origin.kind === WorkspaceKind.LOCAL) {
+ await workspaces.createSavePoint({ workspaceId: props.workspaceFile.workspaceId, gitConfig });
+ }
+ }, [
+ props.editor,
+ props.workspaceFile.workspaceId,
+ props.workspace.descriptor.origin.kind,
+ workspaces,
+ downloadAllRef,
+ gitConfig,
+ ]);
+
+ const value = useMemo(
+ () => ({
+ isEmbedModalOpen,
+ isCreateGitRepositoryModalOpen,
+ isCreateGistOrSnippetModalOpen,
+ isShareDropdownOpen,
+ isSyncGistOrSnippetDropdownOpen,
+ isSyncGitRepositoryDropdownOpen,
+ isAcceleratorsDropdownOpen,
+ isLargeKebabOpen,
+ isSmallKebabOpen,
+ isNewFileDropdownMenuOpen,
+ downloadRef,
+ downloadAllRef,
+ downloadPreviewRef,
+ downloadWorkspaceZip,
+ workspace: props.workspace,
+ }),
+ [
+ isCreateGistOrSnippetModalOpen,
+ isCreateGitRepositoryModalOpen,
+ isEmbedModalOpen,
+ isLargeKebabOpen,
+ isNewFileDropdownMenuOpen,
+ isShareDropdownOpen,
+ isSmallKebabOpen,
+ isSyncGistOrSnippetDropdownOpen,
+ isSyncGitRepositoryDropdownOpen,
+ isAcceleratorsDropdownOpen,
+ downloadWorkspaceZip,
+ props.workspace,
+ ]
+ );
+
+ const dispatch = useMemo(
+ () => ({
+ setEmbedModalOpen,
+ setCreateGitRepositoryModalOpen,
+ setCreateGistOrSnippetModalOpen,
+ setShareDropdownOpen,
+ setSyncGistOrSnippetDropdownOpen,
+ setSyncGitRepositoryDropdownOpen,
+ setLargeKebabOpen,
+ setSmallKebabOpen,
+ setNewFileDropdownMenuOpen,
+ setAcceleratorsDropdownOpen,
+ }),
+ []
+ );
+
+ return (
+ <EditorToolbarContext.Provider value={value}>
+ <EditorToolbarDispatchContext.Provider value={dispatch}>
+ <GitIntegrationContextProvider workspace={props.workspace}>
+ {props.children}
+ <CreateGitRepositoryModal
+ workspace={props.workspace.descriptor}
+ isOpen={isCreateGitRepositoryModalOpen}
+ onClose={() => setCreateGitRepositoryModalOpen(false)}
+ />
+ <CreateGistOrSnippetModal
+ workspace={props.workspace.descriptor}
+ isOpen={isCreateGistOrSnippetModalOpen}
+ onClose={() => setCreateGistOrSnippetModalOpen(false)}
+ />
+ <EmbedModal
+ workspace={props.workspace.descriptor}
+ workspaceFile={props.workspaceFile}
+ isOpen={isEmbedModalOpen}
+ onClose={() => setEmbedModalOpen(false)}
+ />
+ <>
+ <a ref={downloadRef} />
+ <a ref={downloadAllRef} />
+ <a ref={downloadPreviewRef} />
+ </>
+ </GitIntegrationContextProvider>
+ </EditorToolbarDispatchContext.Provider>
+ </EditorToolbarContext.Provider>
+ );
+}
+
+export function useEditorToolbarContext() {
+ return useContext(EditorToolbarContext);
+}
+
+export function useEditorToolbarDispatchContext() {
+ return useContext(EditorToolbarDispatchContext);
+}
diff --git a/packages/online-editor/src/editor/Toolbar/FileStatus.tsx b/packages/online-editor/src/editor/Toolbar/FileStatus.tsx
new file mode 100644
index 0000000000..1b33627fec
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/FileStatus.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useEffect, useMemo } from "react";
+import { EmbeddedEditorRef, useDirtyState } from "@kie-tools-core/editor/dist/embedded";
+import { WorkspaceFile, useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
+import OutlinedClockIcon from "@patternfly/react-icons/dist/js/icons/outlined-clock-icon";
+import DesktopIcon from "@patternfly/react-icons/dist/js/icons/desktop-icon";
+import { useSharedValue } from "@kie-tools-core/envelope-bus/dist/hooks";
+import OutlinedHddIcon from "@patternfly/react-icons/dist/js/icons/outlined-hdd-icon";
+
+type Props = {
+ workspace: ActiveWorkspace;
+ workspaceFile: WorkspaceFile;
+ editor: EmbeddedEditorRef | undefined;
+};
+
+export function FileStatus(props: Props) {
+ const workspaces = useWorkspaces();
+ const isEdited = useDirtyState(props.editor);
+
+ const [flushes] = useSharedValue(
+ workspaces.workspacesSharedWorker.workspacesWorkerBus.clientApi.shared.kieSandboxWorkspacesStorage_flushes
+ );
+
+ const isSaved = useMemo(() => {
+ return !isEdited && flushes && !flushes.some((f) => f.includes(props.workspaceFile.workspaceId));
+ }, [isEdited, flushes, props.workspaceFile.workspaceId]);
+
+ // Prevent from closing without flushing before.
+ useEffect(() => {
+ if (isSaved) {
+ return;
+ }
+
+ window.onbeforeunload = () => "Some changes are not written to disk yet.";
+ return () => {
+ window.onbeforeunload = null;
+ };
+ }, [isSaved]);
+
+ return (
+ <>
+ <FlexItem>
+ {(isEdited && (
+ <Tooltip content={"Saving in memory..."} position={"bottom"}>
+ <TextContent style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}>
+ <Text
+ style={{ display: "flex" }}
+ aria-label={"Saving in memory..."}
+ data-testid="is-saving-in-memory-indicator"
+ component={TextVariants.small}
+ >
+ <OutlinedClockIcon size={"sm"} style={{ margin: 0 }} />
+ </Text>
+ </TextContent>
+ </Tooltip>
+ )) || (
+ <Tooltip content={"File is in memory."} position={"bottom"}>
+ <TextContent style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}>
+ <Text
+ style={{ display: "flex" }}
+ aria-label={"File is in memory."}
+ data-testid="is-saved-in-memory-indicator"
+ component={TextVariants.small}
+ >
+ <DesktopIcon size={"sm"} style={{ margin: 0 }} />
+ </Text>
+ </TextContent>
+ </Tooltip>
+ )}
+ </FlexItem>
+ <FlexItem>
+ {(!isSaved && (
+ <Tooltip content={"Writing file..."} position={"bottom"}>
+ <TextContent style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}>
+ <Text
+ style={{ display: "flex" }}
+ aria-label={"Writing file..."}
+ data-testid="is-writing-indicator"
+ component={TextVariants.small}
+ >
+ <OutlinedClockIcon size={"sm"} style={{ margin: 0 }} />
+ </Text>
+ </TextContent>
+ </Tooltip>
+ )) || (
+ <Tooltip content={"File is written on disk."} position={"bottom"}>
+ <TextContent style={{ color: "gray", ...(!props.workspaceFile ? { visibility: "hidden" } : {}) }}>
+ <Text
+ style={{ display: "flex" }}
+ aria-label={"File is written on disk."}
+ data-testid="is-written-indicator"
+ component={TextVariants.small}
+ >
+ <OutlinedHddIcon size={"sm"} style={{ margin: 0 }} />
+ </Text>
+ </TextContent>
+ </Tooltip>
+ )}
+ </FlexItem>
+ </>
+ );
+}
diff --git a/packages/online-editor/src/editor/FileSwitcher.tsx b/packages/online-editor/src/editor/Toolbar/FileSwitcher.tsx
similarity index 51%
rename from packages/online-editor/src/editor/FileSwitcher.tsx
rename to packages/online-editor/src/editor/Toolbar/FileSwitcher.tsx
index 61082f489e..eee495cb4b 100644
--- a/packages/online-editor/src/editor/FileSwitcher.tsx
+++ b/packages/online-editor/src/editor/Toolbar/FileSwitcher.tsx
@@ -16,13 +16,12 @@
import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
-import { useRoutes } from "../navigation/Hooks";
import * as React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { join } from "path";
import { Dropdown } from "@patternfly/react-core/dist/js/components/Dropdown";
import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
-import { FileLabel } from "../filesList/FileLabel";
+import { FileLabel } from "../../filesList/FileLabel";
import { Toggle } from "@patternfly/react-core/dist/js/components/Dropdown/Toggle";
import { Title } from "@patternfly/react-core/dist/js/components/Title";
import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
@@ -46,29 +45,25 @@ import { ThLargeIcon } from "@patternfly/react-icons/dist/js/icons/th-large-icon
import { ListIcon } from "@patternfly/react-icons/dist/js/icons/list-icon";
import { useWorkspaceDescriptorsPromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspacesHooks";
import { PromiseStateWrapper, useCombinedPromiseState } from "@kie-tools-core/react-hooks/dist/PromiseState";
-import { Split, SplitItem } from "@patternfly/react-core/dist/js/layouts/Split";
import { Button } from "@patternfly/react-core/dist/js/components/Button";
-import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
import { useWorkspacesFilesPromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspacesFiles";
import { Skeleton } from "@patternfly/react-core/dist/js/components/Skeleton";
import { Card, CardBody, CardHeader, CardHeaderMain } from "@patternfly/react-core/dist/js/components/Card";
import { Gallery } from "@patternfly/react-core/dist/js/layouts/Gallery";
-import { useHistory } from "react-router";
import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye";
import { EmptyState, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState";
import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon";
-import { ArrowRightIcon } from "@patternfly/react-icons/dist/js/icons/arrow-right-icon";
-import { ArrowLeftIcon } from "@patternfly/react-icons/dist/js/icons/arrow-left-icon";
-import { useEditorEnvelopeLocator } from "../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
+import { useEditorEnvelopeLocator } from "../../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
import { VariableSizeList } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import {
FileDataList,
FileLink,
FileListItem,
+ FileListItemDisplayMode,
getFileDataListHeight,
SingleFileWorkspaceDataList,
-} from "../filesList/FileDataList";
+} from "../../filesList/FileDataList";
import {
DataList,
DataListCell,
@@ -76,26 +71,45 @@ import {
DataListItemCells,
DataListItemRow,
} from "@patternfly/react-core/dist/js/components/DataList";
-import { WorkspaceListItem } from "../workspace/components/WorkspaceListItem";
-import { usePreviewSvg } from "../previewSvgs/PreviewSvgHooks";
-import { WorkspaceLoadingMenuItem } from "../workspace/components/WorkspaceLoadingCard";
+import { WorkspaceListItem } from "../../workspace/components/WorkspaceListItem";
+import { usePreviewSvg } from "../../previewSvgs/PreviewSvgHooks";
+import { WorkspaceLoadingMenuItem } from "../../workspace/components/WorkspaceLoadingCard";
+import {
+ listDeletedFiles,
+ resolveGitLocalChangesStatus,
+ WorkspaceGitLocalChangesStatus,
+} from "../../workspace/components/WorkspaceStatusIndicator";
+import { Checkbox } from "@patternfly/react-core/dist/js/components/Checkbox";
+import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
+import { switchExpression } from "../../switchExpression/switchExpression";
+import { GitStatusProps } from "../../workspace/components/GitStatusIndicatorActions";
const ROOT_MENU_ID = "rootMenu";
-enum FilesDropdownMode {
- LIST_MODELS,
- LIST_MODELS_AND_OTHERS,
+enum FilesMenuMode {
+ LIST,
CAROUSEL,
}
const MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX = 500;
const MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN = 40;
+// properties to be used for menu height calculation
+const FILE_SWITCHER_INITIAL_HEIGHT_OFFSET_IN_PX = 204;
+const MENU_HEIGHT_MAX_LIMIT_CSS = `calc(100vh - ${FILE_SWITCHER_INITIAL_HEIGHT_OFFSET_IN_PX}px)` as const;
+const DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX = 40;
+const MENU_SEARCH_HEIGHT_IN_PX = 64;
+const MENU_SHOW_CHANGED_CHECKBOX_HEIGHT_IN_PX = 36;
+const MENU_DIVIDER_HEIGHT_IN_PX = 17;
-export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile: WorkspaceFile }) {
+export function FileSwitcher(props: {
+ workspace: ActiveWorkspace;
+ gitStatusProps?: GitStatusProps;
+ workspaceFile: WorkspaceFile;
+ onDeletedWorkspaceFile: () => void;
+}) {
const workspaces = useWorkspaces();
const workspaceFileNameRef = useRef<HTMLInputElement>(null);
const [newFileNameValid, setNewFileNameValid] = useState<boolean>(true);
- const [filesDropdownMode, setFilesDropdownMode] = useState(FilesDropdownMode.LIST_MODELS);
const resetWorkspaceFileName = useCallback(() => {
if (workspaceFileNameRef.current) {
@@ -174,15 +188,11 @@ export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile:
const [menuHeights, setMenuHeights] = useState<{ [key: string]: number }>({});
const [activeMenu, setActiveMenu] = useState(ROOT_MENU_ID);
- useEffect(() => {
- setMenuHeights({});
- }, [props.workspace, filesDropdownMode, activeMenu]);
+ const [filesMenuMode, setFilesMenuMode] = useState(FilesMenuMode.LIST);
useEffect(() => {
- setFilesDropdownMode((prev) =>
- prev === FilesDropdownMode.LIST_MODELS_AND_OTHERS ? FilesDropdownMode.LIST_MODELS : prev
- );
- }, [activeMenu]);
+ setMenuHeights({});
+ }, [props.workspace, filesMenuMode, activeMenu]);
useEffect(() => {
if (isFilesDropdownOpen) {
@@ -194,21 +204,25 @@ export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile:
setActiveMenu(`dd${props.workspace.descriptor.workspaceId}`);
}, [isFilesDropdownOpen, props.workspace.descriptor.workspaceId]);
- const drillIn = useCallback((fromMenuId, toMenuId, pathId) => {
+ const drillIn = useCallback((_event, fromMenuId, toMenuId, pathId) => {
setMenuDrilledIn((prev) => [...prev, fromMenuId]);
setDrilldownPath((prev) => [...prev, pathId]);
setActiveMenu(toMenuId);
}, []);
- const drillOut = useCallback((toMenuId) => {
+ const drillOut = useCallback((_event, toMenuId) => {
setMenuDrilledIn((prev) => prev.slice(0, prev.length - 1));
setDrilldownPath((prev) => prev.slice(0, prev.length - 1));
setActiveMenu(toMenuId);
}, []);
const setHeight = useCallback((menuId: string, height: number) => {
- // do not try to simplify this ternary's condition as some heights are 0, resulting in an infinite loop.
- setMenuHeights((prev) => (prev[menuId] === height ? prev : { ...prev, [menuId]: height }));
+ setMenuHeights((prev) => {
+ if (prev[menuId] === undefined || (menuId !== ROOT_MENU_ID && prev[menuId] !== height)) {
+ return { ...prev, [menuId]: height };
+ }
+ return prev;
+ });
}, []);
const workspacesMenuItems = useMemo(() => {
@@ -221,11 +235,11 @@ export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile:
activeMenu={activeMenu}
currentWorkspace={props.workspace}
onSelectFile={() => setFilesDropdownOpen(false)}
- filesDropdownMode={filesDropdownMode}
- setFilesDropdownMode={setFilesDropdownMode}
+ filesMenuMode={filesMenuMode}
+ setFilesMenuMode={setFilesMenuMode}
/>
);
- }, [activeMenu, filesDropdownMode, props.workspace]);
+ }, [activeMenu, filesMenuMode, props.workspace]);
return (
<>
@@ -336,11 +350,6 @@ export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile:
style={{
boxShadow: "none",
minWidth: `${MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX}px`,
- width: `${
- filesDropdownMode === FilesDropdownMode.LIST_MODELS_AND_OTHERS
- ? MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX * 2
- : MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX
- }px`,
}}
id={ROOT_MENU_ID}
containsDrilldown={true}
@@ -350,37 +359,45 @@ export function FileSwitcher(props: { workspace: ActiveWorkspace; workspaceFile:
onDrillIn={drillIn}
onDrillOut={drillOut}
onGetMenuHeight={setHeight}
+ className={"kie-sandbox--files-menu"}
>
<MenuContent
// MAGIC NUMBER ALERT
//
// 204px is the exact number that allows the menu to grow to
// the maximum size of the screen without adding scroll to the page.
- maxMenuHeight={`calc(100vh - 204px)`}
+ maxMenuHeight={MENU_HEIGHT_MAX_LIMIT_CSS}
menuHeight={activeMenu === ROOT_MENU_ID ? undefined : `${menuHeights[activeMenu]}px`}
- style={{ overflow: "hidden" }}
>
- <MenuList>
+ <MenuList style={{ padding: 0 }}>
<MenuItem
itemId={props.workspace.descriptor.workspaceId}
direction={"down"}
drilldownMenu={
<DrilldownMenu id={`dd${props.workspace.descriptor.workspaceId}`}>
<FilesMenuItems
- shouldFocusOnSearch={activeMenu === `dd${props.workspace.descriptor.workspaceId}`}
- filesDropdownMode={filesDropdownMode}
- setFilesDropdownMode={setFilesDropdownMode}
- workspaceDescriptor={props.workspace.descriptor}
- workspaceFiles={props.workspace.files}
+ shouldFocusOnSearch={activeMenu.startsWith(`dd${props.workspace.descriptor.workspaceId}`)}
+ filesMenuMode={filesMenuMode}
+ setFilesMenuMode={setFilesMenuMode}
+ workspace={props.workspace}
currentWorkspaceFile={props.workspaceFile}
onSelectFile={() => setFilesDropdownOpen(false)}
+ currentWorkspaceGitStatusProps={props.gitStatusProps}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
/>
</DrilldownMenu>
}
>
Current
</MenuItem>
- {workspacesMenuItems}
+ <MenuGroup
+ style={{
+ maxHeight: `calc(${MENU_HEIGHT_MAX_LIMIT_CSS} - ${DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX}px)` /* height of menu minus the height of Current item*/,
+ overflowY: "auto",
+ }}
+ >
+ {workspacesMenuItems}
+ </MenuGroup>
</MenuList>
</MenuContent>
</Menu>
@@ -395,8 +412,8 @@ export function WorkspacesMenuItems(props: {
activeMenu: string;
currentWorkspace: ActiveWorkspace;
onSelectFile: () => void;
- filesDropdownMode: FilesDropdownMode;
- setFilesDropdownMode: React.Dispatch<React.SetStateAction<FilesDropdownMode>>;
+ filesMenuMode: FilesMenuMode;
+ setFilesMenuMode: React.Dispatch<React.SetStateAction<FilesMenuMode>>;
}) {
const editorEnvelopeLocator = useEditorEnvelopeLocator();
const workspaceDescriptorsPromise = useWorkspaceDescriptorsPromise();
@@ -443,13 +460,13 @@ export function WorkspacesMenuItems(props: {
itemId={descriptor.workspaceId}
direction={"down"}
drilldownMenu={
- <DrilldownMenu id={`dd${descriptor.workspaceId}`}>
+ <DrilldownMenu id={`dd${descriptor.workspaceId}`} style={{ position: "fixed" }}>
+ {/* position:fixed important to render properly inside menugroup*/}
<FilesMenuItems
- shouldFocusOnSearch={props.activeMenu === `dd${descriptor.workspaceId}`}
- filesDropdownMode={props.filesDropdownMode}
- setFilesDropdownMode={props.setFilesDropdownMode}
- workspaceDescriptor={descriptor}
- workspaceFiles={workspaceFiles.get(descriptor.workspaceId) ?? []}
+ shouldFocusOnSearch={props.activeMenu.startsWith(`dd${descriptor.workspaceId}`)}
+ filesMenuMode={props.filesMenuMode}
+ setFilesMenuMode={props.setFilesMenuMode}
+ workspace={{ descriptor, files: workspaceFiles.get(descriptor.workspaceId) ?? [] }}
onSelectFile={props.onSelectFile}
/>
</DrilldownMenu>
@@ -503,7 +520,19 @@ export function FileSvg(props: { workspaceFile: WorkspaceFile }) {
return (
<>
<PromiseStateWrapper
- pending={<Skeleton height={"180px"} style={{ margin: "10px" }} />}
+ pending={
+ imgRef.current ? (
+ <Bullseye>
+ <img
+ style={{ height: "180px", margin: "10px" }}
+ ref={imgRef}
+ alt={`SVG for ${props.workspaceFile.relativePath}`}
+ />
+ </Bullseye>
+ ) : (
+ <Skeleton height={"180px"} style={{ margin: "10px" }} />
+ )
+ }
rejected={() => (
<div style={{ height: "180px", margin: "10px", borderRadius: "5px", backgroundColor: "#EEE" }}>
<Bullseye>
@@ -526,78 +555,37 @@ export function FileSvg(props: { workspaceFile: WorkspaceFile }) {
);
}
-export function SearchableFilesMenuGroup(props: {
- shouldFocusOnSearch: boolean;
- filesDropdownMode: FilesDropdownMode;
- label: string;
+export function FilteredFilesMenuGroup(props: {
+ filesMenuMode: FilesMenuMode;
allFiles: WorkspaceFile[];
+ filteredFiles: WorkspaceFile[];
search: string;
- setSearch: React.Dispatch<React.SetStateAction<string>>;
children: (args: { filteredFiles: WorkspaceFile[] }) => React.ReactNode;
+ heightMaxLimitCss: string;
+ style?: React.CSSProperties;
}) {
- const searchInputRef = useRef<HTMLInputElement>(null);
-
- useEffect(() => {
- if (props.shouldFocusOnSearch) {
- const task = setTimeout(() => {
- searchInputRef.current?.focus();
- }, 500);
- return () => {
- clearTimeout(task);
- };
- }
- }, [props.shouldFocusOnSearch, props.filesDropdownMode]);
-
- const filteredFiles = useMemo(
- () => props.allFiles.filter((file) => file.name.toLowerCase().includes(props.search.toLowerCase())),
- [props.allFiles, props.search]
- );
-
const height = useMemo(() => {
- if (
- props.filesDropdownMode === FilesDropdownMode.LIST_MODELS ||
- props.filesDropdownMode === FilesDropdownMode.LIST_MODELS_AND_OTHERS
- ) {
+ if (props.filesMenuMode === FilesMenuMode.LIST) {
// No reason to know the exact size.
const sizeOfFirst50Elements = props.allFiles
.slice(0, 50)
.map((f) => getFileDataListHeight(f))
.reduce((a, b) => a + b, 0);
-
- // MAGIC NUMBER ALERT
- //
- // 440px is the exact number that allows the menu to grow to the end of the screen without adding scroll to the
- // entire page, It includes the first menu item, the search bar and the "View other files" button at the bottom.
- return `max(300px, min(calc(100vh - 440px), ${sizeOfFirst50Elements}px))`;
- } else if (props.filesDropdownMode === FilesDropdownMode.CAROUSEL) {
- // MAGIC NUMBER ALERT
- //
- // 384px is the exact number that allows the menu to grow to the end of the screen without
- // adding a scroll to the entire page. It includes the first menu item and the search bar.
- //
+ return `max(300px, min(${props.heightMaxLimitCss}, ${sizeOfFirst50Elements}px))`;
+ } else if (props.filesMenuMode === FilesMenuMode.CAROUSEL) {
// 280px is the size of a File SVG card.
- return `min(calc(100vh - 384px), calc(${props.allFiles.length} * 280px))`;
+ return `min(${props.heightMaxLimitCss}, calc(${props.allFiles.length} * 280px))`;
} else {
return "";
}
- }, [props.allFiles, props.filesDropdownMode]);
+ }, [props.allFiles, props.filesMenuMode, props.heightMaxLimitCss]);
return (
- <MenuGroup label={props.label}>
+ <MenuGroup>
{/* Allows for arrows to work when editing the text. */}
- <MenuInput onKeyDown={(e) => e.stopPropagation()}>
- <TextInput
- ref={searchInputRef}
- value={props.search}
- aria-label={"Other files menu items"}
- iconVariant={"search"}
- type={"search"}
- onChange={(value) => props.setSearch(value)}
- />
- </MenuInput>
- <div style={{ overflowY: "auto", height }}>
- {filteredFiles.length > 0 && props.children({ filteredFiles })}
- {filteredFiles.length <= 0 && (
+ <div style={{ ...props.style, height }}>
+ {props.filteredFiles.length > 0 && props.children({ filteredFiles: props.filteredFiles })}
+ {props.filteredFiles.length <= 0 && (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
@@ -613,72 +601,354 @@ export function SearchableFilesMenuGroup(props: {
}
export function FilesMenuItems(props: {
- workspaceDescriptor: WorkspaceDescriptor;
- workspaceFiles: WorkspaceFile[];
+ workspace: ActiveWorkspace;
+ currentWorkspaceGitStatusProps?: GitStatusProps;
currentWorkspaceFile?: WorkspaceFile;
+ onDeletedWorkspaceFile?: () => void;
onSelectFile: () => void;
- filesDropdownMode: FilesDropdownMode;
- setFilesDropdownMode: React.Dispatch<React.SetStateAction<FilesDropdownMode>>;
+ filesMenuMode: FilesMenuMode;
+ setFilesMenuMode: React.Dispatch<React.SetStateAction<FilesMenuMode>>;
shouldFocusOnSearch: boolean;
}) {
- const history = useHistory();
- const routes = useRoutes();
const editorEnvelopeLocator = useEditorEnvelopeLocator();
- const sortedAndFilteredFiles = useMemo(
- () =>
- props.workspaceFiles
- .sort((a, b) => a.relativePath.localeCompare(b.relativePath))
- .filter((file) => file.relativePath !== props.currentWorkspaceFile?.relativePath),
- [props.workspaceFiles, props.currentWorkspaceFile]
+ const [topSectionHeightInPx, setTopSectionHeightInPx] = useState<number>(
+ DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX +
+ MENU_SEARCH_HEIGHT_IN_PX +
+ MENU_SHOW_CHANGED_CHECKBOX_HEIGHT_IN_PX +
+ MENU_DIVIDER_HEIGHT_IN_PX
);
+ const [bottomSectionHeightInPx, setBottomSectionHeightInPx] = useState<number>(
+ DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX + MENU_DIVIDER_HEIGHT_IN_PX
+ );
+
+ const [isModelsMenuActive, setModelsMenuActive] = useState(true);
+ const [searchValue, setSearchValue] = useState("");
+ const [filteredGitSyncStatus, setFilteredGitSyncStatus] = useState<WorkspaceGitLocalChangesStatus>();
+
+ const searchInputRef = useRef<HTMLInputElement>(null);
+ const carouselScrollRef = useRef<HTMLDivElement>(null);
+ const listScrollRef = useRef<VariableSizeList>(null);
+
+ const deletedWorkspaceFiles = useMemo(() => {
+ if (!props.currentWorkspaceGitStatusProps) {
+ return [];
+ }
+ return listDeletedFiles(props.currentWorkspaceGitStatusProps);
+ }, [props.currentWorkspaceGitStatusProps]);
+
+ const allFiles = useMemo(() => {
+ return [...props.workspace.files, ...deletedWorkspaceFiles];
+ }, [deletedWorkspaceFiles, props.workspace.files]);
+
+ const sortedFiles = useMemo(() => allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath)), [allFiles]);
const models = useMemo(
- () => sortedAndFilteredFiles.filter((file) => editorEnvelopeLocator.hasMappingFor(file.relativePath)),
- [editorEnvelopeLocator, sortedAndFilteredFiles]
+ () => sortedFiles.filter((file) => editorEnvelopeLocator.hasMappingFor(file.relativePath)),
+ [editorEnvelopeLocator, sortedFiles]
);
const otherFiles = useMemo(
- () => sortedAndFilteredFiles.filter((file) => !editorEnvelopeLocator.hasMappingFor(file.relativePath)),
- [editorEnvelopeLocator, sortedAndFilteredFiles]
+ () => sortedFiles.filter((file) => !editorEnvelopeLocator.hasMappingFor(file.relativePath)),
+ [editorEnvelopeLocator, sortedFiles]
);
- const [search, setSearch] = useState("");
- const [otherFilesSearch, setOtherFilesSearch] = useState("");
+ const filteredModels = useMemo(
+ () =>
+ models
+ .filter((file) => file.name.toLowerCase().includes(searchValue.toLowerCase()))
+ .filter(
+ (file) =>
+ !filteredGitSyncStatus ||
+ resolveGitLocalChangesStatus({
+ workspaceGitStatus: props.currentWorkspaceGitStatusProps?.workspaceGitStatusPromise.data,
+ file,
+ }) === filteredGitSyncStatus
+ ),
+ [filteredGitSyncStatus, models, props.currentWorkspaceGitStatusProps?.workspaceGitStatusPromise.data, searchValue]
+ );
+
+ const filteredOtherFiles = useMemo(
+ () => otherFiles.filter((file) => file.name.toLowerCase().includes(searchValue.toLowerCase())),
+ [otherFiles, searchValue]
+ );
+
+ const computedInitialScrollOffset = useMemo(() => {
+ if (props.filesMenuMode !== FilesMenuMode.LIST || !isModelsMenuActive || !props.currentWorkspaceFile) {
+ return;
+ }
+ const index = filteredModels.findIndex((it) => it.relativePath === props.currentWorkspaceFile?.relativePath);
+ if (index < 0) {
+ return 0;
+ }
+ return filteredModels.slice(0, index).reduce((sum, current) => sum + getFileDataListHeight(current), 0);
+ }, [filteredModels, isModelsMenuActive, props.currentWorkspaceFile, props.filesMenuMode]);
+
+ const searchInput = useCallback(
+ (searchRef?: React.RefObject<HTMLInputElement>) => {
+ return (
+ <MenuInput onKeyDown={(e) => e.stopPropagation()}>
+ <SearchInput
+ ref={searchRef}
+ value={searchValue}
+ type={"search"}
+ onChange={(_ev, value) => {
+ setSearchValue(value);
+ }}
+ placeholder={`In '${props.workspace.descriptor.name}'`}
+ style={{ fontSize: "small" }}
+ onClick={(e) => {
+ e.stopPropagation();
+ }}
+ />
+ </MenuInput>
+ );
+ },
+ [props.workspace.descriptor.name, searchValue]
+ );
+
+ const fileListMenuItemsHeightMaxLimitCss = useMemo(() => {
+ return `calc(${MENU_HEIGHT_MAX_LIMIT_CSS} - ${
+ topSectionHeightInPx + (otherFiles.length ? bottomSectionHeightInPx : 0)
+ }px)`;
+ }, [bottomSectionHeightInPx, otherFiles.length, topSectionHeightInPx]);
+
+ const isCurrentWorkspaceFile = useCallback(
+ (file: WorkspaceFile) => {
+ return (
+ props.workspace.descriptor.workspaceId === file.workspaceId &&
+ props.currentWorkspaceFile?.relativePath === file.relativePath
+ );
+ },
+ [props.currentWorkspaceFile?.relativePath, props.workspace.descriptor.workspaceId]
+ );
+
+ const getFileListItemDisplayMode = useCallback(
+ (file: WorkspaceFile) =>
+ deletedWorkspaceFiles.some((it) => it.relativePath === file.relativePath)
+ ? FileListItemDisplayMode.deleted
+ : FileListItemDisplayMode.enabled,
+ [deletedWorkspaceFiles]
+ );
+
+ useEffect(() => {
+ const task = setTimeout(() => {
+ if (props.filesMenuMode === FilesMenuMode.CAROUSEL && isModelsMenuActive) {
+ carouselScrollRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" });
+ }
+ if (props.filesMenuMode === FilesMenuMode.LIST && isModelsMenuActive) {
+ listScrollRef.current?.scrollToItem(
+ filteredModels.findIndex((it) => it.relativePath === props.currentWorkspaceFile?.relativePath)
+ );
+ }
+ if (props.shouldFocusOnSearch) {
+ searchInputRef.current?.focus({ preventScroll: true });
+ }
+ }, 500);
+ return () => {
+ clearTimeout(task);
+ };
+ }, [
+ props.filesMenuMode,
+ isModelsMenuActive,
+ filteredGitSyncStatus,
+ props.currentWorkspaceFile?.relativePath,
+ filteredModels,
+ props.shouldFocusOnSearch,
+ ]);
return (
<>
- <Split>
- <SplitItem isFilled={true}>
- <MenuItem direction="up" itemId={props.workspaceDescriptor.workspaceId}>
- All
- </MenuItem>
- </SplitItem>
- <SplitItem>
- <FilesDropdownModeIcons
- filesDropdownMode={props.filesDropdownMode}
- setFilesDropdownMode={props.setFilesDropdownMode}
- />
-
- </SplitItem>
- </Split>
+ <MenuItem direction={"up"} itemId={`${props.workspace.descriptor.workspaceId}-breadcrumb`}>
+ All
+ </MenuItem>
+ {searchInput(isModelsMenuActive ? searchInputRef : undefined)}
+ <Flex
+ justifyContent={{ default: "justifyContentFlexEnd" }}
+ alignItems={{ default: "alignItemsCenter" }}
+ style={{ paddingRight: "16px" }}
+ >
+ <FlexItem>
+ {props.currentWorkspaceGitStatusProps !== undefined && (
+ <Tooltip content={"Select to display only modified, added, or deleted files"} position={"bottom"}>
+ <Checkbox
+ label={"Only modified"}
+ id={"filter-git-status-modified-locally"}
+ aria-label={"Select to display only modified, added, or deleted files"}
+ isChecked={filteredGitSyncStatus === WorkspaceGitLocalChangesStatus.pending}
+ onChange={(checked) => {
+ setFilteredGitSyncStatus(checked ? WorkspaceGitLocalChangesStatus.pending : undefined);
+ }}
+ />
+ </Tooltip>
+ )}
+ </FlexItem>
+ <FlexItem>
+ <Flex justifyContent={{ default: "justifyContentCenter" }}>
+ <FilesMenuModeIcons filesMenuMode={props.filesMenuMode} setFilesMenuMode={props.setFilesMenuMode} />
+ </Flex>
+ </FlexItem>
+ </Flex>
<Divider component={"li"} />
- <Split>
- {(props.filesDropdownMode === FilesDropdownMode.LIST_MODELS ||
- props.filesDropdownMode === FilesDropdownMode.LIST_MODELS_AND_OTHERS) && (
- <SplitItem isFilled={true} style={{ minWidth: `${MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX}px` }}>
- <>
- <SearchableFilesMenuGroup
- search={search}
- setSearch={setSearch}
- filesDropdownMode={props.filesDropdownMode}
- shouldFocusOnSearch={props.shouldFocusOnSearch}
- label={`Models in '${props.workspaceDescriptor.name}'`}
- allFiles={models}
+ {props.filesMenuMode === FilesMenuMode.LIST && (
+ <FilteredFilesMenuGroup
+ search={searchValue}
+ filesMenuMode={props.filesMenuMode}
+ allFiles={models}
+ filteredFiles={filteredModels}
+ heightMaxLimitCss={fileListMenuItemsHeightMaxLimitCss}
+ >
+ {({ filteredFiles }) => {
+ return (
+ <AutoSizer>
+ {({ height, width }) => (
+ <VariableSizeList
+ height={height}
+ itemCount={filteredFiles.length}
+ itemSize={(index) => getFileDataListHeight(filteredFiles[index])}
+ width={width}
+ initialScrollOffset={computedInitialScrollOffset}
+ ref={listScrollRef}
+ >
+ {({ index, style }) => {
+ const fileListItemDisplayMode = getFileListItemDisplayMode(filteredFiles[index]);
+ return (
+ <MenuItem
+ key={filteredFiles[index].relativePath}
+ onClick={
+ fileListItemDisplayMode === FileListItemDisplayMode.enabled ? props.onSelectFile : undefined
+ }
+ className={"kie-tools--file-switcher-no-padding-menu-item"}
+ isFocused={isCurrentWorkspaceFile(filteredFiles[index])}
+ isActive={isCurrentWorkspaceFile(filteredFiles[index])}
+ style={style}
+ component={"div"}
+ >
+ <FileDataList
+ file={filteredFiles[index]}
+ displayMode={fileListItemDisplayMode}
+ gitStatusProps={props.currentWorkspaceGitStatusProps}
+ isCurrentWorkspaceFile={isCurrentWorkspaceFile(filteredFiles[index])}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ />
+ </MenuItem>
+ );
+ }}
+ </VariableSizeList>
+ )}
+ </AutoSizer>
+ );
+ }}
+ </FilteredFilesMenuGroup>
+ )}
+
+ {props.filesMenuMode === FilesMenuMode.CAROUSEL && (
+ <FilteredFilesMenuGroup
+ search={searchValue}
+ filesMenuMode={props.filesMenuMode}
+ allFiles={models}
+ filteredFiles={filteredModels}
+ heightMaxLimitCss={fileListMenuItemsHeightMaxLimitCss}
+ style={{ overflowY: "auto" }}
+ >
+ {({ filteredFiles }) => {
+ const isCurrentFileOutsideOfShownList =
+ props.currentWorkspaceFile &&
+ filteredFiles.findIndex((file) => file.relativePath === props.currentWorkspaceFile?.relativePath) >=
+ MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN;
+ const shouldScrollToTop =
+ props.currentWorkspaceFile &&
+ filteredFiles.findIndex((file) => file.relativePath === props.currentWorkspaceFile?.relativePath) < 0;
+ return (
+ <Gallery
+ hasGutter={true}
+ style={{
+ paddingLeft: "8px",
+ paddingRight: "8px",
+ borderTop: "var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100)",
+ }}
>
- {({ filteredFiles }) => (
+ {shouldScrollToTop && <div ref={carouselScrollRef} />}
+ {filteredFiles.slice(0, MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN).map((file, index) => (
+ <FilesMenuItemCarouselCard
+ key={index}
+ file={file}
+ gitStatusProps={props.currentWorkspaceGitStatusProps}
+ isCurrentWorkspaceFile={isCurrentWorkspaceFile(file)}
+ displayMode={getFileListItemDisplayMode(file)}
+ onSelectFile={props.onSelectFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ scrollRef={carouselScrollRef}
+ />
+ ))}
+ {filteredFiles.length > MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN && (
<>
+ {props.currentWorkspaceFile && isCurrentFileOutsideOfShownList && (
+ <FilesMenuItemCarouselCard
+ file={props.currentWorkspaceFile}
+ gitStatusProps={props.currentWorkspaceGitStatusProps}
+ isCurrentWorkspaceFile={isCurrentWorkspaceFile(props.currentWorkspaceFile)}
+ displayMode={getFileListItemDisplayMode(props.currentWorkspaceFile)}
+ onSelectFile={props.onSelectFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ scrollRef={carouselScrollRef}
+ />
+ )}
+ <Card style={{ border: 0 }}>
+ <CardBody>
+ <Bullseye>
+ <div>
+ ...and{" "}
+ {filteredFiles.length -
+ MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN -
+ (isCurrentFileOutsideOfShownList ? 1 : 0)}{" "}
+ more.
+ </div>
+ </Bullseye>
+ </CardBody>
+ </Card>
+ </>
+ )}
+ </Gallery>
+ );
+ }}
+ </FilteredFilesMenuGroup>
+ )}
+ {otherFiles.length > 0 && (
+ <>
+ <Divider component={"li"} />
+ <MenuItem
+ itemId={`${props.workspace.descriptor.workspaceId}-other`}
+ direction={"down"}
+ onClick={(_event) => {
+ setModelsMenuActive(false);
+ setBottomSectionHeightInPx(0);
+ setTopSectionHeightInPx((prev) => prev - MENU_SHOW_CHANGED_CHECKBOX_HEIGHT_IN_PX);
+ }}
+ drilldownMenu={
+ <DrilldownMenu id={`dd${props.workspace.descriptor.workspaceId}-other`} style={{ position: "fixed" }}>
+ <MenuItem
+ itemId={"back-up"}
+ direction={"up"}
+ onClick={(_event) => {
+ setModelsMenuActive(true);
+ setBottomSectionHeightInPx(MENU_DIVIDER_HEIGHT_IN_PX + DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX);
+ setTopSectionHeightInPx((prev) => prev + MENU_SHOW_CHANGED_CHECKBOX_HEIGHT_IN_PX);
+ }}
+ >
+ Back
+ </MenuItem>
+ {searchInput(isModelsMenuActive ? undefined : searchInputRef)}
+ <Divider component={"li"} />
+ <FilteredFilesMenuGroup
+ search={searchValue}
+ filesMenuMode={FilesMenuMode.LIST} // always LIST for otherFiles, even for mode CAROUSEL
+ allFiles={otherFiles}
+ filteredFiles={filteredOtherFiles}
+ heightMaxLimitCss={fileListMenuItemsHeightMaxLimitCss}
+ >
+ {({ filteredFiles }) => (
<AutoSizer>
{({ height, width }) => (
<VariableSizeList
@@ -690,190 +960,112 @@ export function FilesMenuItems(props: {
{({ index, style }) => (
<MenuItem
key={filteredFiles[index].relativePath}
- onClick={props.onSelectFile}
style={style}
+ component={"div"}
className={"kie-tools--file-switcher-no-padding-menu-item"}
>
- <FileDataList file={filteredFiles[index]} isEditable={true} />
+ <FileDataList
+ file={filteredFiles[index]}
+ displayMode={FileListItemDisplayMode.readonly}
+ gitStatusProps={props.currentWorkspaceGitStatusProps}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ />
</MenuItem>
)}
</VariableSizeList>
)}
</AutoSizer>
- </>
- )}
- </SearchableFilesMenuGroup>
- {otherFiles.length > 0 && (
- <>
- <Divider component={"li"} />
- <MenuGroup>
- <MenuList>
- <MenuItem
- onClick={(e) => {
- e.stopPropagation();
- props.setFilesDropdownMode((prev) =>
- prev === FilesDropdownMode.LIST_MODELS
- ? FilesDropdownMode.LIST_MODELS_AND_OTHERS
- : FilesDropdownMode.LIST_MODELS
- );
- }}
- >
- {props.filesDropdownMode === FilesDropdownMode.LIST_MODELS
- ? "View other files"
- : "Hide other files"}
-
- {props.filesDropdownMode === FilesDropdownMode.LIST_MODELS ? (
- <ArrowRightIcon />
- ) : (
- <ArrowLeftIcon />
- )}
- </MenuItem>
- </MenuList>
- </MenuGroup>
- </>
- )}
- </>
- </SplitItem>
- )}
-
- {props.filesDropdownMode === FilesDropdownMode.LIST_MODELS_AND_OTHERS && (
- <SplitItem isFilled={true} style={{ minWidth: `${MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX}px` }}>
- <SearchableFilesMenuGroup
- search={otherFilesSearch}
- setSearch={setOtherFilesSearch}
- filesDropdownMode={props.filesDropdownMode}
- shouldFocusOnSearch={props.shouldFocusOnSearch}
- label={`Other files in '${props.workspaceDescriptor.name}'`}
- allFiles={otherFiles}
- >
- {({ filteredFiles }) => (
- <>
- <AutoSizer>
- {({ height, width }) => (
- <VariableSizeList
- height={height}
- itemCount={filteredFiles.length}
- itemSize={(index) => getFileDataListHeight(filteredFiles[index])}
- width={width}
- >
- {({ index, style }) => (
- <MenuItem
- component={"span"}
- onClick={() => {}}
- style={style}
- className={"kie-tools--file-switcher-no-padding-menu-item"}
- >
- <FileDataList file={filteredFiles[index]} isEditable={false} />
- </MenuItem>
- )}
- </VariableSizeList>
- )}
- </AutoSizer>
- </>
- )}
- </SearchableFilesMenuGroup>
- </SplitItem>
- )}
-
- {props.filesDropdownMode === FilesDropdownMode.CAROUSEL && (
- <SplitItem isFilled={true}>
- <SearchableFilesMenuGroup
- search={search}
- setSearch={setSearch}
- filesDropdownMode={props.filesDropdownMode}
- shouldFocusOnSearch={props.shouldFocusOnSearch}
- label={`Models in '${props.workspaceDescriptor.name}'`}
- allFiles={models}
- >
- {({ filteredFiles }) => (
- <Gallery
- hasGutter={true}
- style={{
- padding: "8px",
- borderTop: "var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100)",
- }}
- >
- {filteredFiles.slice(0, MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN).map((file) => (
- <Card
- key={file.relativePath}
- isSelectable={true}
- isRounded={true}
- isCompact={true}
- isHoverable={true}
- isFullHeight={true}
- onClick={() => {
- history.push({
- pathname: routes.workspaceWithFilePath.path({
- workspaceId: file.workspaceId,
- fileRelativePath: file.relativePathWithoutExtension,
- extension: file.extension,
- }),
- });
-
- props.onSelectFile();
- }}
- >
- <FileLink file={file}>
- {/* The default display:flex makes the text overflow */}
- <CardHeader style={{ display: "block" }}>
- <CardHeaderMain>
- <FileListItem file={file} isEditable={true} />
- </CardHeaderMain>
- </CardHeader>
- </FileLink>
- <Divider inset={{ default: "insetMd" }} />
- <CardBody style={{ padding: 0 }}>
- <FileSvg workspaceFile={file} />
- </CardBody>
- </Card>
- ))}
- {filteredFiles.length > MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN && (
- <Card style={{ border: 0 }}>
- <CardBody>
- <Bullseye>
- <div>...and {filteredFiles.length - MAX_NUMBER_OF_CAROUSEL_ITEMS_SHOWN} more.</div>
- </Bullseye>
- </CardBody>
- </Card>
)}
- </Gallery>
- )}
- </SearchableFilesMenuGroup>
- </SplitItem>
- )}
- </Split>
+ </FilteredFilesMenuGroup>
+ </DrilldownMenu>
+ }
+ >
+ Other files
+ </MenuItem>
+ </>
+ )}
</>
);
}
-export function FilesDropdownModeIcons(props: {
- filesDropdownMode: FilesDropdownMode;
- setFilesDropdownMode: React.Dispatch<React.SetStateAction<FilesDropdownMode>>;
+const FilesMenuItemCarouselCard = (props: {
+ file: WorkspaceFile;
+ gitStatusProps?: GitStatusProps;
+ isCurrentWorkspaceFile?: boolean;
+ onDeletedWorkspaceFile?: () => void;
+ displayMode: FileListItemDisplayMode;
+ onSelectFile: () => void;
+ scrollRef: React.RefObject<HTMLDivElement>;
+}) => {
+ const cardInternals = [
+ <CardHeader style={{ display: "block" }} key={0}>
+ <CardHeaderMain>
+ <FileListItem
+ file={props.file}
+ displayMode={props.displayMode}
+ gitStatusProps={props.gitStatusProps}
+ isCurrentWorkspaceFile={props.isCurrentWorkspaceFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ />
+ </CardHeaderMain>
+ </CardHeader>,
+ <Divider inset={{ default: "insetMd" }} key={1} />,
+ <CardBody style={{ padding: 0 }} key={2}>
+ <FileSvg workspaceFile={props.file} />
+ </CardBody>,
+ ];
+ return (
+ <Card
+ key={props.file.relativePath}
+ isSelectable={props.displayMode === FileListItemDisplayMode.enabled}
+ isRounded={true}
+ isCompact={true}
+ isFullHeight={true}
+ onClick={props.displayMode === FileListItemDisplayMode.enabled ? props.onSelectFile : undefined}
+ className={switchExpression(props.displayMode, {
+ enabled: "kie-tools--file-list-item-enabled",
+ deleted: "kie-tools--file-list-item-deleted",
+ readonly: "kie-tools--file-list-item-readonly",
+ })}
+ >
+ <div id={`scrollRef-${props.file.relativePath}`} ref={props.isCurrentWorkspaceFile ? props.scrollRef : undefined}>
+ {props.displayMode === FileListItemDisplayMode.enabled ? (
+ <FileLink file={props.file}>{cardInternals}</FileLink>
+ ) : (
+ cardInternals
+ )}
+ </div>
+ </Card>
+ );
+};
+
+export function FilesMenuModeIcons(props: {
+ filesMenuMode: FilesMenuMode;
+ setFilesMenuMode: React.Dispatch<React.SetStateAction<FilesMenuMode>>;
}) {
return (
<>
- {props.filesDropdownMode === FilesDropdownMode.CAROUSEL && (
+ {props.filesMenuMode === FilesMenuMode.CAROUSEL && (
<Button
className={"kie-tools--masthead-hoverable"}
variant="plain"
aria-label="Switch to list view"
onClick={(e) => {
e.stopPropagation();
- props.setFilesDropdownMode(FilesDropdownMode.LIST_MODELS);
+ props.setFilesMenuMode(FilesMenuMode.LIST);
}}
>
<ListIcon />
</Button>
)}
- {(props.filesDropdownMode === FilesDropdownMode.LIST_MODELS ||
- props.filesDropdownMode === FilesDropdownMode.LIST_MODELS_AND_OTHERS) && (
+ {props.filesMenuMode === FilesMenuMode.LIST && (
<Button
className={"kie-tools--masthead-hoverable"}
variant="plain"
aria-label="Switch to carousel view"
onClick={(e) => {
e.stopPropagation();
- props.setFilesDropdownMode(FilesDropdownMode.CAROUSEL);
+ props.setFilesMenuMode(FilesMenuMode.CAROUSEL);
}}
>
<ThLargeIcon />
diff --git a/packages/online-editor/src/editor/CreateGistOrSnippetModal.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGistOrSnippetModal.tsx
similarity index 89%
rename from packages/online-editor/src/editor/CreateGistOrSnippetModal.tsx
rename to packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGistOrSnippetModal.tsx
index 28100c07f4..04033b0907 100644
--- a/packages/online-editor/src/editor/CreateGistOrSnippetModal.tsx
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGistOrSnippetModal.tsx
@@ -27,22 +27,23 @@ import { LockIcon } from "@patternfly/react-icons/dist/js/icons/lock-icon";
import { ExclamationCircleIcon } from "@patternfly/react-icons/dist/js/icons/exclamation-circle-icon";
import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { GIST_ORIGIN_REMOTE_NAME } from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
-import { useSettingsDispatch } from "../settings/SettingsContext";
import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
-import { useAuthSession } from "../authSessions/AuthSessionsContext";
-import { useBitbucketClient } from "../bitbucket/Hooks";
-import { GithubIcon, BitbucketIcon } from "@patternfly/react-icons";
-import { useGitHubClient } from "../github/Hooks";
+import { useAuthSession } from "../../../authSessions/AuthSessionsContext";
+import { useBitbucketClient } from "../../../bitbucket/Hooks";
+import { BitbucketIcon } from "@patternfly/react-icons/dist/js/icons/bitbucket-icon";
+import { GithubIcon } from "@patternfly/react-icons/dist/js/icons/github-icon";
+import { useGitHubClient } from "../../../github/Hooks";
import {
AuthProviderType,
GistEnabledAuthProviderType,
isGistEnabledAuthProviderType,
isSupportedGitAuthProviderType,
-} from "../authProviders/AuthProvidersApi";
-import { useAuthProvider } from "../authProviders/AuthProvidersContext";
-import { switchExpression } from "../switchExpression/switchExpression";
-import { useOnlineI18n } from "../i18n";
+} from "../../../authProviders/AuthProvidersApi";
+import { useAuthProvider } from "../../../authProviders/AuthProvidersContext";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { useOnlineI18n } from "../../../i18n";
import { LoadOrganizationsSelect, SelectOptionObjectType } from "./LoadOrganizationsSelect";
+import { useGitIntegration } from "./GitIntegrationContextProvider";
export interface CreateGistOrSnippetResponse {
cloneUrl: string;
@@ -52,11 +53,10 @@ export const CreateGistOrSnippetModal = (props: {
workspace: WorkspaceDescriptor;
isOpen: boolean;
onClose: () => void;
- onSuccess: (args: { url: string }) => void;
- onError: () => void;
+ onSuccess?: (args: { url: string }) => void;
+ onError?: () => void;
}) => {
const workspaces = useWorkspaces();
- const settingsDispatch = useSettingsDispatch();
const { authSession, gitConfig, authInfo } = useAuthSession(props.workspace.gitAuthSessionId);
const authProvider = useAuthProvider(authSession);
const bitbucketClient = useBitbucketClient(authSession);
@@ -68,6 +68,10 @@ export const CreateGistOrSnippetModal = (props: {
const [selectedOrganization, setSelectedOrganization] = useState<SelectOptionObjectType>();
const [isGistOrSnippetLoading, setGistOrSnippetLoading] = useState(false);
+ const {
+ alerts: { successfullyUpdatedGistOrSnippetAlert, errorAlert },
+ } = useGitIntegration();
+
const createGitHubGist: () => Promise<CreateGistOrSnippetResponse> = useCallback(async () => {
const gist = await gitHubClient.gists.create({
description: props.workspace.name ?? "",
@@ -193,16 +197,28 @@ If you are, it means that creating this Snippet failed and it can safely be dele
});
props.onClose();
- props.onSuccess({ url: gistOrSnippet.cloneUrl });
+ successfullyUpdatedGistOrSnippetAlert.show({ url: gistOrSnippet.cloneUrl });
+ props.onSuccess?.({ url: gistOrSnippet.cloneUrl });
return;
} catch (err) {
setError(err);
- props.onError();
+ errorAlert.show();
+ props.onError?.();
throw err;
} finally {
setGistOrSnippetLoading(false);
}
- }, [authInfo, authProvider?.type, createGitHubGist, createBitbucketSnippet, workspaces, props, gitConfig]);
+ }, [
+ authInfo,
+ authProvider?.type,
+ createGitHubGist,
+ createBitbucketSnippet,
+ workspaces,
+ props,
+ gitConfig,
+ successfullyUpdatedGistOrSnippetAlert,
+ errorAlert,
+ ]);
if (!authProvider?.type || !isSupportedGitAuthProviderType(authProvider?.type)) {
return <></>;
diff --git a/packages/online-editor/src/editor/CreateGitRepositoryModal.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGitRepositoryModal.tsx
similarity index 91%
rename from packages/online-editor/src/editor/CreateGitRepositoryModal.tsx
rename to packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGitRepositoryModal.tsx
index 6481dd7649..9f4e69a1f5 100644
--- a/packages/online-editor/src/editor/CreateGitRepositoryModal.tsx
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGitRepositoryModal.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 Red Hat, Inc. and/or its affiliates.
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,17 +30,18 @@ import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { ValidatedOptions } from "@patternfly/react-core/dist/js/helpers/constants";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import { GIT_ORIGIN_REMOTE_NAME } from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
-import { useSettingsDispatch } from "../settings/SettingsContext";
import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
-import { useAuthSession } from "../authSessions/AuthSessionsContext";
-import { useBitbucketClient } from "../bitbucket/Hooks";
-import { GithubIcon, BitbucketIcon } from "@patternfly/react-icons";
-import { useGitHubClient } from "../github/Hooks";
-import { isSupportedGitAuthProviderType } from "../authProviders/AuthProvidersApi";
-import { useAuthProvider } from "../authProviders/AuthProvidersContext";
-import { switchExpression } from "../switchExpression/switchExpression";
-import { useOnlineI18n } from "../i18n";
+import { useAuthSession } from "../../../authSessions/AuthSessionsContext";
+import { useBitbucketClient } from "../../../bitbucket/Hooks";
+import { BitbucketIcon } from "@patternfly/react-icons/dist/js/icons/bitbucket-icon";
+import { GithubIcon } from "@patternfly/react-icons/dist/js/icons/github-icon";
+import { useGitHubClient } from "../../../github/Hooks";
+import { isSupportedGitAuthProviderType } from "../../../authProviders/AuthProvidersApi";
+import { useAuthProvider } from "../../../authProviders/AuthProvidersContext";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { useOnlineI18n } from "../../../i18n";
import { LoadOrganizationsSelect, SelectOptionObjectType } from "./LoadOrganizationsSelect";
+import { useGitIntegration } from "./GitIntegrationContextProvider";
export interface CreateRepositoryResponse {
cloneUrl: string;
@@ -57,10 +58,9 @@ export function CreateGitRepositoryModal(props: {
workspace: WorkspaceDescriptor;
isOpen: boolean;
onClose: () => void;
- onSuccess: (args: { url: string }) => void;
+ onSuccess?: (args: { url: string }) => void;
}) {
const workspaces = useWorkspaces();
- const settingsDispatch = useSettingsDispatch();
const { authSession, gitConfig, authInfo } = useAuthSession(props.workspace.gitAuthSessionId);
const authProvider = useAuthProvider(authSession);
const bitbucketClient = useBitbucketClient(authSession);
@@ -73,6 +73,10 @@ export function CreateGitRepositoryModal(props: {
const { i18n } = useOnlineI18n();
const [selectedOrganization, setSelectedOrganization] = useState<SelectOptionObjectType>();
+ const {
+ alerts: { createRepositorySuccessAlert, errorAlert },
+ } = useGitIntegration();
+
useEffect(() => {
setName(getSuggestedRepositoryName(props.workspace.name));
}, [props.workspace.name]);
@@ -196,8 +200,10 @@ export function CreateGitRepositoryModal(props: {
});
props.onClose();
- props.onSuccess({ url: websiteUrl });
+ createRepositorySuccessAlert.show({ url: websiteUrl });
+ props.onSuccess?.({ url: websiteUrl });
} catch (err) {
+ errorAlert.show();
setError(err);
throw err;
} finally {
@@ -212,6 +218,8 @@ export function CreateGitRepositoryModal(props: {
pushEmptyCommitIntoBitbucket,
workspaces,
props,
+ createRepositorySuccessAlert,
+ errorAlert,
]);
const isNameValid = useMemo(() => {
@@ -229,6 +237,7 @@ export function CreateGitRepositoryModal(props: {
if (!authProvider?.type || !isSupportedGitAuthProviderType(authProvider?.type)) {
return <></>;
}
+
return (
<Modal
variant={ModalVariant.medium}
diff --git a/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationAlerts.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationAlerts.tsx
new file mode 100644
index 0000000000..e9867eea81
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationAlerts.tsx
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback } from "react";
+import { useGlobalAlert } from "../../../alerts";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import {
+ WorkspaceKind,
+ WorkspaceKindGistLike,
+ isGistLikeWorkspaceKind,
+} from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/js/components/Alert";
+import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { useOnlineI18n } from "../../../i18n";
+
+export function useGitIntegrationAlerts(workspace: ActiveWorkspace) {
+ const { i18n } = useOnlineI18n();
+
+ const createRepositorySuccessAlert = useGlobalAlert<{ url: string }>(
+ useCallback(({ close }, { url }) => {
+ return (
+ <Alert
+ variant="success"
+ title={`GitHub repository created.`}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ actionLinks={<AlertActionLink onClick={() => window.open(url, "_blank")}>{url}</AlertActionLink>}
+ />
+ );
+ }, [])
+ );
+
+ const pushingAlert = useGlobalAlert(
+ useCallback(() => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return (
+ <Alert
+ variant="info"
+ title={
+ <>
+ <Spinner size={"sm"} />
+ {`Pushing to '${workspace.descriptor.origin.url}'...`}
+ </>
+ }
+ />
+ );
+ }, [workspace.descriptor])
+ );
+
+ const comittingAlert = useGlobalAlert(
+ useCallback(() => {
+ return (
+ <Alert
+ variant="info"
+ title={
+ <>
+ <Spinner size={"sm"} />
+ {`Creating commit...`}
+ </>
+ }
+ />
+ );
+ }, [])
+ );
+
+ const commitSuccessAlert = useGlobalAlert(
+ useCallback(() => {
+ return <Alert variant="success" title={`Commit created.`} />;
+ }, []),
+ { durationInSeconds: 2 }
+ );
+
+ const commitFailAlert = useGlobalAlert(
+ useCallback(({ close }, staticArgs: { reason: string }) => {
+ return (
+ <Alert
+ variant="danger"
+ title={`Failed to commit: ${staticArgs.reason}`}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ />
+ );
+ }, [])
+ );
+
+ const pushSuccessAlert = useGlobalAlert(
+ useCallback(() => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return <Alert variant="success" title={`Pushed to '${workspace.descriptor.origin.url}'`} />;
+ }, [workspace.descriptor]),
+ { durationInSeconds: 4 }
+ );
+
+ const pushErrorAlert = useGlobalAlert(
+ useCallback(
+ ({ close }) => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return (
+ <Alert
+ variant="danger"
+ title={`Error Pushing to '${workspace.descriptor.origin.url}'`}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ />
+ );
+ },
+ [workspace.descriptor]
+ )
+ );
+
+ const pullingAlert = useGlobalAlert(
+ useCallback(() => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return (
+ <Alert
+ variant="info"
+ title={
+ <>
+ <Spinner size={"sm"} />
+ {`Pulling from '${workspace.descriptor.origin.url}'...`}
+ </>
+ }
+ />
+ );
+ }, [workspace.descriptor])
+ );
+
+ const pullSuccessAlert = useGlobalAlert(
+ useCallback(() => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return <Alert variant="success" title={`Pulled from '${workspace.descriptor.origin.url}'`} />;
+ }, [workspace.descriptor]),
+ { durationInSeconds: 4 }
+ );
+
+ const pullErrorAlert = useGlobalAlert<{
+ newBranchName: string;
+ canPushToGitRepository: boolean;
+ pushNewBranch: (branchName: string) => Promise<void>;
+ }>(
+ useCallback(
+ ({ close }, { newBranchName, canPushToGitRepository, pushNewBranch }) => {
+ if (workspace.descriptor.origin.kind !== WorkspaceKind.GIT) {
+ return <></>;
+ }
+
+ return (
+ <Alert
+ variant="danger"
+ title={`Error Pulling from '${workspace.descriptor.origin.url}'`}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ actionLinks={<></>}
+ >
+ {`This usually happens when your branch has conflicts with the upstream branch or you don't have permission to Pull.`}
+ <br />
+ <br />
+ {`You can save your work to a new branch.`}
+ <br />
+ <br />
+ <Tooltip
+ data-testid={"gist-it-tooltip"}
+ content={<div>{`You need select an authentication source to be able to Push to a new branch.`}</div>}
+ trigger={!canPushToGitRepository ? "mouseenter click" : ""}
+ position="left"
+ >
+ <Button
+ onClick={() => pushNewBranch(newBranchName)}
+ variant={ButtonVariant.link}
+ style={{ paddingLeft: 0 }}
+ isSmall={true}
+ isDisabled={!canPushToGitRepository}
+ >
+ {`Switch to '${newBranchName}'`}
+ </Button>
+ </Tooltip>
+ <br />
+ <br />
+
+ {`Or change the authentication source for '${workspace.descriptor.name}' and try again`}
+ <br />
+ </Alert>
+ );
+ },
+ [workspace.descriptor]
+ )
+ );
+
+ const nothingToCommitAlert = useGlobalAlert(
+ useCallback(() => {
+ return <Alert variant="info" title={"Nothing to commit."} />;
+ }, []),
+ { durationInSeconds: 2 }
+ );
+
+ const successfullyCreatedGistOrSnippetAlert = useGlobalAlert(
+ useCallback(
+ ({ close }) => {
+ if (!isGistLikeWorkspaceKind(workspace.descriptor.origin.kind)) {
+ return <></>;
+ }
+
+ const gistOrSnippetUrl = workspace.descriptor.origin.url;
+ return (
+ <Alert
+ variant="success"
+ title={switchExpression(workspace.descriptor.origin.kind, {
+ GITHUB_GIST: i18n.editorPage.alerts.createGist,
+ BITBUCKET_SNIPPET: i18n.editorPage.alerts.createSnippet,
+ })}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ actionLinks={
+ <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
+ {gistOrSnippetUrl}
+ </AlertActionLink>
+ }
+ />
+ );
+ },
+ [
+ i18n.editorPage.alerts.createGist,
+ i18n.editorPage.alerts.createSnippet,
+ workspace.descriptor.origin.kind,
+ workspace.descriptor.origin.url,
+ ]
+ ),
+ { durationInSeconds: 4 }
+ );
+
+ const loadingGistOrSnippetAlert = useGlobalAlert(
+ useCallback(
+ ({ close }) => {
+ if (!isGistLikeWorkspaceKind(workspace.descriptor.origin.kind)) {
+ return <></>;
+ }
+
+ const gistOrSnippetUrl = workspace.descriptor.origin.url;
+ return (
+ <Alert
+ variant="info"
+ title={
+ <>
+ <Spinner size={"sm"} />
+ Updating{" "}
+ {switchExpression(workspace.descriptor.origin.kind, {
+ BITBUCKET_SNIPPET: "Snippet",
+ GITHUB_GIST: "Gist",
+ })}
+ ...
+ </>
+ }
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ actionLinks={
+ <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
+ {gistOrSnippetUrl}
+ </AlertActionLink>
+ }
+ />
+ );
+ },
+ [workspace.descriptor.origin.kind, workspace.descriptor.origin.url]
+ )
+ );
+
+ const successfullyUpdatedGistOrSnippetAlert = useGlobalAlert(
+ useCallback(
+ ({ close }) => {
+ if (!isGistLikeWorkspaceKind(workspace.descriptor.origin.kind)) {
+ return <></>;
+ }
+
+ const gistOrSnippetUrl = workspace.descriptor.origin.url;
+ return (
+ <Alert
+ variant="success"
+ title={switchExpression(workspace.descriptor.origin.kind, {
+ GITHUB_GIST: i18n.editorPage.alerts.updateGist,
+ BITBUCKET_SNIPPET: i18n.editorPage.alerts.updateSnippet,
+ })}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ actionLinks={
+ <AlertActionLink onClick={() => window.open(gistOrSnippetUrl, "_blank")}>
+ {gistOrSnippetUrl}
+ </AlertActionLink>
+ }
+ />
+ );
+ },
+ [
+ i18n.editorPage.alerts.updateGist,
+ i18n.editorPage.alerts.updateSnippet,
+ workspace.descriptor.origin.kind,
+ workspace.descriptor.origin.url,
+ ]
+ ),
+ { durationInSeconds: 4 }
+ );
+
+ const errorPushingGistOrSnippet = useGlobalAlert<{ forceUpdateGistOrSnippet: () => Promise<void> }>(
+ useCallback(
+ ({ close }, { forceUpdateGistOrSnippet }) => {
+ return (
+ <Alert
+ variant="danger"
+ title={switchExpression(workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
+ GITHUB_GIST: i18n.editorPage.alerts.errorPushingGist,
+ BITBUCKET_SNIPPET: i18n.editorPage.alerts.errorPushingSnippet,
+ })}
+ actionLinks={[
+ <AlertActionLink
+ key="force"
+ onClick={() => {
+ close();
+ forceUpdateGistOrSnippet();
+ }}
+ >
+ Push forcefully
+ </AlertActionLink>,
+ <AlertActionLink key="dismiss" onClick={close}>
+ Dismiss
+ </AlertActionLink>,
+ ]}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ >
+ <b>{i18n.editorPage.alerts.forcePushWarning}</b>
+ </Alert>
+ );
+ },
+ [
+ workspace.descriptor.origin.kind,
+ i18n.editorPage.alerts.errorPushingGist,
+ i18n.editorPage.alerts.errorPushingSnippet,
+ i18n.editorPage.alerts.forcePushWarning,
+ ]
+ )
+ );
+
+ const errorAlert = useGlobalAlert(
+ useCallback(
+ ({ close }) => (
+ <Alert
+ variant="danger"
+ title={i18n.editorPage.alerts.error}
+ actionClose={<AlertActionCloseButton onClose={close} />}
+ />
+ ),
+ [i18n]
+ )
+ );
+
+ return {
+ createRepositorySuccessAlert,
+ comittingAlert,
+ commitSuccessAlert,
+ commitFailAlert,
+ pushingAlert,
+ pushSuccessAlert,
+ pushErrorAlert,
+ pullingAlert,
+ pullSuccessAlert,
+ pullErrorAlert,
+ nothingToCommitAlert,
+ successfullyCreatedGistOrSnippetAlert,
+ loadingGistOrSnippetAlert,
+ successfullyUpdatedGistOrSnippetAlert,
+ errorPushingGistOrSnippet,
+ errorAlert,
+ };
+}
diff --git a/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationContextProvider.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationContextProvider.tsx
new file mode 100644
index 0000000000..be98c9ca2c
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/GitIntegrationContextProvider.tsx
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, {
+ ReactNode,
+ SetStateAction,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import { useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { AuthSessionSelectFilter } from "../../../authSessions/AuthSessionSelect";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { useAuthSession } from "../../../authSessions/AuthSessionsContext";
+import { useAuthProvider } from "../../../authProviders/AuthProvidersContext";
+import { useGitHubClient } from "../../../github/Hooks";
+import { useBitbucketClient } from "../../../bitbucket/Hooks";
+import {
+ WorkspaceKind,
+ isGistLikeWorkspaceKind,
+} from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import {
+ authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain,
+ authSessionsSelectFilterCompatibleWithGitUrlDomain,
+ gitAuthSessionSelectFilter,
+} from "../../../authSessions/CompatibleAuthSessions";
+import type { RestEndpointMethodTypes as OctokitRestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types";
+import { useCancelableEffect } from "@kie-tools-core/react-hooks/dist/useCancelableEffect";
+import { ImportableUrl, UrlType, useImportableUrl } from "../../../importFromUrl/ImportableUrlHooks";
+import {
+ GIST_ORIGIN_REMOTE_NAME,
+ GIT_ORIGIN_REMOTE_NAME,
+} from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
+import { useNavigationBlockersBypass, useRoutes } from "../../../navigation/Hooks";
+import { useHistory } from "react-router";
+import { useGitIntegrationAlerts } from "./GitIntegrationAlerts";
+import { isGistEnabledAuthProviderType } from "../../../authProviders/AuthProvidersApi";
+import { useEditorToolbarDispatchContext } from "../EditorToolbarContextProvider";
+
+export type GitIntegrationContextType = {
+ workspaceImportableUrl: ImportableUrl;
+ git: {
+ canPushToGitRepository: boolean;
+ canCreateGitRepository: boolean;
+ pushNewBranch: (newBranchName: string) => Promise<void>;
+ pushToGitRepository: () => Promise<void>;
+ pullFromGitRepository: (args: { showAlerts: boolean }) => Promise<void>;
+ };
+ gistOrSnippet: {
+ canCreateGistOrSnippet: boolean;
+ canUpdateGistOrSnippet: boolean;
+ canForkGitHubGist: boolean;
+ forceUpdateGistOrSnippet: () => Promise<void>;
+ updateGistOrSnippet: () => Promise<void>;
+ forkGitHubGist: () => Promise<void>;
+ };
+ auth: {
+ changeGitAuthSessionId: (
+ newGitAuthSessionId: SetStateAction<string | undefined>,
+ lastAuthSessionId: string | undefined
+ ) => void;
+ authSessionSelectFilter: AuthSessionSelectFilter;
+ };
+ alerts: ReturnType<typeof useGitIntegrationAlerts>;
+};
+
+export const GitIntegrationContext = createContext<GitIntegrationContextType>({} as any);
+
+export type GitIntegrationContextProviderProps = {
+ children: ReactNode;
+ workspace: ActiveWorkspace;
+};
+
+export function GitIntegrationContextProvider(props: GitIntegrationContextProviderProps) {
+ const workspaces = useWorkspaces();
+ const routes = useRoutes();
+ const history = useHistory();
+ const { authSession, authInfo, gitConfig } = useAuthSession(props.workspace.descriptor.gitAuthSessionId);
+ const authProvider = useAuthProvider(authSession);
+ const workspaceImportableUrl = useImportableUrl(props.workspace.descriptor.origin.url?.toString());
+ const alerts = useGitIntegrationAlerts(props.workspace);
+ const navigationBlockersBypass = useNavigationBlockersBypass();
+ const { setSyncGistOrSnippetDropdownOpen } = useEditorToolbarDispatchContext();
+
+ const gitHubClient = useGitHubClient(authSession);
+ const bitbucketClient = useBitbucketClient(authSession);
+
+ const [isGistOrSnippetLoading, setGistOrSnippetLoading] = useState(false);
+ const [gitHubGist, setGitHubGist] =
+ useState<OctokitRestEndpointMethodTypes["gists"]["get"]["response"]["data"] | undefined>(undefined);
+ const [bitbucketSnippet, setBitbucketSnippet] = useState<any>(undefined);
+
+ useCancelableEffect(
+ useCallback(
+ ({ canceled }) => {
+ if (gitHubGist || workspaceImportableUrl.type !== UrlType.GIST_DOT_GITHUB_DOT_COM) {
+ return;
+ }
+
+ const { gistId } = workspaceImportableUrl;
+
+ if (!gistId) {
+ return;
+ }
+
+ gitHubClient.gists.get({ gist_id: gistId }).then(({ data: gist }) => {
+ if (canceled.get()) {
+ return;
+ }
+
+ if (gist) {
+ setGitHubGist(gist);
+ }
+ });
+ },
+ [gitHubGist, workspaceImportableUrl, gitHubClient.gists]
+ )
+ );
+
+ useCancelableEffect(
+ useCallback(
+ ({ canceled }) => {
+ if (bitbucketSnippet || workspaceImportableUrl.type !== UrlType.BITBUCKET_DOT_ORG_SNIPPET) {
+ return;
+ }
+
+ const { snippetId, org } = workspaceImportableUrl;
+
+ if (!snippetId || !org) {
+ return;
+ }
+
+ bitbucketClient.getSnippet({ workspace: org, snippetId }).then(async (response) => {
+ if (canceled.get()) {
+ return;
+ }
+ const json = await response.json();
+ setBitbucketSnippet(json);
+ });
+ },
+ [bitbucketSnippet, workspaceImportableUrl, bitbucketClient]
+ )
+ );
+
+ const authSessionSelectFilter = useMemo(() => {
+ if (!props.workspace) {
+ return gitAuthSessionSelectFilter();
+ }
+
+ if (props.workspace.descriptor.origin.kind === WorkspaceKind.LOCAL) {
+ return gitAuthSessionSelectFilter();
+ }
+
+ if (props.workspace.descriptor.origin.kind === WorkspaceKind.GIT) {
+ return authSessionsSelectFilterCompatibleWithGitUrlDomain(
+ new URL(props.workspace.descriptor.origin.url).hostname
+ );
+ }
+
+ if (props.workspace.descriptor.origin.kind === WorkspaceKind.GITHUB_GIST) {
+ return authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain(
+ new URL(props.workspace.descriptor.origin.url).hostname,
+ gitHubGist?.owner?.login
+ );
+ }
+
+ if (props.workspace.descriptor.origin.kind === WorkspaceKind.BITBUCKET_SNIPPET) {
+ return authSessionsSelectFilterCompatibleWithGistOrSnippetUrlDomain(
+ new URL(props.workspace.descriptor.origin.url).hostname,
+ bitbucketSnippet?.owner?.login
+ );
+ }
+
+ return gitAuthSessionSelectFilter();
+ }, [bitbucketSnippet?.owner?.login, gitHubGist?.owner?.login, props.workspace]);
+
+ const changeGitAuthSessionId = useCallback(
+ (newGitAuthSessionId: React.SetStateAction<string | undefined>, lastAuthSessionId: string | undefined) => {
+ workspaces.changeGitAuthSessionId({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ gitAuthSessionId:
+ typeof newGitAuthSessionId === "function" ? newGitAuthSessionId(lastAuthSessionId) : newGitAuthSessionId,
+ });
+ },
+ [props.workspace.descriptor.workspaceId, workspaces]
+ );
+
+ const workspaceHasNestedDirectories = useMemo(
+ () => props.workspace.files.filter((f) => f.relativePath !== f.name).length !== 0,
+ [props.workspace]
+ );
+
+ const canCreateGitHubRepository = useMemo(() => authProvider?.type === "github", [authProvider?.type]);
+
+ const canCreateBitbucketRepository = useMemo(() => authProvider?.type === "bitbucket", [authProvider?.type]);
+
+ const canCreateGitRepository = useMemo(
+ () => canCreateGitHubRepository || canCreateBitbucketRepository,
+ [canCreateGitHubRepository, canCreateBitbucketRepository]
+ );
+
+ const canPushToGitRepository = useMemo(
+ () => authSession?.type === "git" && !!authProvider,
+ [authProvider, authSession?.type]
+ );
+
+ const isGitHubGistOwner = useMemo(() => {
+ return authInfo?.username && gitHubGist?.owner?.login === authInfo.username;
+ }, [authInfo, gitHubGist]);
+
+ const isBitbucketSnippetOwner = useMemo(() => {
+ return authInfo?.uuid && bitbucketSnippet?.creator.uuid === authInfo.uuid;
+ }, [authInfo, bitbucketSnippet]);
+
+ const isGistOrSnippetOwner = isGitHubGistOwner || isBitbucketSnippetOwner;
+
+ const canCreateGistOrSnippet = useMemo(
+ () =>
+ Boolean(
+ authProvider &&
+ isGistEnabledAuthProviderType(authProvider?.type) &&
+ props.workspace.descriptor.origin.kind === WorkspaceKind.LOCAL &&
+ !workspaceHasNestedDirectories
+ ),
+ [authProvider, props.workspace.descriptor.origin.kind, workspaceHasNestedDirectories]
+ );
+
+ const canUpdateGistOrSnippet = useMemo(
+ () =>
+ Boolean(
+ authProvider &&
+ isGistEnabledAuthProviderType(authProvider?.type) &&
+ !!isGistOrSnippetOwner &&
+ props.workspace &&
+ isGistLikeWorkspaceKind(props.workspace.descriptor.origin.kind) &&
+ !workspaceHasNestedDirectories
+ ),
+ [authProvider, isGistOrSnippetOwner, props.workspace, workspaceHasNestedDirectories]
+ );
+
+ const canForkGitHubGist = useMemo(
+ () =>
+ authProvider?.type === "github" &&
+ !isGistOrSnippetOwner &&
+ props.workspace.descriptor.origin.kind === WorkspaceKind.GITHUB_GIST &&
+ !workspaceHasNestedDirectories,
+ [authProvider?.type, isGistOrSnippetOwner, props.workspace.descriptor.origin.kind, workspaceHasNestedDirectories]
+ );
+
+ const pushNewBranch = useCallback(
+ async (newBranchName: string) => {
+ if (!authInfo || !props.workspace) {
+ return;
+ }
+
+ try {
+ alerts.pushingAlert.show();
+
+ await workspaces.createSavePoint({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ gitConfig,
+ });
+
+ await workspaces.branch({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ checkout: false,
+ name: newBranchName,
+ });
+
+ await workspaces.push({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ remote: GIT_ORIGIN_REMOTE_NAME,
+ remoteRef: `refs/heads/${newBranchName}`,
+ ref: newBranchName,
+ force: false,
+ authInfo,
+ });
+
+ history.push({
+ pathname: routes.import.path({}),
+ search: routes.import.queryString({
+ url: `${props.workspace.descriptor.origin.url}`,
+ branch: newBranchName,
+ authSessionId: props.workspace.descriptor.gitAuthSessionId,
+ }),
+ });
+ } catch (e) {
+ alerts.pushErrorAlert.show();
+ } finally {
+ alerts.pushingAlert.close();
+ }
+ },
+ [
+ authInfo,
+ props.workspace,
+ alerts.pushingAlert,
+ alerts.pushErrorAlert,
+ workspaces,
+ gitConfig,
+ history,
+ routes.import,
+ ]
+ );
+
+ const pullFromGitRepository = useCallback(
+ async (args: { showAlerts: boolean }) => {
+ alerts.pullingAlert.close();
+ alerts.pullErrorAlert.close();
+ alerts.pullSuccessAlert.close();
+
+ if (args.showAlerts) {
+ alerts.pullingAlert.show();
+ }
+ await workspaces.createSavePoint({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ gitConfig,
+ });
+
+ try {
+ await workspaces.pull({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ authInfo,
+ });
+
+ if (args.showAlerts) {
+ alerts.pullSuccessAlert.show();
+ }
+ } catch (e) {
+ console.error(e);
+ if (args.showAlerts) {
+ const randomString = (Math.random() + 1).toString(36).substring(7);
+ const newBranchName = `${props.workspace.descriptor.origin.branch}-${randomString}`;
+ alerts.pullErrorAlert.show({ newBranchName, canPushToGitRepository, pushNewBranch });
+ }
+ } finally {
+ if (args.showAlerts) {
+ alerts.pullingAlert.close();
+ }
+ }
+ },
+ [
+ alerts.pullingAlert,
+ alerts.pullErrorAlert,
+ alerts.pullSuccessAlert,
+ workspaces,
+ props.workspace.descriptor.workspaceId,
+ props.workspace.descriptor.origin.branch,
+ gitConfig,
+ authInfo,
+ canPushToGitRepository,
+ pushNewBranch,
+ ]
+ );
+
+ const pushToGitRepository = useCallback(async () => {
+ alerts.pushingAlert.close();
+ alerts.pushErrorAlert.close();
+ alerts.pushSuccessAlert.close();
+
+ if (!authInfo) {
+ return;
+ }
+
+ alerts.pushingAlert.show();
+ try {
+ const workspaceId = props.workspace.descriptor.workspaceId;
+ await workspaces.createSavePoint({
+ workspaceId: workspaceId,
+ gitConfig,
+ });
+
+ const workspace = await workspaces.getWorkspace({ workspaceId });
+ await workspaces.push({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ ref: workspace.origin.branch,
+ remote: GIST_ORIGIN_REMOTE_NAME,
+ remoteRef: `refs/heads/${workspace.origin.branch}`,
+ force: false,
+ authInfo,
+ });
+ await pullFromGitRepository({ showAlerts: false });
+ alerts.pushSuccessAlert.show();
+ } catch (e) {
+ console.error(e);
+ alerts.pushErrorAlert.show();
+ } finally {
+ alerts.pushingAlert.close();
+ }
+ }, [
+ alerts.pushingAlert,
+ alerts.pushErrorAlert,
+ alerts.pushSuccessAlert,
+ authInfo,
+ props.workspace.descriptor.workspaceId,
+ workspaces,
+ gitConfig,
+ pullFromGitRepository,
+ ]);
+
+ const forceUpdateGistOrSnippet = useCallback(async () => {
+ try {
+ if (!authInfo || !props.workspace) {
+ return;
+ }
+
+ setGistOrSnippetLoading(true);
+
+ await workspaces.push({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ remote: GIST_ORIGIN_REMOTE_NAME,
+ ref: props.workspace.descriptor.origin.branch,
+ remoteRef: `refs/heads/${props.workspace.descriptor.origin.branch}`,
+ force: true,
+ authInfo,
+ });
+
+ await workspaces.pull({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ authInfo,
+ });
+ } catch (e) {
+ alerts.errorAlert.show();
+ } finally {
+ setGistOrSnippetLoading(false);
+ setSyncGistOrSnippetDropdownOpen(false);
+ }
+
+ alerts.successfullyUpdatedGistOrSnippetAlert.show();
+ }, [
+ alerts.errorAlert,
+ alerts.successfullyUpdatedGistOrSnippetAlert,
+ authInfo,
+ props.workspace,
+ setSyncGistOrSnippetDropdownOpen,
+ workspaces,
+ ]);
+
+ const updateGistOrSnippet = useCallback(async () => {
+ try {
+ if (
+ !authInfo ||
+ !props.workspace ||
+ !isGistEnabledAuthProviderType(authProvider?.type) ||
+ !isGistLikeWorkspaceKind(props.workspace.descriptor.origin.kind)
+ ) {
+ return;
+ }
+ setGistOrSnippetLoading(true);
+
+ await workspaces.createSavePoint({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ gitConfig,
+ });
+
+ await workspaces.push({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ remote: GIST_ORIGIN_REMOTE_NAME,
+ ref: props.workspace.descriptor.origin.branch,
+ remoteRef: `refs/heads/${props.workspace.descriptor.origin.branch}`,
+ force: false,
+ authInfo,
+ });
+
+ await workspaces.pull({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ authInfo,
+ });
+ } catch (e) {
+ alerts.errorPushingGistOrSnippet.show({ forceUpdateGistOrSnippet });
+ throw e;
+ } finally {
+ setGistOrSnippetLoading(false);
+ setSyncGistOrSnippetDropdownOpen(false);
+ }
+
+ alerts.successfullyUpdatedGistOrSnippetAlert.show();
+ }, [
+ alerts.successfullyUpdatedGistOrSnippetAlert,
+ alerts.errorPushingGistOrSnippet,
+ authInfo,
+ props.workspace,
+ authProvider?.type,
+ workspaces,
+ gitConfig,
+ forceUpdateGistOrSnippet,
+ setSyncGistOrSnippetDropdownOpen,
+ ]);
+
+ const forkGitHubGist = useCallback(async () => {
+ try {
+ if (!authSession || !authInfo || !gitHubGist?.id || !props.workspace) {
+ return;
+ }
+
+ setGistOrSnippetLoading(true);
+
+ // Fork Gist
+ const gist = await gitHubClient.gists.fork({
+ gist_id: gitHubGist.id,
+ });
+
+ const remoteName = gist.data.id;
+
+ // Adds forked gist remote to current one
+ await workspaces.addRemote({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ url: gist.data.git_push_url,
+ name: remoteName,
+ force: true,
+ });
+
+ // Commit
+ await workspaces.createSavePoint({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ gitConfig,
+ });
+
+ // Push to forked gist remote
+ await workspaces.push({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ remote: remoteName,
+ ref: props.workspace.descriptor.origin.branch,
+ remoteRef: `refs/heads/${props.workspace.descriptor.origin.branch}`,
+ force: true,
+ authInfo,
+ });
+
+ // Redirect to import workspace
+ navigationBlockersBypass.execute(() => {
+ history.push({
+ pathname: routes.import.path({}),
+ search: routes.import.queryString({
+ url: gist.data.html_url,
+ authSessionId: authSession.id,
+ }),
+ });
+ });
+ } catch (err) {
+ alerts.errorAlert.show();
+ throw err;
+ } finally {
+ setGistOrSnippetLoading(false);
+ }
+ }, [
+ authSession,
+ authInfo,
+ gitHubGist,
+ props.workspace,
+ gitHubClient.gists,
+ workspaces,
+ gitConfig,
+ navigationBlockersBypass,
+ history,
+ routes.import,
+ alerts.errorAlert,
+ ]);
+
+ useEffect(() => {
+ if (isGistOrSnippetLoading) {
+ alerts.loadingGistOrSnippetAlert.show();
+ } else {
+ alerts.loadingGistOrSnippetAlert.close();
+ }
+ }, [alerts.loadingGistOrSnippetAlert, isGistOrSnippetLoading]);
+
+ const value = useMemo(
+ () => ({
+ workspaceImportableUrl,
+ git: {
+ canPushToGitRepository,
+ canCreateGitRepository,
+ pushNewBranch,
+ pushToGitRepository,
+ pullFromGitRepository,
+ },
+ gistOrSnippet: {
+ canCreateGistOrSnippet,
+ canUpdateGistOrSnippet,
+ canForkGitHubGist,
+ forceUpdateGistOrSnippet,
+ updateGistOrSnippet,
+ forkGitHubGist,
+ },
+ auth: {
+ authSessionSelectFilter,
+ changeGitAuthSessionId,
+ },
+ alerts,
+ }),
+ [
+ workspaceImportableUrl,
+ canPushToGitRepository,
+ canCreateGitRepository,
+ pushNewBranch,
+ pushToGitRepository,
+ pullFromGitRepository,
+ canCreateGistOrSnippet,
+ canUpdateGistOrSnippet,
+ canForkGitHubGist,
+ forceUpdateGistOrSnippet,
+ updateGistOrSnippet,
+ forkGitHubGist,
+ authSessionSelectFilter,
+ changeGitAuthSessionId,
+ alerts,
+ ]
+ );
+
+ return <GitIntegrationContext.Provider value={value}>{props.children}</GitIntegrationContext.Provider>;
+}
+
+export function useGitIntegration() {
+ return useContext(GitIntegrationContext);
+}
diff --git a/packages/online-editor/src/editor/LoadOrganizationsSelect.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/LoadOrganizationsSelect.tsx
similarity index 82%
rename from packages/online-editor/src/editor/LoadOrganizationsSelect.tsx
rename to packages/online-editor/src/editor/Toolbar/GitIntegration/LoadOrganizationsSelect.tsx
index 43ec3f76d4..15aa56d71a 100644
--- a/packages/online-editor/src/editor/LoadOrganizationsSelect.tsx
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/LoadOrganizationsSelect.tsx
@@ -14,28 +14,25 @@
* limitations under the License.
*/
-import * as React from "react";
-import { useMemo } from "react";
+import React, { useMemo } from "react";
import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
-import {
- Button,
- Divider,
- Flex,
- FlexItem,
- Select,
- SelectGroup,
- SelectOption,
- SelectVariant,
-} from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Select, SelectGroup, SelectOption, SelectVariant } from "@patternfly/react-core/dist/js/components/Select";
import { useCallback, useEffect, useState } from "react";
-import { AuthProviderType, isSupportedGitAuthProviderType } from "../authProviders/AuthProvidersApi";
-import { useAuthProvider } from "../authProviders/AuthProvidersContext";
-import { useAuthSession } from "../authSessions/AuthSessionsContext";
-import { useBitbucketClient } from "../bitbucket/Hooks";
-import { useGitHubClient } from "../github/Hooks";
-import { switchExpression } from "../switchExpression/switchExpression";
-import { BitbucketIcon, GithubIcon, SyncAltIcon, UserAltIcon, UserIcon, UsersIcon } from "@patternfly/react-icons";
-import { useOnlineI18n } from "../i18n";
+import { AuthProviderType, isSupportedGitAuthProviderType } from "../../../authProviders/AuthProvidersApi";
+import { useAuthProvider } from "../../../authProviders/AuthProvidersContext";
+import { useAuthSession } from "../../../authSessions/AuthSessionsContext";
+import { useBitbucketClient } from "../../../bitbucket/Hooks";
+import { useGitHubClient } from "../../../github/Hooks";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { BitbucketIcon } from "@patternfly/react-icons/dist/js/icons/bitbucket-icon";
+import { GithubIcon } from "@patternfly/react-icons/dist/js/icons/github-icon";
+import { SyncAltIcon } from "@patternfly/react-icons/dist/js/icons/sync-alt-icon";
+import { UserIcon } from "@patternfly/react-icons/dist/js/icons/user-icon";
+import { UsersIcon } from "@patternfly/react-icons/dist/js/icons/users-icon";
+import { useOnlineI18n } from "../../../i18n";
export interface LoadOrganizationsReponse {
organizations: {
@@ -105,12 +102,13 @@ export const LoadOrganizationsSelect = (props: Props) => {
return { organizations: values.map((it) => ({ name: it.workspace.slug })) };
}, [bitbucketClient]);
+ const { onSelect } = props;
const setSelectedOption = useCallback(
(option?: SelectOptionObjectType) => {
- props.onSelect(option);
+ onSelect(option);
setInternalSelectedOption(option);
},
- [props]
+ [onSelect]
);
const selectDefaultOption = useCallback(
@@ -125,7 +123,7 @@ export const LoadOrganizationsSelect = (props: Props) => {
})
);
},
- [authInfo?.username, authProvider?.type]
+ [authInfo, authProvider?.type, setSelectedOption]
);
const loadOrganizations = useCallback(() => {
@@ -152,9 +150,9 @@ export const LoadOrganizationsSelect = (props: Props) => {
if (authProvider && isSupportedGitAuthProviderType(authProvider?.type)) {
loadOrganizations();
}
- }, [loadOrganizations, authProvider?.type, authProvider]);
+ }, [loadOrganizations, setSelectedOption, authProvider]);
- const selectOptions = useCallback(() => {
+ const selectOptions = useMemo(() => {
const options: JSX.Element[] = [];
if (!authProvider || !isSupportedGitAuthProviderType(authProvider.type)) {
return options;
@@ -215,7 +213,7 @@ export const LoadOrganizationsSelect = (props: Props) => {
selections={internalSelectedOption}
isDisabled={props.readonly}
>
- {selectOptions()}
+ {selectOptions}
</Select>
)}
</FlexItem>
diff --git a/packages/online-editor/src/editor/Toolbar/GitIntegration/PushToGitAlertActionLinks.tsx b/packages/online-editor/src/editor/Toolbar/GitIntegration/PushToGitAlertActionLinks.tsx
new file mode 100644
index 0000000000..6ab16f1801
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/GitIntegration/PushToGitAlertActionLinks.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useMemo } from "react";
+import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
+import { AuthSessionSelect, AuthSessionSelectFilter } from "../../../authSessions/AuthSessionSelect";
+import { AccountsDispatchActionKind, useAccountsDispatch } from "../../../accounts/AccountsContext";
+import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { Alert, AlertActionLink } from "@patternfly/react-core/dist/js/components/Alert";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { SelectPosition } from "@patternfly/react-core/dist/js/components/Select";
+import { AuthProviderGroup } from "../../../authProviders/AuthProvidersApi";
+
+export function PushToGitAlertActionLinks(props: {
+ onPush: () => void;
+ canPush?: boolean;
+ remoteRef?: string;
+ workspaceDescriptor: WorkspaceDescriptor | undefined;
+ changeGitAuthSessionId: (a: React.SetStateAction<string | undefined>, b: string | undefined) => void;
+ authSessionSelectFilter: AuthSessionSelectFilter;
+}) {
+ const accountsDispatch = useAccountsDispatch();
+ if (props.workspaceDescriptor?.origin.kind === WorkspaceKind.GIT && !props.remoteRef) {
+ throw new Error("Should specify remoteRef for GIT workspaces");
+ }
+
+ const pushButton = useMemo(
+ () => (
+ <AlertActionLink onClick={props.onPush} style={{ fontWeight: "bold" }} isDisabled={!props.canPush}>
+ {props.workspaceDescriptor
+ ? switchExpression(props.workspaceDescriptor.origin.kind, {
+ GIT: `Push to '${props.remoteRef}'`,
+ GITHUB_GIST: "Update Gist",
+ BITBUCKET_SNIPPET: "Update Snippet",
+ default: "",
+ })
+ : ""}
+ </AlertActionLink>
+ ),
+ [props]
+ );
+
+ return (
+ <>
+ {!props.canPush && (
+ <Alert
+ isInline={true}
+ variant={"default"}
+ title={"Can't Push without selecting an authentication source"}
+ actionLinks={
+ <>
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspaceDescriptor?.name}'...`}
+ position={SelectPosition.right}
+ isPlain={false}
+ authSessionId={props.workspaceDescriptor?.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ props.changeGitAuthSessionId(newAuthSessionId, props.workspaceDescriptor?.gitAuthSessionId);
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ }}
+ filter={props.authSessionSelectFilter}
+ />
+ <br />
+ <br />
+ {pushButton}
+ </>
+ }
+ >
+ {`Select an authentication source for '${props.workspaceDescriptor?.name}' to be able to Push.`}
+ <br />
+ </Alert>
+ )}
+ {props.canPush && pushButton}
+ </>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/KebabDropdown.tsx b/packages/online-editor/src/editor/Toolbar/KebabDropdown.tsx
new file mode 100644
index 0000000000..0743a3b0a4
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/KebabDropdown.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import { Dropdown, DropdownPosition, DropdownToggle } from "@patternfly/react-core/dist/js/components/Dropdown";
+import EllipsisVIcon from "@patternfly/react-icons/dist/js/icons/ellipsis-v-icon";
+
+export function KebabDropdown(props: {
+ id: string;
+ items: React.ReactNode[];
+ state: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
+}) {
+ return (
+ <Dropdown
+ className={"kie-tools--masthead-hoverable"}
+ isOpen={props.state[0]}
+ isPlain={true}
+ position={DropdownPosition.right}
+ onSelect={() => props.state[1](false)}
+ toggle={
+ <DropdownToggle
+ id={props.id}
+ toggleIndicator={null}
+ onToggle={(isOpen) => props.state[1](isOpen)}
+ ouiaId={props.id}
+ >
+ <EllipsisVIcon />
+ </DropdownToggle>
+ }
+ dropdownItems={props.items}
+ />
+ );
+}
diff --git a/packages/online-editor/src/editor/NewFileDropdownMenu.tsx b/packages/online-editor/src/editor/Toolbar/NewFileDropdownMenu.tsx
similarity index 81%
rename from packages/online-editor/src/editor/NewFileDropdownMenu.tsx
rename to packages/online-editor/src/editor/Toolbar/NewFileDropdownMenu.tsx
index 17e88deb70..14094d632c 100644
--- a/packages/online-editor/src/editor/NewFileDropdownMenu.tsx
+++ b/packages/online-editor/src/editor/Toolbar/NewFileDropdownMenu.tsx
@@ -17,7 +17,7 @@
import * as React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
-import { FileLabel } from "../filesList/FileLabel";
+import { FileLabel } from "../../filesList/FileLabel";
import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import {
@@ -32,22 +32,25 @@ import {
import {
SupportedFileExtensions,
useEditorEnvelopeLocator,
-} from "../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
+} from "../../envelopeLocator/hooks/EditorEnvelopeLocatorContext";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert";
import { basename, extname } from "path";
-import { ImportSingleFileFromUrlForm } from "../importFromUrl/ImportSingleFileFromUrlForm";
-import { ImportableUrl, UrlType, useImportableUrl } from "../importFromUrl/ImportableUrlHooks";
-import { useRoutes } from "../navigation/Hooks";
-import { fetchSingleFileContent } from "../importFromUrl/fetchSingleFileContent";
-import { useAuthSession, useAuthSessions } from "../authSessions/AuthSessionsContext";
-import { useGitHubClient } from "../github/Hooks";
-import { useAuthProviders } from "../authProviders/AuthProvidersContext";
-import { getCompatibleAuthSessionWithUrlDomain } from "../authSessions/CompatibleAuthSessions";
+import { ImportSingleFileFromUrlForm } from "../../importFromUrl/ImportSingleFileFromUrlForm";
+import { ImportableUrl, UrlType, useImportableUrl } from "../../importFromUrl/ImportableUrlHooks";
+import { useRoutes } from "../../navigation/Hooks";
+import { fetchSingleFileContent } from "../../importFromUrl/fetchSingleFileContent";
+import { useAuthSession, useAuthSessions } from "../../authSessions/AuthSessionsContext";
+import { useGitHubClient } from "../../github/Hooks";
+import { useAuthProviders } from "../../authProviders/AuthProvidersContext";
+import { getCompatibleAuthSessionWithUrlDomain } from "../../authSessions/CompatibleAuthSessions";
import { decoder } from "@kie-tools-core/workspaces-git-fs/dist/encoderdecoder/EncoderDecoder";
import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
-import { useGlobalAlert } from "../alerts";
-import { useBitbucketClient } from "../bitbucket/Hooks";
+import { useGlobalAlert } from "../../alerts";
+import { useBitbucketClient } from "../../bitbucket/Hooks";
+import { isEditable } from "../../envelopeLocator/EditorEnvelopeLocatorFactory";
+
+const ROOT_MENU_ID = "addFileRootMenu";
export function NewFileDropdownMenu(props: {
destinationDirPath: string;
@@ -59,28 +62,33 @@ export function NewFileDropdownMenu(props: {
const [menuDrilledIn, setMenuDrilledIn] = useState<string[]>([]);
const [drilldownPath, setDrilldownPath] = useState<string[]>([]);
const [menuHeights, setMenuHeights] = useState<{ [key: string]: number }>({});
- const [activeMenu, setActiveMenu] = useState("addFileRootMenu");
+ const [activeMenu, setActiveMenu] = useState(ROOT_MENU_ID);
- const drillIn = useCallback((fromMenuId, toMenuId, pathId) => {
+ const drillIn = useCallback((_event, fromMenuId, toMenuId, pathId) => {
setMenuDrilledIn((prev) => [...prev, fromMenuId]);
setDrilldownPath((prev) => [...prev, pathId]);
setActiveMenu(toMenuId);
}, []);
- const drillOut = useCallback((toMenuId) => {
+ const drillOut = useCallback((_event, toMenuId) => {
setMenuDrilledIn((prev) => prev.slice(0, prev.length - 1));
setDrilldownPath((prev) => prev.slice(0, prev.length - 1));
setActiveMenu(toMenuId);
}, []);
const setHeight = useCallback((menuId: string, height: number) => {
- // do not try to simplify this ternary's condition as some heights are 0, resulting in an infinite loop.
- setMenuHeights((prev) => (prev[menuId] !== undefined ? prev : { ...prev, [menuId]: height }));
+ setMenuHeights((prev) => {
+ if (prev[menuId] === undefined || (menuId !== ROOT_MENU_ID && prev[menuId] !== height)) {
+ return { ...prev, [menuId]: height };
+ }
+ return prev;
+ });
}, []);
const workspaces = useWorkspaces();
const routes = useRoutes();
const editorEnvelopeLocator = useEditorEnvelopeLocator();
+ const { gitConfig } = useAuthSession(props.workspaceDescriptor.gitAuthSessionId);
const addEmptyFile = useCallback(
async (extension: SupportedFileExtensions) => {
@@ -147,12 +155,30 @@ export function NewFileDropdownMenu(props: {
})
);
+ // Since non-editable files are not checked for changes, manually stage and commit these files
+ await Promise.all(
+ uploadedFiles.map(async (file) => {
+ if (!isEditable(file.relativePath)) {
+ return workspaces.stageFile({
+ workspaceId: props.workspaceDescriptor.workspaceId,
+ relativePath: file.relativePath,
+ });
+ }
+ })
+ );
+ await workspaces.createSavePoint({
+ workspaceId: props.workspaceDescriptor.workspaceId,
+ gitConfig,
+ commitMessage: `KIE Sandbox: Added files${uploadedFiles.map((file) => `\n- ${file.relativePath}`)}`,
+ forceHasChanges: true,
+ });
+
const fileToGoTo = uploadedFiles.filter((file) => editorEnvelopeLocator.hasMappingFor(file.relativePath)).pop();
await props.onAddFile(fileToGoTo);
successfullyUploadedAlert.show({ qtt: uploadedFiles.length });
},
- [editorEnvelopeLocator, workspaces, props, successfullyUploadedAlert]
+ [editorEnvelopeLocator, workspaces, props, successfullyUploadedAlert, gitConfig]
);
const [url, setUrl] = useState("");
@@ -247,7 +273,7 @@ export function NewFileDropdownMenu(props: {
<Menu
tabIndex={1}
style={{ boxShadow: "none", minWidth: "400px" }}
- id="addFileRootMenu"
+ id={ROOT_MENU_ID}
containsDrilldown={true}
onDrillIn={drillIn}
onDrillOut={drillOut}
@@ -257,7 +283,7 @@ export function NewFileDropdownMenu(props: {
drilledInMenus={menuDrilledIn}
>
<MenuContent menuHeight={`${menuHeights[activeMenu]}px`}>
- <MenuList>
+ <MenuList style={{ padding: 0 }}>
<MenuItem
itemId={"newBpmnItemId"}
onClick={() => addEmptyFile("bpmn")}
@@ -267,28 +293,24 @@ export function NewFileDropdownMenu(props: {
<FileLabel style={{ marginBottom: "4px" }} extension={"bpmn"} />
</b>
</MenuItem>
- <MenuGroup label={" "}>
- <MenuItem
- itemId={"newDmnItemId"}
- onClick={() => addEmptyFile("dmn")}
- description="DMN files are used to generate decision models"
- >
- <b>
- <FileLabel style={{ marginBottom: "4px" }} extension={"dmn"} />
- </b>
- </MenuItem>
- </MenuGroup>
- <MenuGroup label={" "}>
- <MenuItem
- itemId={"newPmmlItemId"}
- onClick={() => addEmptyFile("pmml")}
- description="PMML files are used to generate scorecards"
- >
- <b>
- <FileLabel style={{ marginBottom: "4px" }} extension={"pmml"} />
- </b>
- </MenuItem>
- </MenuGroup>
+ <MenuItem
+ itemId={"newDmnItemId"}
+ onClick={() => addEmptyFile("dmn")}
+ description="DMN files are used to generate decision models"
+ >
+ <b>
+ <FileLabel style={{ marginBottom: "4px" }} extension={"dmn"} />
+ </b>
+ </MenuItem>
+ <MenuItem
+ itemId={"newPmmlItemId"}
+ onClick={() => addEmptyFile("pmml")}
+ description="PMML files are used to generate scorecards"
+ >
+ <b>
+ <FileLabel style={{ marginBottom: "4px" }} extension={"pmml"} />
+ </b>
+ </MenuItem>
<Divider />
<MenuItem
description={"Try sample models"}
diff --git a/packages/online-editor/src/editor/Toolbar/Share/DownloadDropdownGroup.tsx b/packages/online-editor/src/editor/Toolbar/Share/DownloadDropdownGroup.tsx
new file mode 100644
index 0000000000..ed0d17e583
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Share/DownloadDropdownGroup.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useEffect, useMemo } from "react";
+import { DropdownGroup, DropdownItem } from "@patternfly/react-core/dist/js/components/Dropdown";
+import DownloadIcon from "@patternfly/react-icons/dist/js/icons/download-icon";
+import FolderIcon from "@patternfly/react-icons/dist/js/icons/folder-icon";
+import ImageIcon from "@patternfly/react-icons/dist/js/icons/image-icon";
+import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { useGlobalAlertsDispatchContext } from "../../../alerts";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { EmbeddedEditorRef } from "@kie-tools-core/editor/dist/embedded";
+import { useEditorToolbarContext } from "../EditorToolbarContextProvider";
+
+type Props = {
+ workspaceFile: WorkspaceFile;
+ workspace: ActiveWorkspace;
+ editor: EmbeddedEditorRef | undefined;
+};
+
+export const DownloadDropdownGroup = (props: Props) => {
+ const alertsDispatch = useGlobalAlertsDispatchContext();
+ const { downloadRef, downloadAllRef, downloadPreviewRef, downloadWorkspaceZip } = useEditorToolbarContext();
+
+ useEffect(() => {
+ if (!props.workspaceFile) {
+ return;
+ }
+ if (downloadRef.current) {
+ downloadRef.current.download = `${props.workspaceFile.name}`;
+ }
+ if (downloadAllRef.current) {
+ downloadAllRef.current.download = `${props.workspace.descriptor.name}.zip`;
+ }
+ if (downloadPreviewRef.current) {
+ downloadPreviewRef.current.download = `${props.workspaceFile.name}.svg`;
+ }
+ }, [props.workspaceFile, props.workspace.descriptor, downloadRef, downloadAllRef, downloadPreviewRef]);
+
+ const shouldIncludeDownloadSvgDropdownItem = useMemo(() => {
+ return props.workspaceFile.extension.toLowerCase() !== "pmml";
+ }, [props.workspaceFile]);
+
+ const onDownload = useCallback(() => {
+ props.editor?.getStateControl().setSavedCommand();
+ alertsDispatch.closeAll();
+ props.workspaceFile.getFileContents().then((content) => {
+ if (downloadRef.current) {
+ const fileBlob = new Blob([content], { type: "text/plain" });
+ downloadRef.current.href = URL.createObjectURL(fileBlob);
+ downloadRef.current.click();
+ }
+ });
+ }, [props.editor, props.workspaceFile, alertsDispatch, downloadRef]);
+
+ const downloadSvg = useCallback(() => {
+ props.editor?.getPreview().then((previewSvg) => {
+ if (downloadPreviewRef.current && previewSvg) {
+ const fileBlob = new Blob([previewSvg], { type: "image/svg+xml" });
+ downloadPreviewRef.current.href = URL.createObjectURL(fileBlob);
+ downloadPreviewRef.current.click();
+ }
+ });
+ }, [downloadPreviewRef, props.editor]);
+
+ return (
+ <DropdownGroup key={"download-group"} label="Download">
+ <DropdownItem
+ onClick={onDownload}
+ key={"download-file-item"}
+ description={`${props.workspaceFile.name} will be downloaded`}
+ icon={<DownloadIcon />}
+ ouiaId="download-file-dropdown-button"
+ >
+ Current file
+ </DropdownItem>
+ {shouldIncludeDownloadSvgDropdownItem && (
+ <DropdownItem
+ key={`dropdown-download-svg`}
+ data-testid="dropdown-download-svg"
+ component="button"
+ onClick={downloadSvg}
+ description={`Image of ${props.workspaceFile.name} will be downloaded in SVG format`}
+ icon={<ImageIcon />}
+ >
+ {"Current file's SVG"}
+ </DropdownItem>
+ )}
+ <DropdownItem
+ onClick={downloadWorkspaceZip}
+ key={"download-zip-item"}
+ description={`A zip file including all files will be downloaded`}
+ icon={<FolderIcon />}
+ >
+ All files
+ </DropdownItem>
+ </DropdownGroup>
+ );
+};
diff --git a/packages/online-editor/src/editor/Toolbar/Share/EmbedDropdownGroup.tsx b/packages/online-editor/src/editor/Toolbar/Share/EmbedDropdownGroup.tsx
new file mode 100644
index 0000000000..7c6520d23e
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Share/EmbedDropdownGroup.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useMemo } from "react";
+import { DropdownGroup, DropdownItem } from "@patternfly/react-core/dist/js/components/Dropdown";
+import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import ColumnsIcon from "@patternfly/react-icons/dist/js/icons/columns-icon";
+import { useOnlineI18n } from "../../../i18n";
+import { useEditorToolbarDispatchContext } from "../EditorToolbarContextProvider";
+
+type Props = {
+ workspaceFile: WorkspaceFile;
+ workspace: ActiveWorkspace;
+};
+
+export const EmbedDropdownGroup = (props: Props) => {
+ const { i18n } = useOnlineI18n();
+ const { setEmbedModalOpen } = useEditorToolbarDispatchContext();
+
+ const shouldIncludeEmbedDropdownItem = useMemo(() => {
+ return props.workspaceFile.extension.toLowerCase() !== "pmml";
+ }, [props.workspaceFile]);
+
+ const openEmbedModal = useCallback(() => {
+ setEmbedModalOpen(true);
+ }, [setEmbedModalOpen]);
+
+ if (!shouldIncludeEmbedDropdownItem) {
+ return null;
+ }
+
+ return (
+ <>
+ <Divider key={"divider-other-group"} />
+ <DropdownGroup key={"other-group"} label="Other">
+ <DropdownItem
+ key={`dropdown-embed`}
+ data-testid="dropdown-embed"
+ component="button"
+ onClick={openEmbedModal}
+ icon={<ColumnsIcon />}
+ >
+ {i18n.editorToolbar.embed}...
+ </DropdownItem>
+ </DropdownGroup>
+ </>
+ );
+};
diff --git a/packages/online-editor/src/editor/EmbedModal.tsx b/packages/online-editor/src/editor/Toolbar/Share/EmbedModal.tsx
similarity index 98%
rename from packages/online-editor/src/editor/EmbedModal.tsx
rename to packages/online-editor/src/editor/Toolbar/Share/EmbedModal.tsx
index 569edcb5af..1573209906 100644
--- a/packages/online-editor/src/editor/EmbedModal.tsx
+++ b/packages/online-editor/src/editor/Toolbar/Share/EmbedModal.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Red Hat, Inc. and/or its affiliates.
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/M
import { Radio } from "@patternfly/react-core/dist/js/components/Radio";
import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
import { ClipboardCopy, ClipboardCopyVariant } from "@patternfly/react-core/dist/js/components/ClipboardCopy";
-import { useOnlineI18n } from "../i18n";
+import { useOnlineI18n } from "../../../i18n";
import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
import { GistOrigin, WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
diff --git a/packages/online-editor/src/editor/Toolbar/Share/GitDropdownGroup.tsx b/packages/online-editor/src/editor/Toolbar/Share/GitDropdownGroup.tsx
new file mode 100644
index 0000000000..771eba4d54
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Share/GitDropdownGroup.tsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import {
+ WorkspaceKind,
+ isGistLikeWorkspaceKind,
+} from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { useOnlineI18n } from "../../../i18n";
+import {
+ AuthProviderGroup,
+ GistEnabledAuthProviderType,
+ GitAuthProviderType,
+} from "../../../authProviders/AuthProvidersApi";
+import { SelectPosition } from "@patternfly/react-core/dist/js/components/Select";
+import { DropdownGroup, DropdownItem } from "@patternfly/react-core/dist/js/components/Dropdown";
+import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
+import { AuthSessionSelect } from "../../../authSessions/AuthSessionSelect";
+import { AccountsDispatchActionKind, useAccountsDispatch } from "../../../accounts/AccountsContext";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import BitbucketIcon from "@patternfly/react-icons/dist/js/icons/bitbucket-icon";
+import GithubIcon from "@patternfly/react-icons/dist/js/icons/github-icon";
+import GitIcon from "@patternfly/react-icons/dist/js/icons/git-icon";
+import { useAuthSession } from "../../../authSessions/AuthSessionsContext";
+import { useAuthProvider } from "../../../authProviders/AuthProvidersContext";
+import { useGitIntegration } from "../GitIntegration/GitIntegrationContextProvider";
+import { useEditorToolbarDispatchContext } from "../EditorToolbarContextProvider";
+
+type Props = {
+ workspace: ActiveWorkspace;
+};
+
+export const GitDropdownGroup = (props: Props) => {
+ const { i18n } = useOnlineI18n();
+ const { authSession } = useAuthSession(props.workspace.descriptor.gitAuthSessionId);
+ const authProvider = useAuthProvider(authSession);
+ const accountsDispatch = useAccountsDispatch();
+
+ const {
+ auth: { authSessionSelectFilter, changeGitAuthSessionId },
+ git: { canCreateGitRepository },
+ gistOrSnippet: { canCreateGistOrSnippet },
+ } = useGitIntegration();
+ const { setCreateGitRepositoryModalOpen, setCreateGistOrSnippetModalOpen, setShareDropdownOpen, setSmallKebabOpen } =
+ useEditorToolbarDispatchContext();
+
+ if (
+ props.workspace.descriptor.origin.kind !== WorkspaceKind.LOCAL &&
+ !isGistLikeWorkspaceKind(props.workspace.descriptor.origin.kind)
+ ) {
+ return null;
+ }
+
+ return (
+ <>
+ <DropdownGroup key={"git-group"} label={i18n.names.git}>
+ <Alert
+ isInline={true}
+ variant={"info"}
+ title={"Authentication source"}
+ actionLinks={
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspace.descriptor.name}'...`}
+ position={SelectPosition.right}
+ isPlain={false}
+ authSessionId={props.workspace.descriptor.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ changeGitAuthSessionId(newAuthSessionId, props.workspace.descriptor.gitAuthSessionId);
+ setTimeout(() => {
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ setShareDropdownOpen(true);
+ setSmallKebabOpen(true);
+ }, 0);
+ }}
+ filter={authSessionSelectFilter}
+ />
+ }
+ >
+ {`Manage authentication sources for '${props.workspace.descriptor.name}' to be able to create Repository, GitHub Gist or Bitbucket Snippet.`}
+ </Alert>
+ {canCreateGitRepository && (
+ <Tooltip
+ data-testid={"create-git-repository-tooltip"}
+ key={`dropdown-create-git-repository`}
+ content={<div>{"Click to create a repository."}</div>}
+ trigger={!canCreateGitRepository ? "mouseenter click" : ""}
+ position="left"
+ >
+ <DropdownItem
+ icon={switchExpression(authProvider?.type as GitAuthProviderType, {
+ bitbucket: <BitbucketIcon />,
+ github: <GithubIcon />,
+ default: <GitIcon />,
+ })}
+ data-testid={"create-git-repository-button"}
+ component="button"
+ onClick={() => setCreateGitRepositoryModalOpen(true)}
+ isDisabled={!canCreateGitRepository}
+ >
+ Create{" "}
+ {switchExpression(authProvider?.type as GitAuthProviderType, {
+ github: "GitHub repository",
+ bitbucket: "Bitbucket repository",
+ default: "Git repository",
+ })}
+ ...
+ </DropdownItem>
+ </Tooltip>
+ )}
+ {canCreateGistOrSnippet && (
+ <Tooltip
+ data-testid={"create-gist-or-snippet-tooltip"}
+ key={`dropdown-create-gist-or-snippet`}
+ content={
+ <div>
+ {switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
+ github: i18n.editorToolbar.cantCreateGistTooltip,
+ bitbucket: i18n.editorToolbar.cantCreateSnippetTooltip,
+ })}
+ </div>
+ }
+ trigger={!canCreateGistOrSnippet ? "mouseenter click" : ""}
+ position="left"
+ >
+ <DropdownItem
+ icon={switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
+ bitbucket: <BitbucketIcon />,
+ github: <GithubIcon />,
+ })}
+ data-testid={"create-gist-or-snippet-button"}
+ component="button"
+ onClick={() => setCreateGistOrSnippetModalOpen(true)}
+ isDisabled={!canCreateGistOrSnippet}
+ >
+ {switchExpression(authProvider?.type as GistEnabledAuthProviderType, {
+ github: i18n.editorToolbar.createGist,
+ bitbucket: i18n.editorToolbar.createSnippet,
+ })}
+ </DropdownItem>
+ </Tooltip>
+ )}
+ </DropdownGroup>
+ </>
+ );
+};
diff --git a/packages/online-editor/src/editor/Toolbar/SyncDropdownMenu.tsx b/packages/online-editor/src/editor/Toolbar/SyncDropdownMenu.tsx
new file mode 100644
index 0000000000..ca0452e58f
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/SyncDropdownMenu.tsx
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import {
+ WorkspaceKind,
+ WorkspaceKindGistLike,
+ isGistLikeWorkspaceKind,
+} from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import {
+ Dropdown,
+ DropdownGroup,
+ DropdownItem,
+ DropdownPosition,
+ DropdownToggle,
+} from "@patternfly/react-core/dist/js/components/Dropdown";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { switchExpression } from "../../switchExpression/switchExpression";
+import { useOnlineI18n } from "../../i18n";
+import BitbucketIcon from "@patternfly/react-icons/dist/js/icons/bitbucket-icon";
+import GithubIcon from "@patternfly/react-icons/dist/js/icons/github-icon";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
+import { AuthSessionSelect } from "../../authSessions/AuthSessionSelect";
+import { AuthProviderGroup, GitAuthProviderType } from "../../authProviders/AuthProvidersApi";
+import { AccountsDispatchActionKind, useAccountsDispatch } from "../../accounts/AccountsContext";
+import SyncAltIcon from "@patternfly/react-icons/dist/js/icons/sync-alt-icon";
+import { GIT_ORIGIN_REMOTE_NAME } from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
+import ArrowCircleUpIcon from "@patternfly/react-icons/dist/js/icons/arrow-circle-up-icon";
+import { useEditorToolbarContext, useEditorToolbarDispatchContext } from "./EditorToolbarContextProvider";
+import { useGitIntegration } from "./GitIntegration/GitIntegrationContextProvider";
+import { useAuthProvider } from "../../authProviders/AuthProvidersContext";
+import { useAuthSession } from "../../authSessions/AuthSessionsContext";
+
+type Props = {
+ workspace: ActiveWorkspace;
+};
+
+export function SyncDropdownMenu(props: Props) {
+ const { i18n } = useOnlineI18n();
+ const accountsDispatch = useAccountsDispatch();
+ const { authSession } = useAuthSession(props.workspace.descriptor.gitAuthSessionId);
+ const authProvider = useAuthProvider(authSession);
+ const { isSyncGistOrSnippetDropdownOpen, isSyncGitRepositoryDropdownOpen } = useEditorToolbarContext();
+
+ const { setSyncGistOrSnippetDropdownOpen, setSyncGitRepositoryDropdownOpen } = useEditorToolbarDispatchContext();
+
+ const {
+ auth: { changeGitAuthSessionId, authSessionSelectFilter },
+ git: { canPushToGitRepository, pushToGitRepository, pullFromGitRepository },
+ gistOrSnippet: { canUpdateGistOrSnippet, updateGistOrSnippet, canForkGitHubGist, forkGitHubGist },
+ } = useGitIntegration();
+
+ return (
+ <>
+ {isGistLikeWorkspaceKind(props.workspace.descriptor.origin.kind) && (
+ <Dropdown
+ onSelect={() => setSyncGistOrSnippetDropdownOpen(false)}
+ isOpen={isSyncGistOrSnippetDropdownOpen}
+ position={DropdownPosition.right}
+ toggle={
+ <DropdownToggle
+ id={"sync-dropdown"}
+ data-testid={"sync-dropdown"}
+ onToggle={(isOpen) => setSyncGistOrSnippetDropdownOpen(isOpen)}
+ >
+ Sync
+ </DropdownToggle>
+ }
+ dropdownItems={[
+ <DropdownGroup key={"sync-gist-or-snippet-dropdown-group"}>
+ <Tooltip
+ data-testid={"gist-or-snippet-it-tooltip"}
+ content={
+ <div>
+ {switchExpression(props.workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
+ GITHUB_GIST: i18n.editorToolbar.cantUpdateGistTooltip,
+ BITBUCKET_SNIPPET: i18n.editorToolbar.cantUpdateSnippetTooltip,
+ })}
+ </div>
+ }
+ trigger={!canUpdateGistOrSnippet ? "mouseenter click" : ""}
+ position="left"
+ >
+ <>
+ <DropdownItem
+ style={{ minWidth: "300px" }}
+ icon={switchExpression(props.workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
+ BITBUCKET_SNIPPET: <BitbucketIcon />,
+ GITHUB_GIST: <GithubIcon />,
+ })}
+ onClick={updateGistOrSnippet}
+ isDisabled={!canUpdateGistOrSnippet}
+ >
+ Update{" "}
+ {switchExpression(props.workspace.descriptor.origin.kind as WorkspaceKindGistLike, {
+ BITBUCKET_SNIPPET: "Bitbucket Snippet",
+ GITHUB_GIST: "GitHub Gist",
+ })}
+ </DropdownItem>
+ {canForkGitHubGist && (
+ <>
+ <Divider />
+ <li role="menuitem">
+ <Alert
+ isInline={true}
+ variant={"default"}
+ title={<span style={{ whiteSpace: "nowrap" }}>{"Can't update Gists you don't own"}</span>}
+ >
+ <br />
+ {`You can create a fork of '${props.workspace.descriptor.name}' to save your updates.`}
+ <br />
+ <br />
+ <Button
+ onClick={forkGitHubGist}
+ variant={ButtonVariant.link}
+ isSmall={true}
+ style={{ paddingLeft: 0 }}
+ >
+ {`Fork Gist`}
+ </Button>
+ <br />
+ <br />
+ {`Or you can change the authentication source for '${props.workspace.descriptor.name}' to be able to Update Gist.`}
+ <br />
+ <br />
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspace.descriptor.name}'...`}
+ isPlain={false}
+ authSessionId={props.workspace.descriptor.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ changeGitAuthSessionId(newAuthSessionId, props.workspace.descriptor.gitAuthSessionId);
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ setTimeout(() => {
+ setSyncGistOrSnippetDropdownOpen(true);
+ }, 0);
+ }}
+ filter={authSessionSelectFilter}
+ />
+ </Alert>
+ </li>
+ </>
+ )}
+ {!canPushToGitRepository && (
+ <>
+ <Divider />
+ <Alert
+ isInline={true}
+ variant={"default"}
+ title={`Can't Update ${switchExpression(authProvider?.type as GitAuthProviderType, {
+ github: "GitHub repository",
+ bitbucket: "Bitbucket repository",
+ default: "Git repository",
+ })} without selecting a matching authentication source`}
+ actionLinks={
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspace.descriptor.name}'...`}
+ isPlain={false}
+ authSessionId={props.workspace.descriptor.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ changeGitAuthSessionId(newAuthSessionId, props.workspace.descriptor.gitAuthSessionId);
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ setTimeout(() => {
+ setSyncGistOrSnippetDropdownOpen(true);
+ }, 0);
+ }}
+ filter={authSessionSelectFilter}
+ />
+ }
+ >
+ {`Select an authentication source for '${
+ props.workspace.descriptor.name
+ }' to be able to Update ${switchExpression(
+ props.workspace.descriptor.origin.kind as WorkspaceKindGistLike,
+ {
+ GITHUB_GIST: "Github Gist",
+ BITBUCKET_SNIPPET: "Bitbucket Snippet",
+ }
+ )}.`}
+ </Alert>
+ </>
+ )}
+ </>
+ </Tooltip>
+ </DropdownGroup>,
+ ]}
+ />
+ )}
+ {props.workspace.descriptor.origin.kind === WorkspaceKind.GIT && (
+ <Dropdown
+ onSelect={() => setSyncGitRepositoryDropdownOpen(false)}
+ isOpen={isSyncGitRepositoryDropdownOpen}
+ position={DropdownPosition.right}
+ toggle={
+ <DropdownToggle
+ id={"sync-dropdown"}
+ data-testid={"sync-dropdown"}
+ onToggle={(isOpen) => setSyncGitRepositoryDropdownOpen(isOpen)}
+ >
+ Sync
+ </DropdownToggle>
+ }
+ dropdownItems={[
+ <DropdownGroup key={"sync-git-dropdown-group"}>
+ <DropdownItem
+ icon={<SyncAltIcon />}
+ onClick={() => pullFromGitRepository({ showAlerts: true })}
+ description={`Get new changes made upstream at '${GIT_ORIGIN_REMOTE_NAME}/${props.workspace.descriptor.origin.branch}'.`}
+ >
+ Pull
+ </DropdownItem>
+ <Tooltip
+ data-testid={"git-it-tooltip"}
+ content={<div>{`You need to select an authentication source to Push to this repository.`}</div>}
+ trigger={!canPushToGitRepository ? "mouseenter click" : ""}
+ position="left"
+ >
+ <>
+ <DropdownItem
+ icon={<ArrowCircleUpIcon />}
+ onClick={pushToGitRepository}
+ isDisabled={!canPushToGitRepository}
+ description={`Send your changes upstream to '${GIT_ORIGIN_REMOTE_NAME}/${props.workspace.descriptor.origin.branch}'.`}
+ >
+ Push
+ </DropdownItem>
+ {!canPushToGitRepository && (
+ <>
+ <Alert
+ isInline={true}
+ variant={"default"}
+ title={"Can't Push without selecting an authentication source"}
+ actionLinks={
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspace.descriptor.name}'...`}
+ isPlain={false}
+ authSessionId={props.workspace.descriptor.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ changeGitAuthSessionId(newAuthSessionId, props.workspace.descriptor.gitAuthSessionId);
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ setTimeout(() => {
+ setSyncGitRepositoryDropdownOpen(true);
+ });
+ }}
+ filter={authSessionSelectFilter}
+ />
+ }
+ >
+ {`Select an authentication source for '${props.workspace.descriptor.name}' to be able to Push.`}
+ </Alert>
+ </>
+ )}
+ </>
+ </Tooltip>
+ </DropdownGroup>,
+ ]}
+ />
+ )}
+ </>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/VsCodeDropdownMenu.tsx b/packages/online-editor/src/editor/Toolbar/VsCodeDropdownMenu.tsx
new file mode 100644
index 0000000000..4566beba98
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/VsCodeDropdownMenu.tsx
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState } from "react";
+import {
+ Dropdown,
+ DropdownGroup,
+ DropdownItem,
+ DropdownToggle,
+} from "@patternfly/react-core/dist/js/components/Dropdown";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
+import CaretDownIcon from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
+import { Alert } from "@patternfly/react-core/dist/js/components/Alert";
+import { PushToGitAlertActionLinks } from "./GitIntegration/PushToGitAlertActionLinks";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import ExternalLinkAltIcon from "@patternfly/react-icons/dist/js/icons/external-link-alt-icon";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { useNavigationStatus, useRoutes } from "../../navigation/Hooks";
+import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { UrlType } from "../../importFromUrl/ImportableUrlHooks";
+import { GIT_ORIGIN_REMOTE_NAME } from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
+import { useGitIntegration } from "./GitIntegration/GitIntegrationContextProvider";
+
+export function VsCodeDropdownMenu(props: { workspace: ActiveWorkspace }) {
+ const routes = useRoutes();
+ const navigationStatus = useNavigationStatus();
+ const [isVsCodeDropdownOpen, setVsCodeDropdownOpen] = useState(false);
+
+ const {
+ workspaceImportableUrl,
+ auth: { changeGitAuthSessionId, authSessionSelectFilter },
+ git: { canPushToGitRepository, pushToGitRepository },
+ } = useGitIntegration();
+
+ if (
+ props.workspace.descriptor.origin.kind !== WorkspaceKind.GIT ||
+ workspaceImportableUrl.type !== UrlType.GITHUB_DOT_COM
+ ) {
+ return null;
+ }
+
+ return (
+ <FlexItem
+ style={{
+ minWidth: "137px",
+ }}
+ >
+ <Dropdown
+ className={"kie-tools--masthead-hoverable"}
+ isPlain={true}
+ onSelect={() => setVsCodeDropdownOpen(false)}
+ isOpen={isVsCodeDropdownOpen}
+ position={"right"}
+ toggle={
+ <DropdownToggle toggleIndicator={null} onToggle={setVsCodeDropdownOpen}>
+ <Flex flexWrap={{ default: "nowrap" }}>
+ <FlexItem
+ style={{
+ minWidth: 0 /* This is to make the flex parent not overflow horizontally */,
+ }}
+ >
+ <Tooltip distance={5} position={"top-start"} content={props.workspace.descriptor.name}>
+ <TextContent>
+ <Text
+ component={TextVariants.small}
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <img
+ style={{
+ minWidth: "14px",
+ maxWidth: "14px",
+ marginTop: "-2px",
+ verticalAlign: "middle",
+ }}
+ alt="vscode-logo-blue"
+ src={routes.static.images.vscodeLogoBlue.path({})}
+ />
+
+ {`Open "${props.workspace.descriptor.name}"`}
+ </Text>
+ </TextContent>
+ </Tooltip>
+ </FlexItem>
+ <FlexItem>
+ <CaretDownIcon />
+ </FlexItem>
+ </Flex>
+ </DropdownToggle>
+ }
+ dropdownItems={[
+ <DropdownGroup key={"open-in-vscode"}>
+ {navigationStatus.shouldBlockNavigationTo({ pathname: "__external" }) && (
+ <>
+ <Alert
+ isInline={true}
+ variant={"warning"}
+ title={"You have new changes to Push"}
+ actionLinks={
+ <PushToGitAlertActionLinks
+ changeGitAuthSessionId={changeGitAuthSessionId}
+ workspaceDescriptor={props.workspace.descriptor}
+ canPush={canPushToGitRepository}
+ authSessionSelectFilter={authSessionSelectFilter}
+ remoteRef={`${GIT_ORIGIN_REMOTE_NAME}/${props.workspace.descriptor.origin.branch}`}
+ onPush={pushToGitRepository}
+ />
+ }
+ >
+ {`Opening '${props.workspace.descriptor.name}' on vscode.dev won't show your latest changes.`}
+ </Alert>
+ <Divider />
+ </>
+ )}
+ <DropdownItem
+ style={{ minWidth: "400px" }}
+ href={`https://vscode.dev/github${
+ new URL(props.workspace.descriptor.origin.url).pathname.endsWith(".git")
+ ? new URL(props.workspace.descriptor.origin.url).pathname.replace(".git", "")
+ : new URL(props.workspace.descriptor.origin.url).pathname
+ }/tree/${props.workspace.descriptor.origin.branch}`}
+ target={"_blank"}
+ icon={<ExternalLinkAltIcon />}
+ description={`The '${props.workspace.descriptor.origin.branch}' branch will be opened.`}
+ >
+ vscode.dev
+ </DropdownItem>
+ <Divider />
+ <DropdownItem
+ href={`vscode://vscode.git/clone?url=${props.workspace.descriptor.origin.url.toString()}`}
+ target={"_blank"}
+ icon={<ExternalLinkAltIcon />}
+ description={"The default branch will be opened."}
+ >
+ VS Code Desktop
+ </DropdownItem>
+ </DropdownGroup>,
+ ]}
+ />
+ </FlexItem>
+ );
+}
diff --git a/packages/online-editor/src/editor/Toolbar/Workspace/Hooks.tsx b/packages/online-editor/src/editor/Toolbar/Workspace/Hooks.tsx
new file mode 100644
index 0000000000..581774a8c4
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Workspace/Hooks.tsx
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useEffect } from "react";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { useNavigationBlockersBypass, useNavigationStatus, useNavigationStatusToggle } from "../../../navigation/Hooks";
+import { useGlobalAlert } from "../../../alerts";
+import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/js/components/Alert";
+import {
+ WorkspaceKind,
+ WorkspaceKindGitBased,
+} from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
+import { useOnlineI18n } from "../../../i18n";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { PushToGitAlertActionLinks } from "../GitIntegration/PushToGitAlertActionLinks";
+import { useGitIntegration } from "../GitIntegration/GitIntegrationContextProvider";
+import { switchExpression } from "../../../switchExpression/switchExpression";
+import { GIT_ORIGIN_REMOTE_NAME } from "@kie-tools-core/workspaces-git-fs/dist/constants/GitConstants";
+import { useHistory } from "react-router";
+import { useEditorToolbarContext } from "../EditorToolbarContextProvider";
+
+export function useWorkspaceNavigationBlocker(workspace: ActiveWorkspace) {
+ const { i18n } = useOnlineI18n();
+ const history = useHistory();
+
+ const {
+ auth: { changeGitAuthSessionId, authSessionSelectFilter },
+ git: { canPushToGitRepository, pushToGitRepository },
+ gistOrSnippet: { canUpdateGistOrSnippet, updateGistOrSnippet },
+ } = useGitIntegration();
+
+ const { downloadWorkspaceZip } = useEditorToolbarContext();
+
+ const navigationStatus = useNavigationStatus();
+ const navigationStatusToggle = useNavigationStatusToggle();
+ const navigationBlockersBypass = useNavigationBlockersBypass();
+
+ const confirmNavigationAlert = useGlobalAlert<{
+ lastBlockedLocation: Location;
+ }>(
+ useCallback(
+ (_, { lastBlockedLocation }) => (
+ <Alert
+ data-testid="unsaved-alert"
+ variant="warning"
+ title={
+ workspace.descriptor.origin.kind === WorkspaceKind.LOCAL
+ ? i18n.editorPage.alerts.unsaved.titleLocal
+ : i18n.editorPage.alerts.unsaved.titleGit
+ }
+ actionClose={
+ <AlertActionCloseButton data-testid="unsaved-alert-close-button" onClose={navigationStatusToggle.unblock} />
+ }
+ actionLinks={
+ <>
+ <Divider inset={{ default: "insetMd" }} />
+ <br />
+ {(workspace.descriptor.origin.kind === WorkspaceKind.LOCAL && (
+ <AlertActionLink
+ data-testid="unsaved-alert-save-button"
+ onClick={() => {
+ navigationStatusToggle.unblock();
+ return downloadWorkspaceZip();
+ }}
+ style={{ fontWeight: "bold" }}
+ >
+ {`${i18n.terms.download} '${workspace.descriptor.name}'`}
+ </AlertActionLink>
+ )) || (
+ <PushToGitAlertActionLinks
+ changeGitAuthSessionId={changeGitAuthSessionId}
+ workspaceDescriptor={workspace.descriptor}
+ canPush={switchExpression(workspace.descriptor.origin.kind as WorkspaceKindGitBased, {
+ BITBUCKET_SNIPPET: canUpdateGistOrSnippet,
+ GITHUB_GIST: canUpdateGistOrSnippet,
+ GIT: canPushToGitRepository,
+ })}
+ remoteRef={`${GIT_ORIGIN_REMOTE_NAME}/${workspace.descriptor.origin.branch}`}
+ authSessionSelectFilter={authSessionSelectFilter}
+ onPush={() => {
+ navigationStatusToggle.unblock();
+ switchExpression(workspace.descriptor.origin.kind as WorkspaceKindGitBased, {
+ BITBUCKET_SNIPPET: updateGistOrSnippet,
+ GITHUB_GIST: updateGistOrSnippet,
+ GIT: pushToGitRepository,
+ })();
+ }}
+ />
+ )}
+ <br />
+ <br />
+ <AlertActionLink
+ data-testid="unsaved-alert-close-without-save-button"
+ onClick={() =>
+ navigationBlockersBypass.execute(() => {
+ history.push(lastBlockedLocation);
+ })
+ }
+ >
+ {i18n.editorPage.alerts.unsaved.proceedAnyway}
+ </AlertActionLink>
+ <br />
+ <br />
+ </>
+ }
+ >
+ <br />
+ <p>{i18n.editorPage.alerts.unsaved.message}</p>
+ </Alert>
+ ),
+ [
+ workspace.descriptor,
+ i18n.editorPage.alerts.unsaved.titleLocal,
+ i18n.editorPage.alerts.unsaved.titleGit,
+ i18n.editorPage.alerts.unsaved.proceedAnyway,
+ i18n.editorPage.alerts.unsaved.message,
+ i18n.terms.download,
+ navigationStatusToggle,
+ changeGitAuthSessionId,
+ canUpdateGistOrSnippet,
+ canPushToGitRepository,
+ authSessionSelectFilter,
+ downloadWorkspaceZip,
+ updateGistOrSnippet,
+ pushToGitRepository,
+ navigationBlockersBypass,
+ history,
+ ]
+ )
+ );
+
+ useEffect(() => {
+ if (navigationStatus.lastBlockedLocation) {
+ confirmNavigationAlert.show({ lastBlockedLocation: navigationStatus.lastBlockedLocation });
+ } else {
+ confirmNavigationAlert.close();
+ }
+ }, [confirmNavigationAlert, navigationStatus]);
+
+ return;
+}
diff --git a/packages/online-editor/src/editor/Toolbar/Workspace/WorkspaceToolbar.tsx b/packages/online-editor/src/editor/Toolbar/Workspace/WorkspaceToolbar.tsx
new file mode 100644
index 0000000000..01dec5770b
--- /dev/null
+++ b/packages/online-editor/src/editor/Toolbar/Workspace/WorkspaceToolbar.tsx
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useEffect, useRef } from "react";
+import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
+import { useHistory } from "react-router";
+import { useRoutes } from "../../../navigation/Hooks";
+import AngleLeftIcon from "@patternfly/react-icons/dist/js/icons/angle-left-icon";
+import { AuthSessionSelect } from "../../../authSessions/AuthSessionSelect";
+import { AuthProviderGroup } from "../../../authProviders/AuthProvidersApi";
+import { useGitIntegration } from "../GitIntegration/GitIntegrationContextProvider";
+import { AccountsDispatchActionKind, useAccountsDispatch } from "../../../accounts/AccountsContext";
+import { WorkspaceLabel } from "../../../workspace/components/WorkspaceLabel";
+import FolderIcon from "@patternfly/react-icons/dist/js/icons/folder-icon";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
+import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
+import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { WorkspaceStatusIndicator } from "../../../workspace/components/WorkspaceStatusIndicator";
+import { AcceleratorIndicator } from "../Accelerators/AcceleratorIndicator";
+import { PromiseState } from "@kie-tools-core/react-hooks/dist/PromiseState";
+import { WorkspaceGitStatusType } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks";
+
+type Props = {
+ workspace: ActiveWorkspace;
+ currentWorkspaceFile: WorkspaceFile;
+ workspaceGitStatusPromise: PromiseState<WorkspaceGitStatusType>;
+ onDeletedWorkspaceFile: () => void;
+};
+
+export function WorkspaceToolbar(props: Props) {
+ const history = useHistory();
+ const routes = useRoutes();
+ const workspaces = useWorkspaces();
+
+ const {
+ auth: { changeGitAuthSessionId, authSessionSelectFilter },
+ } = useGitIntegration();
+
+ const accountsDispatch = useAccountsDispatch();
+
+ const workspaceNameRef = useRef<HTMLInputElement>(null);
+
+ const resetWorkspaceName = useCallback(() => {
+ if (workspaceNameRef.current) {
+ workspaceNameRef.current.value = props.workspace.descriptor.name;
+ }
+ }, [props.workspace]);
+
+ useEffect(resetWorkspaceName, [resetWorkspaceName]);
+
+ const onRenameWorkspace = useCallback(
+ async (newName: string | undefined) => {
+ if (!newName) {
+ resetWorkspaceName();
+ return;
+ }
+
+ if (newName === props.workspace.descriptor.name) {
+ return;
+ }
+
+ await workspaces.renameWorkspace({
+ workspaceId: props.workspace.descriptor.workspaceId,
+ newName: newName.trim(),
+ });
+ },
+ [props.workspace.descriptor, workspaces, resetWorkspaceName]
+ );
+
+ const onWorkspaceNameKeyDown = useCallback(
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ if (e.keyCode === 13 /* Enter */) {
+ e.currentTarget.blur();
+ } else if (e.keyCode === 27 /* ESC */) {
+ resetWorkspaceName();
+ e.currentTarget.blur();
+ }
+ },
+ [resetWorkspaceName]
+ );
+
+ return (
+ <Flex
+ justifyContent={{ default: "justifyContentFlexStart" }}
+ flexWrap={{ default: "nowrap" }}
+ spaceItems={{ default: "spaceItemsSm" }}
+ alignItems={{ default: "alignItemsCenter" }}
+ >
+ <FlexItem>
+ <Button
+ className={"kie-tools--masthead-hoverable"}
+ variant={ButtonVariant.plain}
+ onClick={() => history.push({ pathname: routes.home.path({}) })}
+ >
+ <AngleLeftIcon />
+ </Button>
+ </FlexItem>
+ <FlexItem>
+ <AuthSessionSelect
+ title={`Select Git authentication for '${props.workspace.descriptor.name}'...`}
+ isPlain={true}
+ authSessionId={props.workspace.descriptor.gitAuthSessionId}
+ showOnlyThisAuthProviderGroupWhenConnectingToNewAccount={AuthProviderGroup.GIT}
+ setAuthSessionId={(newAuthSessionId) => {
+ changeGitAuthSessionId(newAuthSessionId, props.workspace.descriptor.gitAuthSessionId);
+ accountsDispatch({ kind: AccountsDispatchActionKind.CLOSE });
+ }}
+ filter={authSessionSelectFilter}
+ />
+ </FlexItem>
+ <FlexItem>
+ <WorkspaceLabel descriptor={props.workspace.descriptor} />
+ </FlexItem>
+ <FlexItem>
+ <AcceleratorIndicator workspaceId={props.workspace.descriptor.workspaceId} />
+ </FlexItem>
+ <FlexItem
+ style={{ minWidth: 0, padding: "0 8px 0 8px", flexShrink: 0 }}
+ className={"kie-tools--masthead-hoverable"}
+ >
+ <FolderIcon style={{ marginRight: "8px", verticalAlign: "middle" }} />
+ <div
+ data-testid={"toolbar-title-workspace"}
+ className={"kogito--editor__toolbar-name-container"}
+ style={{ display: "inline-block", verticalAlign: "middle" }}
+ >
+ <Title
+ aria-label={"EmbeddedEditorFile name"}
+ headingLevel={"h3"}
+ size={"md"}
+ style={{
+ fontStyle: "italic",
+ }}
+ >
+ {props.workspace.descriptor.name}
+ </Title>
+ <TextInput
+ ref={workspaceNameRef}
+ type={"text"}
+ aria-label={"Edit workspace name"}
+ onKeyDown={onWorkspaceNameKeyDown}
+ className={"kogito--editor__toolbar-subtitle"}
+ onBlur={(e) => onRenameWorkspace(e.target.value)}
+ style={{ fontStyle: "italic", top: "4px", height: "calc(100% - 8px)" }}
+ />
+ </div>
+ </FlexItem>
+ <FlexItem>
+ <WorkspaceStatusIndicator
+ gitStatusProps={{
+ workspaceDescriptor: props.workspace.descriptor,
+ workspaceGitStatusPromise: props.workspaceGitStatusPromise,
+ }}
+ currentWorkspaceFile={props.currentWorkspaceFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ workspaceFiles={props.workspace.files}
+ />
+ </FlexItem>
+ </Flex>
+ );
+}
diff --git a/packages/online-editor/src/env/EnvJson.ts b/packages/online-editor/src/env/EnvJson.ts
index 2b92854ec7..4df37326a9 100644
--- a/packages/online-editor/src/env/EnvJson.ts
+++ b/packages/online-editor/src/env/EnvJson.ts
@@ -15,6 +15,7 @@
*/
import { AuthProvider } from "../authProviders/AuthProvidersApi";
+import { AcceleratorConfig } from "../accelerators/AcceleratorsApi";
export interface EnvJson {
KIE_SANDBOX_EXTENDED_SERVICES_URL: string;
@@ -22,4 +23,5 @@ export interface EnvJson {
KIE_SANDBOX_REQUIRE_CUSTOM_COMMIT_MESSAGE: boolean;
KIE_SANDBOX_CUSTOM_COMMIT_MESSAGE_VALIDATION_SERVICE_URL: string;
KIE_SANDBOX_AUTH_PROVIDERS: AuthProvider[];
+ KIE_SANDBOX_ACCELERATORS: AcceleratorConfig[];
}
diff --git a/packages/online-editor/src/envelopeLocator/EditorEnvelopeLocatorFactory.ts b/packages/online-editor/src/envelopeLocator/EditorEnvelopeLocatorFactory.ts
index 91c8332fca..d278130ad8 100644
--- a/packages/online-editor/src/envelopeLocator/EditorEnvelopeLocatorFactory.ts
+++ b/packages/online-editor/src/envelopeLocator/EditorEnvelopeLocatorFactory.ts
@@ -20,24 +20,82 @@ import {
EnvelopeMapping,
} from "@kie-tools-core/editor/dist/api/EditorEnvelopeLocator";
+const REGEX = {
+ supported: /(\.bpmn|\.bpmn2|\.dmn|\.pmml|\.scesim)$/i,
+ dmn: /^.*\.dmn$/i,
+ bpmn: /^.*\.(bpmn|bpmn2)$/i,
+ scesim: /^.*\.scesim$/i,
+ pmml: /^.*\.pmml$/i,
+};
+
+export const GLOB_PATTERN = {
+ all: "**/*",
+ dmn: "**/*.dmn",
+ bpmn: "**/*.bpmn?(2)",
+ scesim: "**/*.scesim",
+ pmml: "**/*.pmml",
+};
+
+export enum FileTypes {
+ DMN = "dmn",
+ BPMN = "bpmn",
+ BPMN2 = "bpmn2",
+ SCESIM = "scesim",
+ PMML = "pmml",
+}
+
+export const supportedFileExtensionArray = [
+ FileTypes.DMN,
+ FileTypes.BPMN,
+ FileTypes.BPMN2,
+ FileTypes.SCESIM,
+ FileTypes.PMML,
+];
+
+export type SupportedFileExtensions = typeof supportedFileExtensionArray[number];
+
+export function isDecision(path: string): boolean {
+ return REGEX.dmn.test(path);
+}
+
+export function isWorkflow(path: string): boolean {
+ return REGEX.bpmn.test(path);
+}
+
+export function isTestScenario(path: string): boolean {
+ return REGEX.scesim.test(path);
+}
+
+export function isScorecard(path: string): boolean {
+ return REGEX.pmml.test(path);
+}
+
+export function isModel(path: string): boolean {
+ return isDecision(path) || isWorkflow(path) || isScorecard(path);
+}
+
+export function isEditable(path: string): boolean {
+ return isModel(path) || isTestScenario(path);
+}
+
export class EditorEnvelopeLocatorFactory {
public create(args: { targetOrigin: string }) {
return new EditorEnvelopeLocator(args.targetOrigin, [
new EnvelopeMapping({
- type: "bpmn",
- filePathGlob: "**/*.bpmn?(2)",
+ type: FileTypes.BPMN,
+ filePathGlob: GLOB_PATTERN.bpmn,
resourcesPathPrefix: "gwt-editors/bpmn",
envelopeContent: { type: EnvelopeContentType.PATH, path: "bpmn-envelope.html" },
}),
new EnvelopeMapping({
- type: "dmn",
- filePathGlob: "**/*.dmn",
+ type: FileTypes.DMN,
+ filePathGlob: GLOB_PATTERN.dmn,
resourcesPathPrefix: "gwt-editors/dmn",
envelopeContent: { type: EnvelopeContentType.PATH, path: "dmn-envelope.html" },
}),
new EnvelopeMapping({
- type: "pmml",
- filePathGlob: "**/*.pmml",
+ type: FileTypes.PMML,
+ filePathGlob: GLOB_PATTERN.pmml,
resourcesPathPrefix: "",
envelopeContent: { type: EnvelopeContentType.PATH, path: "pmml-envelope.html" },
}),
diff --git a/packages/online-editor/src/filesList/FileDataList.tsx b/packages/online-editor/src/filesList/FileDataList.tsx
index e1b4152f94..e5fd724a91 100644
--- a/packages/online-editor/src/filesList/FileDataList.tsx
+++ b/packages/online-editor/src/filesList/FileDataList.tsx
@@ -32,6 +32,10 @@ import { useRoutes } from "../navigation/Hooks";
import { TaskIcon } from "@patternfly/react-icons/dist/js/icons/task-icon";
import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
import { WorkspaceDescriptorDates } from "../workspace/components/WorkspaceDescriptorDates";
+import { EyeIcon } from "@patternfly/react-icons/dist/js/icons/eye-icon";
+import { GitStatusIndicator } from "../workspace/components/WorkspaceStatusIndicator";
+import { GitStatusIndicatorActions, GitStatusProps } from "../workspace/components/GitStatusIndicatorActions";
+import { switchExpression } from "../switchExpression/switchExpression";
const FILE_DATA_LIST_HEIGHTS = {
atRoot: 55 + 24,
@@ -42,31 +46,73 @@ export function getFileDataListHeight(file: WorkspaceFile) {
return file.relativePath.indexOf("/") >= 0 ? FILE_DATA_LIST_HEIGHTS.atSubDir : FILE_DATA_LIST_HEIGHTS.atRoot;
}
-export function FileListItem(props: { file: WorkspaceFile; isEditable: boolean }) {
+export enum FileListItemDisplayMode {
+ enabled = "enabled",
+ readonly = "readonly",
+ deleted = "deleted",
+}
+
+export function FileListItem(props: {
+ file: WorkspaceFile;
+ displayMode: FileListItemDisplayMode;
+ isCurrentWorkspaceFile?: boolean;
+ onDeletedWorkspaceFile?: () => void;
+ gitStatusProps?: GitStatusProps;
+}) {
+ const [keepGitStatusActionsDisplayed, setKeepGitStatusActionsDisplayed] = React.useState(false);
const fileDirPath = props.file.relativeDirPath.split("/").join(" > ");
- const fileName = props.isEditable ? props.file.nameWithoutExtension : props.file.name;
+ const fileName = [FileListItemDisplayMode.enabled, FileListItemDisplayMode.deleted].includes(props.displayMode)
+ ? props.file.nameWithoutExtension
+ : props.file.name;
return (
<>
- <Flex flexWrap={{ default: "nowrap" }}>
+ <Flex
+ flexWrap={{ default: "nowrap" }}
+ onClick={(e) => {
+ if (props.displayMode !== FileListItemDisplayMode.enabled) {
+ // this prevents the FileSwitcher from closing on click.
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }}
+ >
+ {props.isCurrentWorkspaceFile && (
+ <FlexItem align={{ default: "alignLeft" }}>
+ <EyeIcon title="Currently selected file." />
+ </FlexItem>
+ )}
<FlexItem style={{ minWidth: 0 /* This is to make the flex parent not overflow horizontally */ }}>
- <Tooltip distance={5} position={"top-start"} content={fileName}>
+ <Tooltip
+ distance={5}
+ position={"top-start"}
+ content={fileName + (props.displayMode === FileListItemDisplayMode.deleted ? " (Deleted)" : "")}
+ >
<TextContent>
- <Text
- component={TextVariants.p}
- style={{
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- }}
- >
- {fileName}
- </Text>
+ <Text component={TextVariants.p}>{fileName}</Text>
</TextContent>
</Tooltip>
</FlexItem>
<FlexItem>
<FileLabel extension={props.file.extension} />
</FlexItem>
+ {props.gitStatusProps && (
+ <FlexItem align={{ default: "alignRight" }}>
+ <GitStatusIndicator
+ gitStatusProps={props.gitStatusProps}
+ workspaceFile={props.file}
+ isHoverable={!keepGitStatusActionsDisplayed}
+ >
+ <GitStatusIndicatorActions
+ gitStatusProps={props.gitStatusProps}
+ workspaceFile={props.file}
+ currentWorkspaceFile={props.isCurrentWorkspaceFile ? props.file : undefined}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ isOpen={keepGitStatusActionsDisplayed}
+ setOpen={setKeepGitStatusActionsDisplayed}
+ />
+ </GitStatusIndicator>
+ </FlexItem>
+ )}
</Flex>
<TextContent>
<Text
@@ -86,13 +132,25 @@ export function FileListItem(props: { file: WorkspaceFile; isEditable: boolean }
);
}
-export function FileDataListItem(props: { file: WorkspaceFile; isEditable: boolean }) {
+export function FileDataListItem(props: {
+ file: WorkspaceFile;
+ displayMode: FileListItemDisplayMode;
+ isCurrentWorkspaceFile?: boolean;
+ onDeletedWorkspaceFile?: () => void;
+ gitStatusProps?: GitStatusProps;
+}) {
return (
- <DataListItemRow>
+ <DataListItemRow disabled={props.displayMode !== FileListItemDisplayMode.enabled}>
<DataListItemCells
dataListCells={[
- <DataListCell key="link" isFilled={false}>
- <FileListItem file={props.file} isEditable={props.isEditable} />
+ <DataListCell key="link" isFilled={true} autoFocus={props.isCurrentWorkspaceFile}>
+ <FileListItem
+ gitStatusProps={props.gitStatusProps}
+ file={props.file}
+ displayMode={props.displayMode}
+ isCurrentWorkspaceFile={props.isCurrentWorkspaceFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ />
</DataListCell>,
]}
/>
@@ -100,16 +158,37 @@ export function FileDataListItem(props: { file: WorkspaceFile; isEditable: boole
);
}
-export function FileDataList(props: { file: WorkspaceFile; isEditable: boolean; style?: React.CSSProperties }) {
+export function FileDataList(props: {
+ file: WorkspaceFile;
+ displayMode: FileListItemDisplayMode;
+ style?: React.CSSProperties;
+ isCurrentWorkspaceFile?: boolean;
+ onDeletedWorkspaceFile?: () => void;
+ gitStatusProps?: GitStatusProps;
+}) {
+ const fileDataListItem = (
+ <FileDataListItem
+ gitStatusProps={props.gitStatusProps}
+ key={props.file.relativePath}
+ file={props.file}
+ displayMode={props.displayMode}
+ isCurrentWorkspaceFile={props.isCurrentWorkspaceFile}
+ onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
+ />
+ );
return (
- <DataList aria-label="file-data-list" style={props.style}>
+ <DataList
+ aria-label="file-data-list"
+ style={props.style}
+ className={switchExpression(props.displayMode, {
+ enabled: "kie-tools--file-list-item-enabled",
+ deleted: "kie-tools--file-list-item-deleted",
+ readonly: "kie-tools--file-list-item-readonly",
+ })}
+ >
<DataListItem style={{ border: 0 }}>
- {(!props.isEditable && (
- <FileDataListItem key={props.file.relativePath} file={props.file} isEditable={props.isEditable} />
- )) || (
- <FileLink file={props.file}>
- <FileDataListItem file={props.file} isEditable={props.isEditable} />
- </FileLink>
+ {(props.displayMode !== FileListItemDisplayMode.enabled && fileDataListItem) || (
+ <FileLink file={props.file}>{fileDataListItem}</FileLink>
)}
</DataListItem>
</DataList>
@@ -133,9 +212,13 @@ export function FileLink(props: React.PropsWithChildren<{ file: WorkspaceFile; s
);
}
-export function SingleFileWorkspaceDataList(props: { workspaceDescriptor: WorkspaceDescriptor; file: WorkspaceFile }) {
+export function SingleFileWorkspaceDataList(props: {
+ workspaceDescriptor: WorkspaceDescriptor;
+ file: WorkspaceFile;
+ style?: React.CSSProperties;
+}) {
return (
- <DataList aria-label="file-data-list">
+ <DataList aria-label="file-data-list" style={props.style}>
<DataListItem style={{ border: 0 }}>
<FileLink file={props.file}>
<DataListItemRow>
diff --git a/packages/online-editor/src/home/HomePage.tsx b/packages/online-editor/src/home/HomePage.tsx
index c10e8f328e..3e6a07446d 100644
--- a/packages/online-editor/src/home/HomePage.tsx
+++ b/packages/online-editor/src/home/HomePage.tsx
@@ -69,19 +69,26 @@ import { UploadCard } from "./UploadCard";
import { ImportFromUrlCard } from "../importFromUrl/ImportFromUrlHomePageCard";
import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin";
import { PlusIcon } from "@patternfly/react-icons/dist/js/icons/plus-icon";
-import { NewFileDropdownMenu } from "../editor/NewFileDropdownMenu";
+import { NewFileDropdownMenu } from "../editor/Toolbar/NewFileDropdownMenu";
import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner";
import { useRoutes } from "../navigation/Hooks";
import { ErrorBoundary } from "../reactExt/ErrorBoundary";
import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
import { VariableSizeList } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
-import { FileDataList, FileLink, getFileDataListHeight, SingleFileWorkspaceListItem } from "../filesList/FileDataList";
+import {
+ FileDataList,
+ FileLink,
+ FileListItemDisplayMode,
+ getFileDataListHeight,
+ SingleFileWorkspaceListItem,
+} from "../filesList/FileDataList";
import { WorkspaceListItem } from "../workspace/components/WorkspaceListItem";
import { WorkspaceLoadingCard } from "../workspace/components/WorkspaceLoadingCard";
import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
import { ResponsiveDropdown } from "../ResponsiveDropdown/ResponsiveDropdown";
import { ResponsiveDropdownToggle } from "../ResponsiveDropdown/ResponsiveDropdownToggle";
+import { listDeletedFiles } from "../workspace/components/WorkspaceStatusIndicator";
export function HomePage() {
const routes = useRoutes();
@@ -117,10 +124,6 @@ export function HomePage() {
document.title = "KIE Sandbox :: Home";
}, []);
- const buildInfo = useMemo(() => {
- return process.env["WEBPACK_REPLACE__buildInfo"];
- }, []);
-
return (
<OnlineEditorPage>
<PageSection
@@ -243,11 +246,6 @@ export function HomePage() {
}}
/>
</PageSection>
- {buildInfo && (
- <div className={"kie-tools--build-info"}>
- <Label>{buildInfo}</Label>
- </div>
- )}
</OnlineEditorPage>
);
}
@@ -569,7 +567,9 @@ export function WorkspacesListDrawerPanelContent(props: { workspaceId: string |
{({ index, style }) => (
<FileDataList
file={arrayWithModelsThenOtherFiles[index]}
- isEditable={index < models.length}
+ displayMode={
+ index < models.length ? FileListItemDisplayMode.enabled : FileListItemDisplayMode.readonly
+ }
style={style}
/>
)}
diff --git a/packages/online-editor/src/i18n/OnlineI18n.ts b/packages/online-editor/src/i18n/OnlineI18n.ts
index 8a4bbc2ef3..a6eec372c0 100644
--- a/packages/online-editor/src/i18n/OnlineI18n.ts
+++ b/packages/online-editor/src/i18n/OnlineI18n.ts
@@ -18,6 +18,7 @@ import { ReferenceDictionary, Wrapped } from "@kie-tools-core/i18n/dist/core";
import { CommonI18n } from "@kie-tools/i18n-common-dictionary";
import { DmnUnitablesI18n } from "@kie-tools/unitables-dmn/dist/i18n";
import { GistEnabledAuthProviderType, SupportedGitAuthProviders } from "../authProviders/AuthProvidersApi";
+import { SupportedActions } from "../workspace/components/GitStatusIndicatorActions";
interface OnlineDictionary extends ReferenceDictionary {
editorPage: {
@@ -64,6 +65,25 @@ interface OnlineDictionary extends ReferenceDictionary {
share: string;
embed: string;
};
+ accelerators: {
+ commitMessage: (acceleratorName: string) => string;
+ loadingAlert: (acceleratorName: string) => string;
+ successAlert: (acceleratorName: string) => string;
+ failAlert: (acceleratorName: string) => string;
+ acceleratorDescription: string;
+ acceleratorDetails: string;
+ dmnFilesMove: string;
+ dmnFilesLocation: string;
+ pmmlFilesMove: string;
+ pmmlFilesLocation: string;
+ bpmnFilesMove: string;
+ bpmnFilesLocation: string;
+ otherFilesMove: string;
+ otherFilesLocation: string;
+ applyAccelerator: string;
+ appliedAt: string;
+ applyDisclaimer: string;
+ };
devDeployments: {
common: {
deployYourModel: string;
@@ -516,6 +536,14 @@ interface OnlineDictionary extends ReferenceDictionary {
organizations: string;
};
};
+ gitStatusIndicatorActions: {
+ [key in SupportedActions]: {
+ title: string;
+ warning: string;
+ description: string;
+ confirmButtonText: string;
+ };
+ };
}
export interface OnlineI18n extends OnlineDictionary, CommonI18n {}
diff --git a/packages/online-editor/src/i18n/locales/en.ts b/packages/online-editor/src/i18n/locales/en.ts
index 3df8360971..a20ad37aa4 100644
--- a/packages/online-editor/src/i18n/locales/en.ts
+++ b/packages/online-editor/src/i18n/locales/en.ts
@@ -18,7 +18,6 @@ import { OnlineI18n } from "..";
import { en as en_common } from "@kie-tools/i18n-common-dictionary";
import { en as en_unitables } from "@kie-tools/unitables/dist/i18n/locales/en";
import { wrapped } from "@kie-tools-core/i18n/dist/core";
-import { names } from "@kie-tools/i18n-common-dictionary/dist/names";
export const en: OnlineI18n = {
...en_common,
@@ -66,6 +65,28 @@ export const en: OnlineI18n = {
share: "Share",
embed: "Embed",
},
+ accelerators: {
+ commitMessage: (acceleratorName: string) =>
+ `${en_common.names.businessModeler.online}: Applying ${acceleratorName} Accelerator`,
+ loadingAlert: (acceleratorName: string) => `Applying ${acceleratorName} Accelerator...`,
+ successAlert: (acceleratorName: string) => `Successfully applied ${acceleratorName} Accelerator`,
+ failAlert: (acceleratorName: string) => `Failed to apply ${acceleratorName} Accelerator`,
+ acceleratorDescription:
+ "An Accelerator is a template. Applying it will move your current files according to the Accelerator specifications and create a new commit for it.",
+ acceleratorDetails: "This Accelerator is hosted at",
+ dmnFilesMove: "Decisions (.dmn) will be moved to:",
+ dmnFilesLocation: "Decisions (.dmn) were moved to:",
+ pmmlFilesMove: "Score cards (.pmml) will moved to:",
+ pmmlFilesLocation: "Score cards (.pmml) were moved to:",
+ bpmnFilesMove: "Workflows (.bpmn, .bpmn2) will be moved to:",
+ bpmnFilesLocation: "Workflows (.bpmn, .bpmn2) were moved to:",
+ otherFilesMove: "Other files will be moved to:",
+ otherFilesLocation: "Other files were moved to:",
+ applyAccelerator: "Apply Accelerator",
+ appliedAt: "This Accelerator was applied at:",
+ applyDisclaimer:
+ "This action is permanent. Any changes made after applying an Accelerator may result in your files being in different directories.",
+ },
devDeployments: {
common: {
deployYourModel: "Deploy",
@@ -626,4 +647,18 @@ export const en: OnlineI18n = {
organizations: "GitHub organizations",
},
},
+ gitStatusIndicatorActions: {
+ revert: {
+ title: "Revert",
+ warning: "This action is permanent",
+ description: "Are you sure you want to revert local changes to:",
+ confirmButtonText: "Yes, revert permanently",
+ },
+ revertAll: {
+ title: "Revert all changes",
+ warning: "This action is permanent",
+ description: "Are you sure? The following files will be reverted to the last commit:",
+ confirmButtonText: "Yes, revert permanently",
+ },
+ },
};
diff --git a/packages/online-editor/src/importFromUrl/ImportFromUrlHomePageCard.tsx b/packages/online-editor/src/importFromUrl/ImportFromUrlHomePageCard.tsx
index 28d42795c1..18fa3e4016 100644
--- a/packages/online-editor/src/importFromUrl/ImportFromUrlHomePageCard.tsx
+++ b/packages/online-editor/src/importFromUrl/ImportFromUrlHomePageCard.tsx
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-import { FlexItem } from "@patternfly/react-core";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/js/components/Card";
import { Form, FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
import { ValidatedOptions } from "@patternfly/react-core/dist/js/helpers/constants";
-import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
import { CodeIcon } from "@patternfly/react-icons/dist/js/icons/code-icon";
import { ExclamationCircleIcon } from "@patternfly/react-icons/dist/js/icons/exclamation-circle-icon";
import * as React from "react";
diff --git a/packages/online-editor/src/pageTemplate/OnlineEditorPage.tsx b/packages/online-editor/src/pageTemplate/OnlineEditorPage.tsx
index 3c6f5d2753..1a98f1afa4 100644
--- a/packages/online-editor/src/pageTemplate/OnlineEditorPage.tsx
+++ b/packages/online-editor/src/pageTemplate/OnlineEditorPage.tsx
@@ -27,7 +27,8 @@ import { DevDeploymentsDropdown } from "../devDeployments/DevDeploymentsDropdown
import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
import { AccountsIcon } from "../accounts/AccountsIcon";
import { AboutButton } from "../aboutModal/AboutButton";
-import { MastheadContent, Toolbar, ToolbarContent } from "@patternfly/react-core";
+import { MastheadContent } from "@patternfly/react-core/dist/js/components/Masthead";
+import { Toolbar, ToolbarContent } from "@patternfly/react-core/dist/js/components/Toolbar";
import { ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/js/components/Toolbar";
export function OnlineEditorPage(props: { children?: React.ReactNode }) {
diff --git a/packages/online-editor/src/workspace/components/GitStatusIndicatorActions.tsx b/packages/online-editor/src/workspace/components/GitStatusIndicatorActions.tsx
new file mode 100644
index 0000000000..818ea1ee7d
--- /dev/null
+++ b/packages/online-editor/src/workspace/components/GitStatusIndicatorActions.tsx
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { PromiseState } from "@kie-tools-core/react-hooks/dist/PromiseState";
+import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext";
+import { WorkspaceGitStatusType } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks";
+import {
+ FileModificationStatus,
+ UnstagedModifiedFilesStatusEntryType,
+} from "@kie-tools-core/workspaces-git-fs/dist/services/GitService";
+import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor";
+
+import {
+ Alert,
+ AlertActionCloseButton,
+ AlertActionLink,
+ AlertVariant,
+} from "@patternfly/react-core/dist/js/components/Alert";
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownPosition,
+ KebabToggle,
+} from "@patternfly/react-core/dist/js/components/Dropdown";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Popover, PopoverPosition } from "@patternfly/react-core/dist/js/components/Popover";
+import { List, ListItem } from "@patternfly/react-core/dist/js/components/List";
+import { UndoAltIcon } from "@patternfly/react-icons/dist/js/icons/undo-alt-icon";
+import * as React from "react";
+import { useOnlineI18n } from "../../i18n";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
+import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
+import { listDeletedFiles } from "./WorkspaceStatusIndicator";
+import { FileLabel } from "../../filesList/FileLabel";
+import CaretDownIcon from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
+import { switchExpression } from "../../switchExpression/switchExpression";
+
+export type GitStatusProps = {
+ workspaceDescriptor: WorkspaceDescriptor;
+ workspaceGitStatusPromise: PromiseState<WorkspaceGitStatusType>;
+};
+
+export enum SupportedActions {
+ revert = "revert",
+ revertAll = "revertAll",
+}
+
+type GitStatusIndicatorActionType = {
+ id: SupportedActions;
+ titleText: string;
+ warningText: string;
+ titleIcon: JSX.Element;
+ description: JSX.Element;
+ alertVariant: AlertVariant;
+ confirmButtonText: string;
+ onConfirm: () => void;
+};
+interface GitStatusIndicatorActionsPropsBase {
+ gitStatusProps: GitStatusProps;
+ isOpen: boolean;
+ setOpen: (isOpen: boolean) => void;
+ currentWorkspaceFile?: WorkspaceFile;
+ onDeletedWorkspaceFile?: () => void;
+}
+
+interface GitStatusIndicatorActionsSingleFileProps extends GitStatusIndicatorActionsPropsBase {
+ workspaceFile: WorkspaceFile;
+}
+interface GitStatusIndicatorActionsMultipleFilesProps extends GitStatusIndicatorActionsPropsBase {
+ workspaceFiles: WorkspaceFile[];
+}
+const isSingleFile = (
+ maybeSingleFile: GitStatusIndicatorActionsSingleFileProps | GitStatusIndicatorActionsMultipleFilesProps
+): maybeSingleFile is GitStatusIndicatorActionsSingleFileProps => {
+ return "workspaceFile" in maybeSingleFile;
+};
+
+export const GitStatusIndicatorActions = (
+ props: GitStatusIndicatorActionsSingleFileProps | GitStatusIndicatorActionsMultipleFilesProps
+) => {
+ const { i18n } = useOnlineI18n();
+ const workspaces = useWorkspaces();
+
+ const renderFile = React.useCallback(
+ (file: WorkspaceFile) => (
+ <Flex direction={{ default: "row" }} flexWrap={{ default: "nowrap" }} spacer={{ default: "spacerMd" }}>
+ <FlexItem style={{ minWidth: 0 /* This is to make the flex parent not overflow horizontally */ }}>
+ <Tooltip distance={5} position={"top-start"} content={file.nameWithoutExtension}>
+ <TextContent>
+ <Text component={TextVariants.p}>{file.nameWithoutExtension}</Text>
+ </TextContent>
+ </Tooltip>
+ </FlexItem>
+ <FlexItem>
+ <FileLabel extension={file.extension} />
+ </FlexItem>
+ <FlexItem>
+ {switchExpression(
+ props.gitStatusProps.workspaceGitStatusPromise.data?.unstagedModifiedFilesStatus.find(
+ (entry) => entry.path === file.relativePath
+ )?.status,
+ {
+ added: (
+ <Tooltip content={"Added"} position={"right"}>
+ <small>
+ <i>A</i>
+ </small>
+ </Tooltip>
+ ),
+ deleted: (
+ <Tooltip content={"Deleted"} position={"right"}>
+ <small>
+ <i>D</i>
+ </small>
+ </Tooltip>
+ ),
+ modified: (
+ <Tooltip content={"Modified"} position={"right"}>
+ <small>
+ <i>M</i>
+ </small>
+ </Tooltip>
+ ),
+ default: <></>,
+ }
+ )}
+ </FlexItem>
+ </Flex>
+ ),
+ [props.gitStatusProps.workspaceGitStatusPromise.data?.unstagedModifiedFilesStatus]
+ );
+
+ const workspaceFilesRendered = React.useMemo(() => {
+ return isSingleFile(props) ? (
+ <b>{renderFile(props.workspaceFile)}</b>
+ ) : (
+ <b>
+ <List>
+ {[...props.workspaceFiles, ...listDeletedFiles(props.gitStatusProps)]
+ .filter((file) =>
+ props.gitStatusProps.workspaceGitStatusPromise.data?.unstagedModifiedFilesStatus.some(
+ (entry) => entry.path === file.relativePath
+ )
+ )
+ .map((file, index) => (
+ <ListItem key={index}>{renderFile(file)}</ListItem>
+ ))}
+ </List>
+ </b>
+ );
+ }, [props, renderFile]);
+
+ const filterAcceptedStageStatuses = React.useCallback(
+ (acceptedValues: FileModificationStatus[]) => {
+ return (stageStatusEntry: UnstagedModifiedFilesStatusEntryType) => {
+ return (
+ acceptedValues.includes(stageStatusEntry.status) &&
+ (isSingleFile(props)
+ ? [props.workspaceFile.relativePath]
+ : props.gitStatusProps.workspaceGitStatusPromise!.data!.unstagedModifiedFilesStatus.map(({ path }) => path)
+ ).includes(stageStatusEntry.path)
+ );
+ };
+ },
+ [props]
+ );
+
+ const actionRevert = React.useCallback((): GitStatusIndicatorActionType | undefined => {
+ const doRevert = async (filepaths: string[]) => {
+ await workspaces.checkoutFilesFromLocalHead({
+ workspaceId: props.gitStatusProps.workspaceDescriptor.workspaceId,
+ ref: props.gitStatusProps.workspaceDescriptor.origin.branch,
+ filepaths,
+ });
+ if (
+ props.currentWorkspaceFile &&
+ !(await workspaces.existsFile({
+ workspaceId: props.gitStatusProps.workspaceDescriptor.workspaceId,
+ relativePath: props.currentWorkspaceFile.relativePath,
+ }))
+ ) {
+ props.onDeletedWorkspaceFile?.();
+ }
+ };
+ const maybeWorkspaceGitStatus = props.gitStatusProps.workspaceGitStatusPromise?.data;
+
+ if (!maybeWorkspaceGitStatus) {
+ return;
+ }
+ const id = isSingleFile(props) ? SupportedActions.revert : SupportedActions.revertAll;
+
+ const filepaths = maybeWorkspaceGitStatus.unstagedModifiedFilesStatus
+ .filter(
+ filterAcceptedStageStatuses([
+ FileModificationStatus.deleted,
+ FileModificationStatus.modified,
+ FileModificationStatus.added,
+ ])
+ )
+ .map(({ path }) => path);
+
+ if (!filepaths.length) {
+ return;
+ }
+ return {
+ onConfirm: async () => await doRevert(filepaths),
+ id,
+ titleText: i18n.gitStatusIndicatorActions[id].title,
+ titleIcon: <UndoAltIcon />,
+ alertVariant: AlertVariant.warning,
+ warningText: i18n.gitStatusIndicatorActions[id].warning,
+ description: (
+ <>
+ <span>{i18n.gitStatusIndicatorActions[id].description}</span>
+ <br />
+ <br />
+ {workspaceFilesRendered}
+ <br />
+ </>
+ ),
+ confirmButtonText: i18n.gitStatusIndicatorActions[id].confirmButtonText,
+ };
+ }, [i18n.gitStatusIndicatorActions, props, filterAcceptedStageStatuses, workspaceFilesRendered, workspaces]);
+
+ const actions: GitStatusIndicatorActionType[] = React.useMemo(() => {
+ const maybeActions = [
+ // list all actions here, which (based on internal state) could be undefined
+ actionRevert(),
+ ];
+
+ const result: GitStatusIndicatorActionType[] = [];
+
+ maybeActions.forEach((action) => {
+ if (action !== undefined) {
+ result.push(action);
+ }
+ });
+
+ return result;
+ }, [actionRevert]);
+
+ return isSingleFile(props) ? (
+ <MultipleActionsButtonsWithPopover actions={actions} isOpen={props.isOpen} setOpen={props.setOpen} />
+ ) : (
+ <MultipleActionsPopoverWithDropdown
+ title={
+ <TextContent>
+ <Text component="h3">Local Changes</Text>
+ </TextContent>
+ }
+ description={
+ <Flex direction={{ default: "column" }}>
+ <FlexItem>
+ <TextContent>
+ <Text component="p">
+ {`There are local changes to the following files in`}
+ <i>{`'${props.gitStatusProps.workspaceDescriptor.name}'`}</i>
+ </Text>
+ </TextContent>
+ </FlexItem>
+ {workspaceFilesRendered}
+ </Flex>
+ }
+ actions={actions}
+ isOpen={props.isOpen}
+ setOpen={props.setOpen}
+ />
+ );
+};
+
+const MultipleActionsButtonsWithPopover = (props: {
+ actions: GitStatusIndicatorActionType[];
+ isOpen: boolean;
+ setOpen: (value: boolean) => void;
+}) => {
+ const [openedPopoverId, setOpenedPopoverId] = React.useState<string>();
+ return (
+ <Flex spaceItems={{ default: "spaceItemsXs" }}>
+ {props.actions.map((action) => (
+ <SingleActionPopover
+ key={action.id}
+ action={action}
+ isOpen={props.isOpen && openedPopoverId === action.id}
+ setOpen={(isOpen: boolean) => {
+ setOpenedPopoverId(isOpen ? action.id : undefined);
+ props.setOpen(isOpen);
+ }}
+ />
+ ))}
+ </Flex>
+ );
+};
+
+const MultipleActionsPopoverWithDropdown = (props: {
+ actions: GitStatusIndicatorActionType[];
+ title: React.ReactNode;
+ description: JSX.Element;
+ isOpen: boolean;
+ setOpen: (isOpen: boolean) => void;
+}) => {
+ const [selectedActionId, setSelectedActionId] = React.useState<string>();
+ const [isDropdownOpen, setDropdownOpen] = React.useState<boolean>(false);
+
+ const selectedAction = React.useMemo(() => {
+ return props.actions.find((action) => action.id === selectedActionId);
+ }, [props.actions, selectedActionId]);
+
+ return (
+ <Popover
+ id={"popover-with-actions-dropdown"}
+ className={selectedActionId !== undefined ? " kie-tools--git-status-indicator__popover-no-padding" : ""} // noPadding prop not working nice for the ActionsAlert
+ position={PopoverPosition.bottom}
+ showClose={false}
+ isVisible={props.isOpen}
+ shouldOpen={() => props.setOpen(true)}
+ shouldClose={() => {
... 57738 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org