From d9155b67e5f6e801126b6bdff0b1759a9b8f93bf Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:35:01 +0530 Subject: [PATCH] feat: signposting update (#24389) --- .../RepoLimitExceededErrorModal_spec.js | 3 +- .../FirstTimeUserOnboarding_spec.js | 269 ++---- .../ClientSide/Onboarding/GuidedTour_spec.js | 4 +- .../locators/FirstTimeUserOnboarding.json | 31 +- app/client/cypress/support/Pages/HomePage.ts | 1 - .../cypress/support/Pages/Onboarding.ts | 57 +- app/client/cypress/support/commands.js | 1 - app/client/src/actions/onboardingActions.ts | 56 ++ .../src/ce/constants/ReduxActionConstants.tsx | 6 + app/client/src/ce/constants/messages.ts | 53 +- app/client/src/constants/Layers.tsx | 1 + .../AnonymousDataPopup.tsx | 90 +- .../Checklist.test.tsx | 57 +- .../FirstTimeUserOnboarding/Checklist.tsx | 879 ++++++++++-------- .../FirstTimeUserOnboarding/HelpMenu.tsx | 140 +++ .../IntroductionModal.tsx | 226 ----- .../Editor/FirstTimeUserOnboarding/Modal.tsx | 61 ++ .../FirstTimeUserOnboarding/Overlay.tsx | 46 + .../Statusbar.test.tsx | 106 ++- .../FirstTimeUserOnboarding/Statusbar.tsx | 274 ++---- .../FirstTimeUserOnboarding/Tasks.test.tsx | 157 ---- .../Editor/FirstTimeUserOnboarding/Tasks.tsx | 344 ------- .../TooltipContent.tsx | 131 +++ .../Editor/FirstTimeUserOnboarding/Utils.ts | 17 +- .../FirstTimeUserOnboarding/testUtils.ts | 1 + .../GeneratePageForm/GeneratePageForm.tsx | 22 - app/client/src/pages/Editor/HelpButton.tsx | 224 +++-- .../Editor/WidgetsEditor/CanvasContainer.tsx | 9 +- .../src/pages/Editor/WidgetsEditor/index.tsx | 129 ++- app/client/src/pages/Editor/index.tsx | 2 + .../reducers/uiReducers/onBoardingReducer.ts | 74 ++ app/client/src/sagas/InitSagas.ts | 2 + app/client/src/sagas/OnboardingSagas.ts | 105 ++- app/client/src/selectors/editorSelectors.tsx | 7 +- app/client/src/selectors/entitiesSelector.ts | 7 + .../src/selectors/onboardingSelectors.tsx | 49 +- app/client/src/utils/AnalyticsUtil.tsx | 14 +- app/client/src/utils/lazyLottie.ts | 7 +- app/client/src/utils/storage.ts | 1 + 39 files changed, 1756 insertions(+), 1907 deletions(-) create mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/HelpMenu.tsx delete mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/IntroductionModal.tsx create mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/Modal.tsx create mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/Overlay.tsx delete mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.test.tsx delete mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.tsx create mode 100644 app/client/src/pages/Editor/FirstTimeUserOnboarding/TooltipContent.tsx diff --git a/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js index 776ae252a0..dd83568807 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js @@ -13,7 +13,8 @@ describe("Repo Limit Exceeded Error Modal", function () { repoName2 = uuid.v4().split("-")[0]; repoName3 = uuid.v4().split("-")[0]; repoName4 = uuid.v4().split("-")[0]; - _.agHelper.ClickButton("Build on my own"); + _.agHelper.AssertElementVisible(_.locators._sidebar); + _.onboarding.closeIntroModal(); }); it("1. Modal should be opened with proper components", function () { diff --git a/app/client/cypress/e2e/Regression/ClientSide/Onboarding/FirstTimeUserOnboarding_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Onboarding/FirstTimeUserOnboarding_spec.js index 90be4752d2..7f5ce85f03 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Onboarding/FirstTimeUserOnboarding_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Onboarding/FirstTimeUserOnboarding_spec.js @@ -6,6 +6,7 @@ import { homePage, onboarding, draggableWidgets, + debuggerHelper, } from "../../../../support/Objects/ObjectsCore"; const datasource = require("../../../../locators/DatasourcesEditor.json"); @@ -19,7 +20,7 @@ describe("FirstTimeUserOnboarding", function () { it("1. onboarding flow - should check page entity selection in explorer", function () { cy.get(OnboardingLocator.introModal).should("be.visible"); - cy.get(OnboardingLocator.introModalBuild).click(); + cy.get(OnboardingLocator.checklistDatasourceBtn).click(); cy.get(OnboardingLocator.introModal).should("not.exist"); cy.get(".t--entity-name:contains(Page1)") .trigger("mouseover") @@ -29,42 +30,43 @@ describe("FirstTimeUserOnboarding", function () { it( "excludeForAirgap", - "2. onboarding flow - should check the checklist page actions", + "2. onboarding flow - should check the checklist actions", function () { - agHelper.GetNClick(OnboardingLocator.introModalBuild); - agHelper.GetNClick(OnboardingLocator.statusbar); agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "0 of 5"); - agHelper.GetNClick(OnboardingLocator.checklistBack); - agHelper.GetNClick(OnboardingLocator.statusbar); - agHelper.AssertElementEnabledDisabled( - OnboardingLocator.checklistDatasourceBtn, - 0, - false, - ); + agHelper.AssertElementExist(OnboardingLocator.checklistDatasourceBtn); agHelper.GetNClick(OnboardingLocator.checklistDatasourceBtn); agHelper.AssertElementVisible(OnboardingLocator.datasourcePage); agHelper.GetNClick(OnboardingLocator.datasourceMock); agHelper.Sleep(); - agHelper.GetNClick(OnboardingLocator.statusbar); + agHelper.GetNClick(debuggerHelper.locators._helpButton); agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "1 of 5"); - agHelper.AssertElementAbsence(OnboardingLocator.checklistDatasourceBtn); + agHelper + .GetElement(OnboardingLocator.checklistDatasourceBtn) + .realHover() + .should("have.css", "cursor", "auto"); agHelper.GetNClick(OnboardingLocator.checklistActionBtn); agHelper.GetNClick(OnboardingLocator.createQuery); agHelper.Sleep(); - agHelper.GetNClick(OnboardingLocator.statusbar); + agHelper.GetNClick(debuggerHelper.locators._helpButton); agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "2 of 5"); - agHelper.AssertElementAbsence(OnboardingLocator.checklistActionBtn); + agHelper + .GetElement(OnboardingLocator.checklistActionBtn) + .realHover() + .should("have.css", "cursor", "auto"); agHelper.GetNClick(OnboardingLocator.checklistWidgetBtn); agHelper.AssertElementVisible(OnboardingLocator.widgetSidebar); entityExplorer.DragDropWidgetNVerify(draggableWidgets.TEXT); - agHelper.GetNClick(OnboardingLocator.statusbar); + agHelper.GetNClick(debuggerHelper.locators._helpButton); agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "3 of 5"); - agHelper.AssertElementAbsence(OnboardingLocator.checklistWidgetBtn); + agHelper + .GetElement(OnboardingLocator.checklistWidgetBtn) + .realHover() + .should("have.css", "cursor", "auto"); agHelper.GetNClick(OnboardingLocator.checklistConnectionBtn); agHelper.AssertElementVisible(OnboardingLocator.snipingBanner); @@ -75,9 +77,12 @@ describe("FirstTimeUserOnboarding", function () { .wait(500); agHelper.GetNClick(OnboardingLocator.widgetName); - agHelper.GetNClick(OnboardingLocator.statusbar); + agHelper.GetNClick(debuggerHelper.locators._helpButton); agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "4 of 5"); - agHelper.AssertElementAbsence(OnboardingLocator.checklistConnectionBtn); + agHelper + .GetElement(OnboardingLocator.checklistConnectionBtn) + .realHover() + .should("have.css", "cursor", "auto"); let open; cy.window().then((window) => { @@ -86,7 +91,8 @@ describe("FirstTimeUserOnboarding", function () { }); agHelper.GetNClick(OnboardingLocator.checklistDeployBtn); - agHelper.GetNAssertContains(OnboardingLocator.checklistStatus, "5 of 5"); + agHelper.GetNClick(debuggerHelper.locators._helpButton); + agHelper.AssertElementExist(OnboardingLocator.checklistCompletionBanner); agHelper.AssertElementAbsence(OnboardingLocator.checklistDeployBtn); cy.window().then((window) => { @@ -99,17 +105,16 @@ describe("FirstTimeUserOnboarding", function () { "airgap", "2. onboarding flow - should check the checklist page actions - airgap", function () { - cy.get(OnboardingLocator.introModalBuild).click(); + cy.get(OnboardingLocator.introModal).should("be.visible"); - cy.get(OnboardingLocator.statusbar).click(); cy.get(OnboardingLocator.checklistStatus).should("be.visible"); cy.get(OnboardingLocator.checklistStatus).should("contain", "0 of 5"); - cy.get(OnboardingLocator.checklistBack).click(); - cy.get(OnboardingLocator.statusbar).click(); - cy.get(OnboardingLocator.checklistDatasourceBtn).should( - "not.be.disabled", - ); + agHelper + .GetElement(OnboardingLocator.checklistDatasourceBtn) + .realHover() + .should("have.css", "cursor", "pointer"); + cy.get(OnboardingLocator.checklistDatasourceBtn).click(); cy.get(OnboardingLocator.datasourcePage).should("be.visible"); cy.get(datasource.MongoDB).click(); @@ -120,24 +125,33 @@ describe("FirstTimeUserOnboarding", function () { }); cy.testSaveDatasource(); cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); + agHelper.GetNClick(debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "1 of 5"); - cy.get(OnboardingLocator.checklistDatasourceBtn).should("not.exist"); + agHelper + .GetElement(OnboardingLocator.checklistDatasourceBtn) + .realHover() + .should("have.css", "cursor", "auto"); cy.get(OnboardingLocator.checklistActionBtn).should("be.visible"); cy.get(OnboardingLocator.checklistActionBtn).click(); cy.get(OnboardingLocator.createQuery).should("be.visible"); cy.get(OnboardingLocator.createQuery).click(); cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); + agHelper.GetNClick(debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "2 of 5"); - cy.get(OnboardingLocator.checklistActionBtn).should("not.exist"); + agHelper + .GetElement(OnboardingLocator.checklistActionBtn) + .realHover() + .should("have.css", "cursor", "auto"); cy.get(OnboardingLocator.checklistWidgetBtn).should("be.visible"); cy.get(OnboardingLocator.checklistWidgetBtn).click(); cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.statusbar).click(); + agHelper.GetNClick(debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "3 of 5"); - cy.get(OnboardingLocator.checklistWidgetBtn).should("not.exist"); + agHelper + .GetElement(OnboardingLocator.checklistWidgetBtn) + .realHover() + .should("have.css", "cursor", "auto"); cy.get(OnboardingLocator.checklistConnectionBtn).should("be.visible"); cy.get(OnboardingLocator.checklistConnectionBtn).click(); @@ -148,9 +162,12 @@ describe("FirstTimeUserOnboarding", function () { .wait(500); cy.get(OnboardingLocator.widgetName).should("be.visible"); cy.get(OnboardingLocator.widgetName).click(); - cy.get(OnboardingLocator.statusbar).click(); + agHelper.GetNClick(debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "4 of 5"); - cy.get(OnboardingLocator.checklistConnectionBtn).should("not.exist"); + agHelper + .GetElement(OnboardingLocator.checklistConnectionBtn) + .realHover() + .should("have.css", "cursor", "auto"); let open; cy.window().then((window) => { @@ -159,172 +176,17 @@ describe("FirstTimeUserOnboarding", function () { }); cy.get(OnboardingLocator.checklistDeployBtn).should("be.visible"); cy.get(OnboardingLocator.checklistDeployBtn).click(); - cy.get(OnboardingLocator.checklistStatus).should("contain", "5 of 5"); - cy.get(OnboardingLocator.checklistDeployBtn).should("not.exist"); + agHelper.AssertElementExist(OnboardingLocator.checklistCompletionBanner); + agHelper.AssertElementAbsence(OnboardingLocator.checklistDeployBtn); cy.window().then((window) => { window.open = open; }); }, ); - it( - "excludeForAirgap", - "3. onboarding flow - should check the tasks page actions", - function () { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_DATASOURCE_HEADER(), - ); - cy.get(OnboardingLocator.taskDatasourceBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(OnboardingLocator.datasourceMock).first().click(); - cy.wait(1000); - cy.get(OnboardingLocator.datasourceBackBtn).click(); - cy.get(OnboardingLocator.taskDatasourceBtn).should("not.exist"); - - cy.get(OnboardingLocator.taskActionBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_QUERY_HEADER(), - ); - cy.get(OnboardingLocator.taskActionBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(OnboardingLocator.createQuery).first().click(); - cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); - cy.get(OnboardingLocator.checklistBack).click(); - cy.get(OnboardingLocator.taskActionBtn).should("not.exist"); - - cy.get(OnboardingLocator.taskWidgetBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_WIDGET_HEADER(), - ); - cy.get(OnboardingLocator.taskWidgetBtn).click(); - cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); - cy.get(OnboardingLocator.dropTarget).should("be.visible"); - cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.textWidgetName).should("be.visible"); - cy.get(OnboardingLocator.taskWidgetBtn).should("not.exist"); - }, - ); - - it( - "airgap", - "3. onboarding flow - should check the tasks page actions - airgap", - function () { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_DATASOURCE_HEADER(), - ); - cy.get(OnboardingLocator.taskDatasourceBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(datasource.MongoDB).click(); - cy.fillMongoDatasourceForm(); - cy.generateUUID().then((uid) => { - datasourceName = `Mongo CRUD ds ${uid}`; - cy.renameDatasource(datasourceName); - }); - cy.testSaveDatasource(); - cy.wait(1000); - cy.get(".t--close-editor").click(); - cy.wait(1000); - cy.get(OnboardingLocator.datasourceBackBtn).click(); - cy.get(OnboardingLocator.taskDatasourceBtn).should("not.exist"); - - cy.get(OnboardingLocator.taskActionBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_QUERY_HEADER(), - ); - cy.get(OnboardingLocator.taskActionBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(OnboardingLocator.createQuery).first().click(); - cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); - cy.get(OnboardingLocator.checklistBack).click(); - cy.get(OnboardingLocator.taskActionBtn).should("not.exist"); - - cy.get(OnboardingLocator.taskWidgetBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceHeader).contains( - Cypress.env("MESSAGES").ONBOARDING_TASK_WIDGET_HEADER(), - ); - cy.get(OnboardingLocator.taskWidgetBtn).click(); - cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); - cy.get(OnboardingLocator.dropTarget).should("be.visible"); - cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.textWidgetName).should("be.visible"); - cy.get(OnboardingLocator.taskWidgetBtn).should("not.exist"); - }, - ); - - it("4. onboarding flow - should check the tasks page datasource action alternate widget action", function () { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceAltBtn).click(); - cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); - cy.get(OnboardingLocator.dropTarget).should("be.visible"); - cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.textWidgetName).should("be.visible"); - }); - - it( - "airgap", - "5. onboarding flow - should check the tasks page query action alternate widget action - airgap", - function () { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(datasource.MongoDB).click(); - cy.fillMongoDatasourceForm(); - cy.generateUUID().then((uid) => { - datasourceName = `Mongo CRUD ds ${uid}`; - cy.renameDatasource(datasourceName); - }); - cy.testSaveDatasource(); - cy.wait(1000); - cy.get(".t--close-editor").click(); - cy.wait(1000); - cy.get(OnboardingLocator.datasourceBackBtn).click(); - - cy.get(OnboardingLocator.taskActionBtn).should("be.visible"); - cy.get(OnboardingLocator.taskActionAltBtn).click(); - cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); - cy.get(OnboardingLocator.dropTarget).should("be.visible"); - cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.textWidgetName).should("be.visible"); - }, - ); - - it( - "excludeForAirgap", - "5. onboarding flow - should check the tasks page query action alternate widget action", - function () { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); - cy.get(OnboardingLocator.taskDatasourceBtn).click(); - cy.get(OnboardingLocator.datasourcePage).should("be.visible"); - cy.get(OnboardingLocator.datasourceMock).first().click(); - cy.wait(1000); - cy.get(OnboardingLocator.datasourceBackBtn).click(); - - cy.get(OnboardingLocator.taskActionBtn).should("be.visible"); - cy.get(OnboardingLocator.taskActionAltBtn).click(); - cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); - cy.get(OnboardingLocator.dropTarget).should("be.visible"); - cy.dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.textWidgetName).should("be.visible"); - }, - ); - - it("6. onboarding flow - should check directly opening widget pane", function () { - cy.get(OnboardingLocator.introModalBuild).click(); - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); + it("3. onboarding flow - should check directly opening widget pane", function () { + cy.get(OnboardingLocator.checklistDatasourceBtn).should("be.visible"); + agHelper.GetNClick(OnboardingLocator.introModalCloseBtn); entityExplorer.NavigateToSwitcher("Widgets"); cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); cy.get(OnboardingLocator.dropTarget).should("be.visible"); @@ -336,21 +198,23 @@ describe("FirstTimeUserOnboarding", function () { "response.body.responseMeta.status", 200, ); - cy.get(OnboardingLocator.statusbar).should("be.visible"); + agHelper.GetNClick(debuggerHelper.locators._helpButton); + agHelper.AssertElementVisible(OnboardingLocator.introModal); cy.get(OnboardingLocator.textWidgetName).should("be.visible"); }); - it("7. onboarding flow - new apps created should start with signposting", function () { - cy.get(OnboardingLocator.introModalBuild).click(); - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); + it("4. onboarding flow - new apps created should start with signposting", function () { + cy.get(OnboardingLocator.checklistDatasourceBtn).should("be.visible"); + agHelper.GetNClick(OnboardingLocator.introModalCloseBtn); homePage.NavigateToHome(); homePage.CreateNewApplication(false); - cy.get(OnboardingLocator.taskDatasourceBtn).should("be.visible"); + agHelper.GetNClick(debuggerHelper.locators._helpButton); + cy.get(OnboardingLocator.checklistDatasourceBtn).should("be.visible"); }); - it("8. onboarding flow - once signposting is completed new apps won't start with signposting", function () { + it("5. onboarding flow - once signposting is completed new apps won't start with signposting", function () { onboarding.completeSignposting(); homePage.NavigateToHome(); @@ -358,6 +222,7 @@ describe("FirstTimeUserOnboarding", function () { homePage.CreateNewApplication(false); agHelper.AssertElementExist(locators._dropHere); - agHelper.AssertElementAbsence(OnboardingLocator.statusbar); + agHelper.GetNClick(debuggerHelper.locators._helpButton); + agHelper.AssertElementAbsence(OnboardingLocator.introModal); }); }); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Onboarding/GuidedTour_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Onboarding/GuidedTour_spec.js index 4a656eda7a..f09ccb5429 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Onboarding/GuidedTour_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Onboarding/GuidedTour_spec.js @@ -16,8 +16,8 @@ describe("excludeForAirgap", "Guided Tour", function () { cy.generateUUID().then((uid) => { cy.Signup(`${uid}@appsmith.com`, uid); }); - cy.get(onboardingLocators.introModalWelcomeTourBtn).should("be.visible"); - cy.get(onboardingLocators.introModalWelcomeTourBtn).click(); + cy.get(onboardingLocators.editorWelcomeTourBtn).should("be.visible"); + cy.get(onboardingLocators.editorWelcomeTourBtn).click(); cy.get(onboardingLocators.welcomeTourBtn).should("be.visible"); }); diff --git a/app/client/cypress/locators/FirstTimeUserOnboarding.json b/app/client/cypress/locators/FirstTimeUserOnboarding.json index 5127352792..707cc1c4e7 100644 --- a/app/client/cypress/locators/FirstTimeUserOnboarding.json +++ b/app/client/cypress/locators/FirstTimeUserOnboarding.json @@ -1,17 +1,12 @@ { - "introModal": ".ads-v2-modal__content", - "introModalBuild": ".t--introduction-modal-build-button", - "introModalWelcomeTourBtn": ".t--introduction-modal-welcome-tour-button", - "introModalCloseBtn": ".ads-v2-modal__content-header-close-button", - "statusbar": ".t--onboarding-statusbar", - "statusbarClose": "[data-testid='statusbar-skip']", - "checklistStatus": ".t--checklist-complete-status", - "checklistDatasourceBtn": ".t--checklist-datasource-button", - "checklistBack": ".t--checklist-back", - "checklistActionBtn": ".t--checklist-action-button", - "checklistWidgetBtn": ".t--checklist-widget-button", - "checklistConnectionBtn": ".t--checklist-connection-button", - "checklistDeployBtn": ".t--checklist-deploy-button", + "introModal": "[data-testid='signposting-modal']", + "introModalCloseBtn": "[data-testid='signposting-modal-close-btn']", + "checklistStatus": "[data-testid='checklist-completion-info']", + "checklistDatasourceBtn": "[data-testid='checklist-datasource']", + "checklistActionBtn": "[data-testid='checklist-action']", + "checklistWidgetBtn": "[data-testid='checklist-widget']", + "checklistConnectionBtn": "[data-testid='checklist-connection']", + "checklistDeployBtn": "[data-testid='checklist-deploy']", "datasourcePage": ".t--integrationsHomePage", "datasourceMock": ".t--mock-datasource", "createQuery": ".t--create-query", @@ -20,13 +15,9 @@ "snipingTextWidget": ".t--snipeable-textwidget", "widgetName": ".t--widget-name", "datasourceBackBtn": ".t--back-button", - "taskDatasourceBtn": ".t--tasks-datasource-button", - "taskDatasourceHeader": ".t--tasks-datasource-header", - "taskActionBtn": ".t--tasks-action-button", - "taskWidgetBtn": ".t--tasks-widget-button", "dropTarget": ".t--drop-target", "textWidgetName": ".t--widget-textwidget", - "taskDatasourceAltBtn": ".t--tasks-datasource-alternate-button", - "taskActionAltBtn": ".t--tasks-action-alternate-button", - "welcomeTourBtn": ".t--start-building" + "welcomeTourBtn": ".t--start-building", + "editorWelcomeTourBtn": "[data-testid='editor-welcome-tour']", + "checklistCompletionBanner": "[data-testid='checklist-completion-banner']" } diff --git a/app/client/cypress/support/Pages/HomePage.ts b/app/client/cypress/support/Pages/HomePage.ts index 6db40c9df2..7ab97cea99 100644 --- a/app/client/cypress/support/Pages/HomePage.ts +++ b/app/client/cypress/support/Pages/HomePage.ts @@ -234,7 +234,6 @@ export class HomePage { if (skipSignposting) { this.agHelper.AssertElementVisible(this.entityExplorer._entityExplorer); this.onboarding.closeIntroModal(); - this.onboarding.skipSignposting(); } this.assertHelper.AssertNetworkStatus("getWorkspace"); } diff --git a/app/client/cypress/support/Pages/Onboarding.ts b/app/client/cypress/support/Pages/Onboarding.ts index 7981373fb1..5b865e517f 100644 --- a/app/client/cypress/support/Pages/Onboarding.ts +++ b/app/client/cypress/support/Pages/Onboarding.ts @@ -6,19 +6,19 @@ let datasourceName; export class Onboarding { private _aggregateHelper = ObjectsRegistry.AggregateHelper; private _datasources = ObjectsRegistry.DataSources; + private _debuggerHelper = ObjectsRegistry.DebuggerHelper; completeSignposting() { - cy.get(OnboardingLocator.introModalBuild).click(); - - cy.get(OnboardingLocator.statusbar).click(); cy.get(OnboardingLocator.checklistStatus).should("be.visible"); cy.get(OnboardingLocator.checklistStatus).should("contain", "0 of 5"); - cy.get(OnboardingLocator.checklistBack).click(); - cy.get(OnboardingLocator.statusbar).click(); - cy.get(OnboardingLocator.checklistDatasourceBtn).should("not.be.disabled"); + this._aggregateHelper + .GetElement(OnboardingLocator.checklistConnectionBtn) + .realHover() + .should("have.css", "cursor", "not-allowed"); cy.get(OnboardingLocator.checklistDatasourceBtn).click(); cy.get(OnboardingLocator.datasourcePage).should("be.visible"); + this._aggregateHelper.AssertElementAbsence(OnboardingLocator.introModal); if (Cypress.env("AIRGAPPED")) { this._datasources.CreateDataSource("Mongo"); cy.get("@dsName").then(($dsName) => { @@ -28,24 +28,33 @@ export class Onboarding { cy.get(OnboardingLocator.datasourceMock).first().click(); } cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); + this._aggregateHelper.GetNClick(this._debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "1 of 5"); - cy.get(OnboardingLocator.checklistDatasourceBtn).should("not.exist"); + this._aggregateHelper + .GetElement(OnboardingLocator.checklistConnectionBtn) + .realHover() + .should("have.css", "cursor", "not-allowed"); cy.get(OnboardingLocator.checklistActionBtn).should("be.visible"); cy.get(OnboardingLocator.checklistActionBtn).click(); cy.get(OnboardingLocator.createQuery).should("be.visible"); cy.get(OnboardingLocator.createQuery).click(); cy.wait(1000); - cy.get(OnboardingLocator.statusbar).click(); + this._aggregateHelper.GetNClick(this._debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "2 of 5"); - cy.get(OnboardingLocator.checklistActionBtn).should("not.exist"); + this._aggregateHelper + .GetElement(OnboardingLocator.checklistActionBtn) + .realHover() + .should("have.css", "cursor", "auto"); cy.get(OnboardingLocator.checklistWidgetBtn).should("be.visible"); cy.get(OnboardingLocator.checklistWidgetBtn).click(); cy.get(OnboardingLocator.widgetSidebar).should("be.visible"); (cy as any).dragAndDropToCanvas("textwidget", { x: 400, y: 400 }); - cy.get(OnboardingLocator.statusbar).click(); + this._aggregateHelper.GetNClick(this._debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "3 of 5"); - cy.get(OnboardingLocator.checklistWidgetBtn).should("not.exist"); + this._aggregateHelper + .GetElement(OnboardingLocator.checklistWidgetBtn) + .realHover() + .should("have.css", "cursor", "auto"); cy.get(OnboardingLocator.checklistConnectionBtn).should("be.visible"); cy.get(OnboardingLocator.checklistConnectionBtn).click(); @@ -56,9 +65,12 @@ export class Onboarding { .wait(500); cy.get(OnboardingLocator.widgetName).should("be.visible"); cy.get(OnboardingLocator.widgetName).click(); - cy.get(OnboardingLocator.statusbar).click(); + this._aggregateHelper.GetNClick(this._debuggerHelper.locators._helpButton); cy.get(OnboardingLocator.checklistStatus).should("contain", "4 of 5"); - cy.get(OnboardingLocator.checklistConnectionBtn).should("not.exist"); + this._aggregateHelper + .GetElement(OnboardingLocator.checklistConnectionBtn) + .realHover() + .should("have.css", "cursor", "auto"); let open: any; cy.window().then((window: any) => { @@ -67,8 +79,13 @@ export class Onboarding { }); cy.get(OnboardingLocator.checklistDeployBtn).should("be.visible"); cy.get(OnboardingLocator.checklistDeployBtn).click(); - cy.get(OnboardingLocator.checklistStatus).should("contain", "5 of 5"); - cy.get(OnboardingLocator.checklistDeployBtn).should("not.exist"); + this._aggregateHelper.AssertElementAbsence(OnboardingLocator.introModal); + this._aggregateHelper.Sleep(); + + this._aggregateHelper.GetNClick(this._debuggerHelper.locators._helpButton); + this._aggregateHelper.AssertElementExist( + OnboardingLocator.checklistCompletionBanner, + ); cy.window().then((window) => { window.open = open; }); @@ -81,12 +98,4 @@ export class Onboarding { } }); } - - skipSignposting() { - cy.get("body").then(($body) => { - if ($body.find(OnboardingLocator.statusbarClose).length) { - this._aggregateHelper.GetNClick(OnboardingLocator.statusbarClose); - } - }); - } } diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 3163aa2ee0..71484c9dc9 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2151,5 +2151,4 @@ Cypress.Commands.add("SelectFromMultiSelect", (options) => { Cypress.Commands.add("skipSignposting", () => { onboarding.closeIntroModal(); - onboarding.skipSignposting(); }); diff --git a/app/client/src/actions/onboardingActions.ts b/app/client/src/actions/onboardingActions.ts index aa6ff12593..ba7a2efbad 100644 --- a/app/client/src/actions/onboardingActions.ts +++ b/app/client/src/actions/onboardingActions.ts @@ -1,4 +1,5 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import type { SIGNPOSTING_STEP } from "pages/Editor/FirstTimeUserOnboarding/Utils"; import type { GUIDED_TOUR_STEPS } from "pages/Editor/GuidedTour/constants"; import type { GuidedTourState } from "reducers/uiReducers/guidedTourReducer"; import type { WidgetProps } from "widgets/BaseWidget"; @@ -26,6 +27,13 @@ export const removeFirstTimeUserOnboardingApplicationId = ( }; }; +export const showSignpostingModal = (payload: boolean) => { + return { + type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL, + payload, + }; +}; + export const disableStartSignpostingAction = () => { return { type: ReduxActionTypes.DISABLE_START_SIGNPOSTING, @@ -45,6 +53,54 @@ export const firstTimeUserOnboardingInit = ( }; }; +export const setSignpostingOverlay = (payload: boolean) => { + return { + type: ReduxActionTypes.SET_SIGNPOSTING_OVERLAY, + payload, + }; +}; + +export const signpostingMarkAllRead = () => { + return { + type: ReduxActionTypes.SIGNPOSTING_MARK_ALL_READ, + }; +}; + +export const signpostingStepUpdateInit = (payload: { + step: SIGNPOSTING_STEP; + completed: boolean; +}) => { + return { + type: ReduxActionTypes.SIGNPOSTING_STEP_UPDATE_INIT, + payload, + }; +}; + +export const signpostingStepUpdate = (payload: { + step: SIGNPOSTING_STEP; + completed: boolean; + read?: boolean; +}) => { + return { + type: ReduxActionTypes.SIGNPOSTING_STEP_UPDATE, + payload, + }; +}; + +export const showSignpostingTooltip = (payload: boolean) => { + return { + type: ReduxActionTypes.SIGNPOSTING_SHOW_TOOLTIP, + payload, + }; +}; + +export const showAnonymousDataPopup = (payload: boolean) => { + return { + type: ReduxActionTypes.SHOW_ANONYMOUS_DATA_POPUP, + payload, + }; +}; + export const markStepComplete = () => { return { type: ReduxActionTypes.GUIDED_TOUR_MARK_STEP_COMPLETED, diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index b422002fbc..99e2aa837e 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -616,6 +616,12 @@ const ActionTypes = { SET_FORCE_WIDGET_PANEL_OPEN: "SET_FORCE_WIDGET_PANEL_OPEN", END_FIRST_TIME_USER_ONBOARDING: "END_FIRST_TIME_USER_ONBOARDING", UNDO_END_FIRST_TIME_USER_ONBOARDING: "UNDO_END_FIRST_TIME_USER_ONBOARDING", + SET_SIGNPOSTING_OVERLAY: "SET_SIGNPOSTING_OVERLAY", + SIGNPOSTING_MARK_ALL_READ: "SIGNPOSTING_MARK_ALL_READ", + SIGNPOSTING_STEP_UPDATE_INIT: "SIGNPOSTING_STEP_UPDATE_INIT", + SIGNPOSTING_STEP_UPDATE: "SIGNPOSTING_STEP_UPDATE", + SIGNPOSTING_SHOW_TOOLTIP: "SIGNPOSTING_SHOW_TOOLTIP", + SHOW_ANONYMOUS_DATA_POPUP: "SHOW_ANONYMOUS_DATA_POPUP", FETCH_ADMIN_SETTINGS: "FETCH_ADMIN_SETTINGS", FETCH_ADMIN_SETTINGS_SUCCESS: "FETCH_ADMIN_SETTINGS_SUCCESS", FETCH_ADMIN_SETTINGS_ERROR: "FETCH_ADMIN_SETTINGS_ERROR", diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index a935eb6ad3..1d205923ae 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -982,24 +982,33 @@ export const ONBOARDING_CHECKLIST_BODY = () => "Let’s get you started on your first application, explore Appsmith yourself or follow our guide below to discover what Appsmith can do."; export const ONBOARDING_CHECKLIST_COMPLETE_TEXT = () => "complete"; +export const SIGNPOSTING_POPUP_SUBTITLE = () => + "These are all the things you need to do to build your first application."; +export const SIGNPOSTING_SUCCESS_POPUP = { + title: () => "🎉 Awesome! You’ve explored the basics of Appsmith", + subtitle: () => + "You can carry on building the app from here on. If you are still not sure, checkout our documentation or try guided tour.", +}; + export const ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE = { bold: () => "Connect your datasource", - normal: () => "to start building an application.", + normal: () => "to start building your app", }; export const ONBOARDING_CHECKLIST_CREATE_A_QUERY = { - bold: () => "Create a query", - normal: () => "of your datasource.", + bold: () => "Write a query", + normalPrefix: () => "to import your", + normal: () => "data into appsmith", }; export const ONBOARDING_CHECKLIST_ADD_WIDGETS = { - bold: () => "Start visualising your application", - normal: () => "using widgets.", + bold: () => "Drag & drop a widget,", + normal: () => "so you can build a beautiful UI", }; export const ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET = { bold: () => "Connect your data to the widgets", - normal: () => "using JavaScript.", + normal: () => "using JavaScript bindings", }; export const ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS = { @@ -1007,6 +1016,35 @@ export const ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS = { normal: () => "and see your creation live.", }; +export const SIGNPOSTING_LAST_STEP_TOOLTIP = () => "You are almost there!"; +export const SIGNPOSTING_TOOLTIP = { + DEFAULT: { + content: () => + "Finish these 5 steps to learn the basics in-order to build an app & deploy it. This would take 5 mins of your time.", + }, + CONNECT_A_DATASOURCE: { + content: () => "Let's add a datasource", + }, + CREATE_QUERY: { + content: () => + "You successfully connected a datasource. Now try to create a query.", + }, + ADD_WIDGET: { + content: () => + "You successfully created a query. Now its time to drag & drop a widget to bind data.", + }, + CONNECT_DATA_TO_WIDGET: { + content: () => + "You have a widget on the canvas now, its time to bind the data with it.", + }, + DEPLOY_APPLICATION: { + content: () => "Deploy you application to see what you’ve built.", + }, + DOCUMENTATION: { + content: () => "Open documentation", + }, +}; + export const ONBOARDING_CHECKLIST_FOOTER = () => "Not sure where to start? Take the welcome tour"; @@ -1033,6 +1071,9 @@ export const START_TUTORIAL = () => "Start tutorial"; export const WELCOME_TO_APPSMITH = () => "Welcome to Appsmith!"; export const QUERY_YOUR_DATABASE = () => "Query your own database or API inside Appsmith. Write JS to construct dynamic queries."; +export const SIGNPOSTING_INFO_MENU = { + documentation: () => "Open documentation", +}; //Statusbar export const ONBOARDING_STATUS_STEPS_FIRST = () => "First, add a datasource"; diff --git a/app/client/src/constants/Layers.tsx b/app/client/src/constants/Layers.tsx index 8ffbd8540c..52d410f40e 100644 --- a/app/client/src/constants/Layers.tsx +++ b/app/client/src/constants/Layers.tsx @@ -51,6 +51,7 @@ export const Layers = { productUpdates: Indices.Layer7, portals: Indices.Layer9, header: Indices.Layer9, + signpostingOverlay: Indices.Layer9, snipeableZone: Indices.Layer10, max: Indices.LayerMax, sideStickyBar: Indices.Layer7, diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/AnonymousDataPopup.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/AnonymousDataPopup.tsx index 0a820319a3..61c4dfdf49 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/AnonymousDataPopup.tsx +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/AnonymousDataPopup.tsx @@ -7,15 +7,54 @@ import { createMessage, } from "@appsmith/constants/messages"; import { ADMIN_SETTINGS_CATEGORY_DEFAULT_PATH } from "constants/routes"; -import { TELEMETRY_DOCS_PAGE_URL } from "./constants"; +import { + ANONYMOUS_DATA_POPOP_TIMEOUT, + TELEMETRY_DOCS_PAGE_URL, +} from "./constants"; +import { useDispatch, useSelector } from "react-redux"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { + getFirstTimeUserOnboardingComplete, + getIsAnonymousDataPopupVisible, + getIsFirstTimeUserOnboardingEnabled, +} from "selectors/onboardingSelectors"; +import { + getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown, + setFirstTimeUserOnboardingTelemetryCalloutVisibility, +} from "utils/storage"; +import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import { deleteCanvasCardsState } from "actions/editorActions"; +import styled from "styled-components"; +import { showAnonymousDataPopup } from "actions/onboardingActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; -export default function AnonymousDataPopup(props: { - onCloseCallout: () => void; -}) { +const Wrapper = styled.div` + margin: ${(props) => + `${props.theme.spaces[7]}px ${props.theme.spaces[16]}px 0px ${props.theme.spaces[13]}px`}; +`; + +export default function AnonymousDataPopup() { + const user = useSelector(getCurrentUser); + const isAdmin = user?.isSuperUser || false; + const isOnboardingCompleted = useSelector(getFirstTimeUserOnboardingComplete); + const isAnonymousDataPopupVisible = useSelector( + getIsAnonymousDataPopupVisible, + ); + const isFirstTimeUserOnboardingEnabled = useSelector( + getIsFirstTimeUserOnboardingEnabled, + ); + const dispatch = useDispatch(); + + const hideAnonymousDataPopup = () => { + dispatch(showAnonymousDataPopup(false)); + setFirstTimeUserOnboardingTelemetryCalloutVisibility(true); + }; + useEffect(() => { - AnalyticsUtil.logEvent("DISPLAY_TELEMETRY_CALLOUT"); - }, []); + if (isAnonymousDataPopupVisible) { + AnalyticsUtil.logEvent("DISPLAY_TELEMETRY_CALLOUT"); + } + }, [isAnonymousDataPopupVisible]); const handleLinkClick = (link: string) => { if (link === ADMIN_SETTINGS_CATEGORY_DEFAULT_PATH) { @@ -26,8 +65,39 @@ export default function AnonymousDataPopup(props: { window.open(link, "_blank"); }; + const showShowAnonymousDataPopup = async () => { + const shouldPopupShow = + !isAirgapped() && + isFirstTimeUserOnboardingEnabled && + isAdmin && + !isOnboardingCompleted; + if (shouldPopupShow) { + const isAnonymousDataPopupAlreadyOpen = + await getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown(); + //true if the modal was already shown else show the modal and set to already shown, also hide the modal after 10 secs + if (isAnonymousDataPopupAlreadyOpen) { + dispatch(showAnonymousDataPopup(false)); + } else { + dispatch(deleteCanvasCardsState()); + dispatch(showAnonymousDataPopup(true)); + setTimeout(() => { + hideAnonymousDataPopup(); + }, ANONYMOUS_DATA_POPOP_TIMEOUT); + await setFirstTimeUserOnboardingTelemetryCalloutVisibility(true); + } + } else { + dispatch(showAnonymousDataPopup(shouldPopupShow)); + } + }; + + useEffect(() => { + showShowAnonymousDataPopup(); + }, []); + + if (!isAnonymousDataPopupVisible) return null; + return ( -
+ handleLinkClick(TELEMETRY_DOCS_PAGE_URL), }, ]} - onClose={() => { - props.onCloseCallout(); - }} + onClose={hideAnonymousDataPopup} > {createMessage(ONBOARDING_TELEMETRY_POPUP)} -
+ ); } diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.test.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.test.tsx index e4338cc96e..ccf51cb760 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.test.tsx +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.test.tsx @@ -11,6 +11,7 @@ import { fireEvent, render, screen } from "test/testUtils"; import OnboardingChecklist from "./Checklist"; import { getStore, initialState } from "./testUtils"; import urlBuilder from "entities/URLRedirect/URLAssembly"; +import "@testing-library/jest-dom"; let container: any = null; @@ -32,6 +33,16 @@ jest.mock("utils/history", () => ({ listen: jest.fn(), })); +jest.mock("utils/lazyLottie", () => ({ + loadAnimation: () => { + return { + play: jest.fn(), + destroy: jest.fn(), + goToAndStop: jest.fn(), + }; + }, +})); + function renderComponent(store: any) { render( @@ -69,20 +80,16 @@ describe("Checklist", () => { const wrapper = screen.getAllByTestId("checklist-wrapper"); expect(wrapper.length).toBe(1); const completionInfo = screen.getAllByTestId("checklist-completion-info"); - expect(completionInfo[0].innerHTML).toBe("0 of 5"); - const datasourceButton = screen.getAllByTestId( - "checklist-datasource-button", - ); + expect(completionInfo[0].innerHTML).toBe("0 of 5 "); + const datasourceButton = screen.getAllByTestId("checklist-datasource"); expect(datasourceButton.length).toBe(1); - const actionButton = screen.getAllByTestId("checklist-action-button"); + const actionButton = screen.getAllByTestId("checklist-action"); expect(actionButton.length).toBe(1); - const widgetButton = screen.getAllByTestId("checklist-widget-button"); + const widgetButton = screen.getAllByTestId("checklist-widget"); expect(widgetButton.length).toBe(1); - const connectionButton = screen.getAllByTestId( - "checklist-connection-button", - ); + const connectionButton = screen.getAllByTestId("checklist-connection"); expect(connectionButton.length).toBe(1); - const deployButton = screen.getAllByTestId("checklist-deploy-button"); + const deployButton = screen.getAllByTestId("checklist-deploy"); expect(deployButton.length).toBe(1); const banner = screen.queryAllByTestId("checklist-completion-banner"); expect(banner.length).toBe(0); @@ -97,11 +104,9 @@ describe("Checklist", () => { it("with `add a datasource` task checked off", () => { renderComponent(getStore(1)); - const datasourceButton = screen.queryAllByTestId( - "checklist-datasource-button", - ); - expect(datasourceButton.length).toBe(0); - const actionButton = screen.queryAllByTestId("checklist-action-button"); + const datasourceButton = screen.queryAllByTestId("checklist-datasource"); + expect(datasourceButton[0]).toHaveStyle("cursor: auto"); + const actionButton = screen.queryAllByTestId("checklist-action"); fireEvent.click(actionButton[0]); expect(history).toHaveBeenCalledWith( integrationEditorURL({ @@ -113,9 +118,9 @@ describe("Checklist", () => { it("with `add a query` task checked off", () => { renderComponent(getStore(2)); - const actionButton = screen.queryAllByTestId("checklist-action-button"); - expect(actionButton.length).toBe(0); - const widgetButton = screen.queryAllByTestId("checklist-widget-button"); + const actionButton = screen.queryAllByTestId("checklist-action"); + expect(actionButton[0]).toHaveStyle("cursor: auto"); + const widgetButton = screen.queryAllByTestId("checklist-widget"); fireEvent.click(widgetButton[0]); expect(history).toHaveBeenCalledWith( builderURL({ pageId: initialState.entities.pageList.currentPageId }), @@ -133,11 +138,9 @@ describe("Checklist", () => { it("with `add a widget` task checked off", () => { const store: any = getStore(3); renderComponent(store); - const widgetButton = screen.queryAllByTestId("checklist-widget-button"); - expect(widgetButton.length).toBe(0); - const connectionButton = screen.queryAllByTestId( - "checklist-connection-button", - ); + const widgetButton = screen.queryAllByTestId("checklist-widget"); + expect(widgetButton[0]).toHaveStyle("cursor: auto"); + const connectionButton = screen.queryAllByTestId("checklist-connection"); fireEvent.click(connectionButton[0]); expect(dispatch).toHaveBeenCalledWith( bindDataOnCanvas({ @@ -151,11 +154,9 @@ describe("Checklist", () => { it("with `connect your data` task checked off", () => { useIsWidgetActionConnectionPresent = true; renderComponent(getStore(4)); - const connectionButton = screen.queryAllByTestId( - "checklist-connection-button", - ); - expect(connectionButton.length).toBe(0); - const deployButton = screen.queryAllByTestId("checklist-deploy-button"); + const connectionButton = screen.queryAllByTestId("checklist-connection"); + expect(connectionButton[0]).toHaveStyle("cursor: auto"); + const deployButton = screen.queryAllByTestId("checklist-deploy"); fireEvent.click(deployButton[0]); expect(dispatch).toHaveBeenCalledWith({ type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.tsx index 64b5319c5c..59938109ca 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.tsx +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Checklist.tsx @@ -1,139 +1,162 @@ -import React from "react"; -import { Text, TextType } from "design-system-old"; -import { Button, Icon, Link } from "design-system"; +import React, { useEffect, useRef } from "react"; +import { Button, Divider, Text, Tooltip } from "design-system"; import styled from "styled-components"; import { useDispatch, useSelector } from "react-redux"; import { getCanvasWidgets, - getDatasources, getPageActions, + getSavedDatasources, } from "selectors/entitiesSelector"; import { useIsWidgetActionConnectionPresent } from "pages/Editor/utils"; import { getEvaluationInverseDependencyMap } from "selectors/dataTreeSelectors"; -import { APPLICATIONS_URL, INTEGRATION_TABS } from "constants/routes"; +import { INTEGRATION_TABS } from "constants/routes"; import { getApplicationLastDeployedAt, getCurrentApplicationId, getCurrentPageId, } from "selectors/editorSelectors"; import history from "utils/history"; -import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions"; +import { + setSignpostingOverlay, + showSignpostingModal, + showSignpostingTooltip, + signpostingMarkAllRead, + toggleInOnboardingWidgetSelection, +} from "actions/onboardingActions"; import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { getFirstTimeUserOnboardingComplete, - getIsFirstTimeUserOnboardingEnabled, + getSignpostingStepStateByStep, } from "selectors/onboardingSelectors"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { forceOpenWidgetPanel } from "actions/widgetSidebarActions"; import { bindDataOnCanvas } from "actions/pluginActionActions"; -import { Redirect } from "react-router"; import { ONBOARDING_CHECKLIST_ACTIONS, - ONBOARDING_CHECKLIST_BANNER_BODY, - ONBOARDING_CHECKLIST_BANNER_HEADER, ONBOARDING_CHECKLIST_HEADER, - ONBOARDING_CHECKLIST_BODY, - ONBOARDING_CHECKLIST_COMPLETE_TEXT, ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE, ONBOARDING_CHECKLIST_CREATE_A_QUERY, ONBOARDING_CHECKLIST_ADD_WIDGETS, ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET, ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS, - ONBOARDING_CHECKLIST_FOOTER, - ONBOARDING_CHECKLIST_BANNER_BUTTON, createMessage, + SIGNPOSTING_POPUP_SUBTITLE, + SIGNPOSTING_SUCCESS_POPUP, + SIGNPOSTING_TOOLTIP, } from "@appsmith/constants/messages"; import type { Datasource } from "entities/Datasource"; import type { ActionDataState } from "reducers/entityReducers/actionsReducer"; import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; -import { triggerWelcomeTour } from "./Utils"; +import { SIGNPOSTING_STEP } from "./Utils"; import { builderURL, integrationEditorURL } from "RouteBuilder"; -import { isAirgapped } from "@appsmith/utils/airgapHelpers"; import { DatasourceCreateEntryPoints } from "constants/Datasource"; +import classNames from "classnames"; +import lazyLottie from "utils/lazyLottie"; +import tickMarkAnimationURL from "assets/lottie/guided-tour-tick-mark.json.txt"; +import { getAppsmithConfigs } from "@appsmith/configs"; +const { intercomAppID } = getAppsmithConfigs(); -const Wrapper = styled.div` - padding: var(--ads-v2-spaces-7); - background: #fff; - height: calc(100vh - ${(props) => props.theme.smallHeaderHeight}); - overflow: auto; +const StyledDivider = styled(Divider)` + display: block; `; -const Pageheader = styled.h4` - font-size: ${(props) => props.theme.fontSizes[6]}px; +const PrefixCircle = styled.div<{ disabled: boolean }>` + height: 13px; + width: 13px; + border-radius: 50%; + border: 1px solid + ${(props) => + !props.disabled + ? "var(--ads-v2-color-bg-brand-secondary)" + : "var(--ads-v2-color-fg-subtle)"}; `; -const PageSubHeader = styled.p` - width: 100%; - margin-bottom: ${(props) => props.theme.spaces[12]}px; +const LottieAnimationContainer = styled.div` + height: 36px; + width: 36px; + left: -12px; + top: -13px; + position: absolute; `; -const StatusWrapper = styled.p` - width: 100%; - margin-bottom: ${(props) => props.theme.spaces[12]}px; - & span { - font-weight: 700; - } +const LottieAnimationWrapper = styled.div` + height: 13px; + width: 13px; + position: relative; `; -const StyledList = styled.ul` - margin: 0; - padding: 0; - list-style-type: none; - overflow: auto; -`; - -const StyledListItem = styled.li` - width: 100%; - display: flex; - padding: var(--ads-v2-spaces-7) 0px; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid var(--ads-v2-color-border); - &:first-child { - border-top: 1px solid var(--ads-v2-color-border); - } -`; -const StyledListItemTextWrapper = styled.div` - display: flex; - align-items: center; - flex: 1; -`; -const CHECKLIST_WIDTH_OFFSET = 268; - -const ChecklistText = styled.div<{ active: boolean }>` - flex-basis: calc(100% - ${CHECKLIST_WIDTH_OFFSET}px); - & span { - font-weight: 700; - } -`; - -const StyledCompleteMarker = styled.div` - flex-basis: 40px; -`; - -const Banner = styled.div` +const ListItem = styled.div<{ disabled: boolean; completed: boolean }>` border-radius: var(--ads-v2-border-radius); - border: 1px solid var(--ads-v2-color-border); - padding: var(--ads-v2-spaces-5); - margin-top: var(--ads-v2-spaces-7); + position: relative; + cursor: ${(props) => { + if (props.disabled) { + return "not-allowed"; + } else if (props.completed) { + return "auto"; + } + + return "pointer;"; + }}; + + // Strikethrought animation + .signposting-strikethrough { + position: relative; + } + .signposting-strikethrough-static { + text-decoration: line-through; + } + .signposting-strikethrough:after { + content: " "; + position: absolute; + top: 50%; + left: 0; + width: 0; + height: 1px; + background: black; + animation-duration: 2s; + animation-fill-mode: forwards; + } + .signposting-strikethrough::after { + -webkit-animation-name: bounceInLeft; + animation-name: bounceInLeft; + } + .signposting-strikethrough-bold::after { + -webkit-animation-name: signposting-strikethrough-bold; + animation-name: signposting-strikethrough-bold; + } + .signposting-strikethrough-normal::after { + -webkit-animation-name: signposting-strikethrough-normal; + animation-name: signposting-strikethrough-normal; + } + @keyframes signposting-strikethrough-bold { + 0% { + width: 0; + } + 50% { + width: 100%; + } + 100% { + width: 100%; + } + } + @keyframes signposting-strikethrough-normal { + 30% { + width: 0; + } + 100% { + width: 100%; + } + } `; -const BannerHeader = styled.h5` - font-size: 20px; - margin: 0; -`; - -const BannerText = styled.p` - margin: ${(props) => props.theme.spaces[3]}px 0px - ${(props) => props.theme.spaces[7]}px; -`; - -const StyledFooter = styled.div` - cursor: pointer; - display: flex; - align-items: center; - margin-top: ${(props) => props.theme.spaces[9]}px; - margin-bottom: ${(props) => props.theme.spaces[9]}px; +const Sibling = styled.div<{ disabled: boolean }>` + border-radius: var(--ads-v2-border-radius); + &:hover { + background-color: ${(props) => + !props.disabled ? "var(--ads-v2-color-bg-subtle)" : "transparent"}; + } + padding: var(--ads-v2-spaces-3); + padding-right: var(--ads-v2-spaces-2); `; function getSuggestedNextActionAndCompletedTasks( @@ -186,10 +209,147 @@ function getSuggestedNextActionAndCompletedTasks( return { suggestedNextAction, completedTasks }; } +function CheckListItem(props: { + boldText: string; + normalPrefixText?: string; + normalText: string; + onClick: () => void; + disabled: boolean; + completed: boolean; + step: SIGNPOSTING_STEP; + docLink?: string; + testid: string; +}) { + const stepState = useSelector((state) => + getSignpostingStepStateByStep(state, props.step), + ); + const tickMarkRef = useRef(null); + useEffect(() => { + if (props.completed) { + const anim = lazyLottie.loadAnimation({ + path: tickMarkAnimationURL, + container: tickMarkRef?.current as HTMLDivElement, + renderer: "svg", + loop: false, + autoplay: false, + }); + if (!stepState?.read) { + anim.play(); + } else { + // We want to show animation only for the first time. Once completed we show the last frame. + // Here 60 is the last frame + anim.goToAndStop(60, true); + } + + return () => { + anim.destroy(); + }; + } + }, [tickMarkRef?.current, props.completed, stepState?.read]); + + return ( +
+ null + : () => { + props.onClick(); + } + } + > + + {props.completed ? ( + + + + ) : ( + + )} +
+ + {props.boldText} + {props.normalPrefixText && ( + +  {props.normalPrefixText} + + )} + +
+ + {props.normalText} + +
+
+ +
+
+
+
+ +
+ ); +} + export default function OnboardingChecklist() { - const isAirgappedInstance = isAirgapped(); const dispatch = useDispatch(); - const datasources = useSelector(getDatasources); + const datasources = useSelector(getSavedDatasources); const pageId = useSelector(getCurrentPageId); const actions = useSelector(getPageActions(pageId)); const widgets = useSelector(getCanvasWidgets); @@ -199,26 +359,21 @@ export default function OnboardingChecklist() { actions, deps, ); - // const theme = useSelector(getCurrentThemeDetails); const applicationId = useSelector(getCurrentApplicationId); const isDeployed = !!useSelector(getApplicationLastDeployedAt); - const isCompleted = useSelector(getFirstTimeUserOnboardingComplete); - const isFirstTimeUserOnboardingEnabled = useSelector( - getIsFirstTimeUserOnboardingEnabled, + const { completedTasks } = getSuggestedNextActionAndCompletedTasks( + datasources, + actions, + widgets, + isConnectionPresent, + isDeployed, + ); + const isFirstTimeUserOnboardingComplete = useSelector( + getFirstTimeUserOnboardingComplete, ); - if (!isFirstTimeUserOnboardingEnabled && !isCompleted) { - return ; - } - const { completedTasks, suggestedNextAction } = - getSuggestedNextActionAndCompletedTasks( - datasources, - actions, - widgets, - isConnectionPresent, - isDeployed, - ); const onconnectYourWidget = () => { const action = actions[0]; + dispatch(showSignpostingModal(false)); if (action && applicationId && pageId) { dispatch( bindDataOnCanvas({ @@ -230,311 +385,219 @@ export default function OnboardingChecklist() { } else { history.push(builderURL({ pageId })); } - AnalyticsUtil.logEvent("SIGNPOSTING_CONNECT_WIDGET_CLICK"); + AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_CONNECT_WIDGET_CLICK"); }; - return ( - - history.push(builderURL({ pageId }))} - startIcon="back-control" - > - Back - - {isCompleted && ( - - - {createMessage(ONBOARDING_CHECKLIST_BANNER_HEADER)} - - - {createMessage(ONBOARDING_CHECKLIST_BANNER_BODY)} - - - - )} - - {createMessage(ONBOARDING_CHECKLIST_HEADER)} - - {createMessage(ONBOARDING_CHECKLIST_BODY)} - - { + if (intercomAppID && window.Intercom) { + // Close signposting modal when intercom modal is open + window.Intercom("onShow", () => { + dispatch(showSignpostingModal(false)); + }); + } + + return () => { + dispatch(signpostingMarkAllRead()); + dispatch(setSignpostingOverlay(false)); + dispatch(showSignpostingTooltip(false)); + }; + }, []); + + // End signposting for the application once signposting is complete and the + // signposting complete menu is closed + useEffect(() => { + return () => { + if (isFirstTimeUserOnboardingComplete) { + dispatch({ + type: ReduxActionTypes.END_FIRST_TIME_USER_ONBOARDING, + }); + } + }; + }, [isFirstTimeUserOnboardingComplete]); + + // Success UI + if (isFirstTimeUserOnboardingComplete) { + return ( + <> +
- {completedTasks} of 5 - -  {createMessage(ONBOARDING_CHECKLIST_COMPLETE_TEXT)} - - - - - - - - - - {createMessage(ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.bold)} - -   - {createMessage(ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.normal)} - - - {!datasources.length && !actions.length && ( - - )} - - - - - - - - - {createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.bold)} - -  {createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.normal)} - - - {!actions.length && ( - - )} - - - - - 1 - ? "var(--ads-v2-color-fg-success)" - : "" - } - data-testid="checklist-widget-complete-icon" - name="oval-check" - size="lg" - /> - - 1}> - - {createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.bold)} - -  {createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.normal)} - - - {Object.keys(widgets).length === 1 && ( - - )} - - - - - - - - - {createMessage( - ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.bold, - )} - -   - {createMessage( - ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.normal, - )} - - - {!isConnectionPresent && ( - - )} - - - - - - - - - {createMessage(ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.bold)} - -   - {createMessage(ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.normal)} - - - {!isDeployed && ( - - )} - - - {!isAirgappedInstance && ( - triggerWelcomeTour(dispatch, applicationId)} - > - - - - - {createMessage(ONBOARDING_CHECKLIST_FOOTER)} + + {createMessage(SIGNPOSTING_SUCCESS_POPUP.title)} - - - )} - +
+ + {createMessage(SIGNPOSTING_SUCCESS_POPUP.subtitle)} + + + + ); + } + + return ( + <> +
+
+ + {createMessage(ONBOARDING_CHECKLIST_HEADER)} + +
+ + {createMessage(SIGNPOSTING_POPUP_SUBTITLE)} + +
+ + {completedTasks} of 5{" "} + + complete +
+ +
+
+ { + AnalyticsUtil.logEvent( + "SIGNPOSTING_MODAL_CREATE_DATASOURCE_CLICK", + { + from: "CHECKLIST", + }, + ); + dispatch(showSignpostingModal(false)); + history.push( + integrationEditorURL({ + pageId, + selectedTab: INTEGRATION_TABS.NEW, + }), + ); + }} + step={SIGNPOSTING_STEP.CONNECT_A_DATASOURCE} + testid={"checklist-datasource"} + /> + { + AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_CREATE_QUERY_CLICK", { + from: "CHECKLIST", + }); + dispatch(showSignpostingModal(false)); + history.push( + integrationEditorURL({ + pageId, + selectedTab: INTEGRATION_TABS.ACTIVE, + }), + ); + // Event for datasource creation click + const entryPoint = DatasourceCreateEntryPoints.NEW_APP_CHECKLIST; + AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", { + entryPoint, + }); + }} + step={SIGNPOSTING_STEP.CREATE_A_QUERY} + testid={"checklist-action"} + /> + 1} + disabled={false} + docLink="https://docs.appsmith.com/reference/widgets" + normalText={createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.normal)} + onClick={() => { + AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_ADD_WIDGET_CLICK", { + from: "CHECKLIST", + }); + dispatch(showSignpostingModal(false)); + dispatch(toggleInOnboardingWidgetSelection(true)); + dispatch(forceOpenWidgetPanel(true)); + history.push(builderURL({ pageId })); + }} + step={SIGNPOSTING_STEP.ADD_WIDGETS} + testid={"checklist-widget"} + /> + + { + AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_PUBLISH_CLICK", { + from: "CHECKLIST", + }); + dispatch(showSignpostingModal(false)); + dispatch({ + type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, + payload: { + applicationId, + }, + }); + }} + step={SIGNPOSTING_STEP.DEPLOY_APPLICATIONS} + testid={"checklist-deploy"} + /> +
+ ); } diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/HelpMenu.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/HelpMenu.tsx new file mode 100644 index 0000000000..cf77d597a4 --- /dev/null +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/HelpMenu.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import { Text, Button } from "design-system"; +import { getAppsmithConfigs } from "@appsmith/configs"; +import { + APPSMITH_DISPLAY_VERSION, + createMessage, +} from "@appsmith/constants/messages"; +import moment from "moment"; +import styled from "styled-components"; +import { triggerWelcomeTour } from "./Utils"; +import { useDispatch, useSelector } from "react-redux"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { IntercomConsent } from "../HelpButton"; +import classNames from "classnames"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +const { appVersion, cloudHosting, intercomAppID } = getAppsmithConfigs(); + +type HelpItem = { + label: string; + link?: string; + id?: string; + icon: string; +}; +const HELP_MENU_ITEMS: HelpItem[] = [ + { + icon: "book-line", + label: "Documentation", + link: "https://docs.appsmith.com/", + }, + { + icon: "bug-line", + label: "Report a bug", + link: "https://github.com/appsmithorg/appsmith/issues/new/choose", + }, +]; + +if (intercomAppID && window.Intercom) { + HELP_MENU_ITEMS.push({ + icon: "chat-help", + label: "Chat with us", + id: "intercom-trigger", + }); +} + +const StyledText = styled(Text)` + font-size: 8px; + font-weight: normal; +`; + +const HelpFooter = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +function HelpMenu(props: { + setShowIntercomConsent: (val: boolean) => void; + showIntercomConsent: boolean; +}) { + const dispatch = useDispatch(); + const user = useSelector(getCurrentUser); + + return ( +
+ {props.showIntercomConsent ? ( + + ) : ( + <> + + Help & Resources + +
+ + {HELP_MENU_ITEMS.map((item) => { + return ( + + ); + })} +
+ + )} + {appVersion.id && ( + + + {createMessage( + APPSMITH_DISPLAY_VERSION, + appVersion.edition, + appVersion.id, + cloudHosting, + )} + + + Released {moment(appVersion.releaseDate).fromNow()} + + + )} +
+ ); +} + +export default HelpMenu; diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/IntroductionModal.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/IntroductionModal.tsx deleted file mode 100644 index dfa5c51b2c..0000000000 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/IntroductionModal.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { - Button, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from "design-system"; -import { - HOW_APPSMITH_WORKS, - BUILD_MY_FIRST_APP, - createMessage, - WELCOME_TO_APPSMITH, - ONBOARDING_INTRO_CONNECT_YOUR_DATABASE, - QUERY_YOUR_DATABASE, - DRAG_AND_DROP, - CUSTOMIZE_WIDGET_STYLING, - ONBOARDING_INTRO_PUBLISH, - CHOOSE_ACCESS_CONTROL_ROLES, - ONBOARDING_INTRO_FOOTER, - START_TUTORIAL, -} from "@appsmith/constants/messages"; -import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; -import React from "react"; -import { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import styled from "styled-components"; -import AnalyticsUtil from "utils/AnalyticsUtil"; -import { triggerWelcomeTour } from "./Utils"; -import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; -import { getCurrentApplicationId } from "selectors/editorSelectors"; -import { getAssetUrl, isAirgapped } from "@appsmith/utils/airgapHelpers"; - -const ModalSubHeader = styled.h5` - font-size: 14px; -`; - -const ModalContentWrapper = styled.div``; -const ModalContentRow = styled.div<{ border?: boolean }>` - flex-direction: row; - display: flex; - justify-content: space-between; - height: 113px; - padding: 20px 0px; - ${(props) => - props.border ? "border-bottom: 1px solid var(--ads-v2-color-border);" : ""} -`; -const ModalContentTextWrapper = styled.div` - display: flex; - align-items: center; - flex: 3; -`; - -const StyledImgWrapper = styled.div` - display: flex; - flex: 1; - justify-content: center; -`; - -const StyledImg = styled.img` - vertical-align: middle; -`; - -const StyledCount = styled.h5` - font-size: 16px; - font-weight: 500; - color: var(--ads-v2-color-fg-emphasis); - width: 32px; - height: 32px; - border-radius: 50%; - background-color: var(--ads-v2-color-bg-muted); - display: flex; - align-items: center; - justify-content: center; -`; - -const ModalContentItem = styled.div` - margin-left: 36px; -`; -const ModalContentHeader = styled.h5` - font-size: 16px; - font-weight: 500; -`; -const ModalContentDescription = styled.h5` - font-size: 14px; -`; - -const ModalFooterText = styled.span` - font-size: 14px; - letter-spacing: -0.24px; -`; - -type IntroductionModalProps = { - close: () => void; -}; - -const getConnectDataImg = () => `${ASSETS_CDN_URL}/ConnectData-v2.svg`; -const getDragAndDropImg = () => `${ASSETS_CDN_URL}/DragAndDrop.svg`; -const getPublishAppsImg = () => `${ASSETS_CDN_URL}/PublishApps-v2.svg`; - -export default function IntroductionModal({ close }: IntroductionModalProps) { - const modalAlwaysOpen = true; - const dispatch = useDispatch(); - const applicationId = useSelector(getCurrentApplicationId); - const isAirgappedInstance = isAirgapped(); - const onBuildApp = () => { - AnalyticsUtil.logEvent("SIGNPOSTING_BUILD_APP_CLICK"); - close(); - }; - - useEffect(() => { - dispatch({ - type: ReduxActionTypes.GET_ALL_APPLICATION_INIT, - }); - }, []); - - const closeModal = (isOpen: boolean) => { - if (!isOpen) { - onBuildApp(); - } - }; - - return ( - - e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - style={{ width: "920px" }} - > - - {createMessage(WELCOME_TO_APPSMITH)} - - - {createMessage(HOW_APPSMITH_WORKS)} - - - -
- 1 -
- - - {createMessage(ONBOARDING_INTRO_CONNECT_YOUR_DATABASE)} - - - {createMessage(QUERY_YOUR_DATABASE)} - - -
- - - -
- - -
- 2 -
- - - {createMessage(DRAG_AND_DROP)} - - - {createMessage(CUSTOMIZE_WIDGET_STYLING)} - - -
- - - -
- - -
- 3 -
- - - {createMessage(ONBOARDING_INTRO_PUBLISH)} - - - {createMessage(CHOOSE_ACCESS_CONTROL_ROLES)} - - -
- - - -
-
- - {createMessage(ONBOARDING_INTRO_FOOTER)} - -
- - {!isAirgappedInstance && ( - - )} - - -
-
- ); -} diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Modal.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Modal.tsx new file mode 100644 index 0000000000..0ec2858603 --- /dev/null +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Modal.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { MenuContent } from "design-system"; +import styled from "styled-components"; +import Checklist from "./Checklist"; +import HelpMenu from "./HelpMenu"; +import { useDispatch } from "react-redux"; +import { showSignpostingModal } from "actions/onboardingActions"; + +const SIGNPOSTING_POPUP_WIDTH = "360px"; + +const StyledMenuContent = styled(MenuContent)<{ animate: boolean }>` + max-width: ${SIGNPOSTING_POPUP_WIDTH}; + overflow: hidden; + display: flex; + animation-name: slideUpAndFade; + @keyframes slideUpAndFade { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } + } +`; +const Wrapper = styled.div` + padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-5); + display: flex; + flex-direction: column; +`; + +function OnboardingModal(props: { + setOverlay: boolean; + showIntercomConsent: boolean; + setShowIntercomConsent: (val: boolean) => void; +}) { + const dispatch = useDispatch(); + + return ( + { + dispatch(showSignpostingModal(false)); + }} + width={SIGNPOSTING_POPUP_WIDTH} + > + + {!props.showIntercomConsent && } + + + + ); +} + +export default OnboardingModal; diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Overlay.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Overlay.tsx new file mode 100644 index 0000000000..6f6ceced64 --- /dev/null +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Overlay.tsx @@ -0,0 +1,46 @@ +import { showSignpostingModal } from "actions/onboardingActions"; +import { Layers } from "constants/Layers"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + getIsFirstTimeUserOnboardingEnabled, + getSignpostingSetOverlay, +} from "selectors/onboardingSelectors"; +import styled from "styled-components"; + +const StyledOverlay = styled.div` + z-index: ${Layers.signpostingOverlay}; + opacity: 0.6; + background-color: transparent; + animation: fade-in 1s forwards; + will-change: background-color; + @keyframes fade-in { + from { + background-color: transparent; + } + to { + background-color: var(--ads-v2-color-bg-emphasis-max); + } + } +`; + +function Overlay() { + const signpostingEnabled = useSelector(getIsFirstTimeUserOnboardingEnabled); + const setOverlay = useSelector(getSignpostingSetOverlay); + const dispatch = useDispatch(); + + if (signpostingEnabled && setOverlay) { + return ( + { + dispatch(showSignpostingModal(false)); + }} + /> + ); + } + + return null; +} + +export default Overlay; diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.test.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.test.tsx index 3e7642e26b..c8351741e6 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.test.tsx +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.test.tsx @@ -1,20 +1,25 @@ +const dispatch = jest.fn(); + import React from "react"; import { Provider } from "react-redux"; -import { render, screen } from "test/testUtils"; +import { render } from "test/testUtils"; import OnboardingStatusbar from "./Statusbar"; import { getStore } from "./testUtils"; -import { - ONBOARDING_STATUS_STEPS_FIRST, - ONBOARDING_STATUS_STEPS_SECOND, - ONBOARDING_STATUS_STEPS_THIRD, - ONBOARDING_STATUS_STEPS_FOURTH, - ONBOARDING_STATUS_STEPS_FIVETH, - ONBOARDING_STATUS_STEPS_SIXTH, -} from "@appsmith/constants/messages"; import { useIsWidgetActionConnectionPresent } from "pages/Editor/utils"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import { SIGNPOSTING_STEP } from "./Utils"; +import { signpostingStepUpdateInit } from "actions/onboardingActions"; let container: any = null; +jest.mock("react-redux", () => { + const originalModule = jest.requireActual("react-redux"); + return { + ...originalModule, + useDispatch: () => dispatch, + }; +}); + function renderComponent(store: any) { render( @@ -30,52 +35,77 @@ describe("Statusbar", () => { document.body.appendChild(container); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it("is rendered", async () => { renderComponent(getStore(0)); - const statusbar = screen.queryAllByTestId("statusbar-container"); - expect(statusbar).toHaveLength(1); + expect(dispatch).toHaveBeenCalledTimes(5); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + payload: { + completed: false, + step: expect.any(String), + }, + type: ReduxActionTypes.SIGNPOSTING_STEP_UPDATE_INIT, + }), + ); }); - it("is pro", async () => { - renderComponent(getStore(0)); - const statusbar = screen.queryAllByTestId("statusbar-container"); - expect(statusbar).not.toBeNull(); - }); - - it("is showing first step", async () => { - renderComponent(getStore(0)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_FIRST()); - }); - - it("is showing second step", async () => { + it("on completing first step", async () => { renderComponent(getStore(1)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_SECOND()); + expect(dispatch).toHaveBeenNthCalledWith( + 1, + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CONNECT_A_DATASOURCE, + completed: true, + }), + ); }); - it("is showing third step", async () => { + it("on completing second step", async () => { renderComponent(getStore(2)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_THIRD()); + expect(dispatch).toHaveBeenNthCalledWith( + 2, + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CREATE_A_QUERY, + completed: true, + }), + ); }); - it("is showing fourth step", async () => { + it("on completing third step", async () => { renderComponent(getStore(3)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_FOURTH()); + expect(dispatch).toHaveBeenNthCalledWith( + 3, + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.ADD_WIDGETS, + completed: true, + }), + ); }); - it("is showing fifth step", async () => { + it("on completing fourth step", async () => { renderComponent(getStore(4)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_FIVETH()); + expect(dispatch).toHaveBeenNthCalledWith( + 4, + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CONNECT_DATA_TO_WIDGET, + completed: true, + }), + ); }); - it("is showing sixth step", async () => { + it("on completing fifth step", async () => { renderComponent(getStore(5)); - const statusbarText = screen.queryAllByTestId("statusbar-text"); - expect(statusbarText[0].innerHTML).toBe(ONBOARDING_STATUS_STEPS_SIXTH()); + expect(dispatch).toHaveBeenNthCalledWith( + 5, + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.DEPLOY_APPLICATIONS, + completed: true, + }), + ); }); it("should test useIsWidgetActionConnectionPresent function", () => { diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.tsx index 9ff66b1ca5..b38aee91d4 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.tsx +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Statusbar.tsx @@ -1,10 +1,6 @@ -import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { useIsWidgetActionConnectionPresent } from "pages/Editor/utils"; -import type { SyntheticEvent } from "react"; -import React from "react"; +import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -import type { RouteComponentProps } from "react-router-dom"; -import { withRouter } from "react-router-dom"; import { getEvaluationInverseDependencyMap } from "selectors/dataTreeSelectors"; import { getApplicationLastDeployedAt, @@ -12,118 +8,16 @@ import { } from "selectors/editorSelectors"; import { getCanvasWidgets, - getDatasources, getPageActions, + getSavedDatasources, } from "selectors/entitiesSelector"; -import { - getFirstTimeUserOnboardingComplete, - getInOnboardingWidgetSelection, -} from "selectors/onboardingSelectors"; -import styled from "styled-components"; -import history from "utils/history"; -import { - ONBOARDING_STATUS_STEPS_FIRST, - ONBOARDING_STATUS_STEPS_FIRST_ALT, - ONBOARDING_STATUS_STEPS_SECOND, - ONBOARDING_STATUS_STEPS_THIRD, - ONBOARDING_STATUS_STEPS_FOURTH, - ONBOARDING_STATUS_STEPS_FIVETH, - ONBOARDING_STATUS_STEPS_SIXTH, - ONBOARDING_STATUS_GET_STARTED, - createMessage, - ONBOARDING_STATUS_STEPS_THIRD_ALT, -} from "@appsmith/constants/messages"; -import { getTypographyByKey } from "design-system-old"; -import { onboardingCheckListUrl } from "RouteBuilder"; -import { Icon, Button } from "design-system"; +import { SIGNPOSTING_STEP } from "./Utils"; +import { getFirstTimeUserOnboardingComplete } from "selectors/onboardingSelectors"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import { signpostingStepUpdateInit } from "actions/onboardingActions"; -const Wrapper = styled.div<{ active: boolean }>` - width: 100%; - background-color: ${(props) => - props.active ? "var(--ads-v2-color-bg-brand)" : ""}; - cursor: ${(props) => (props.active ? "default" : "pointer")}; - height: ${(props) => props.theme.onboarding.statusBarHeight}px; - padding: 12px 16px; - transition: background-color 0.3s ease; - border-bottom: 1px solid var(--ads-v2-color-border); - - ${(props) => - props.active && - ` - p { - color: var(--ads-v2-color-fg-on-brand); - } - svg, svg path { - fill: var(--ads-v2-color-fg-on-brand) !important; - } - `} - - &:hover .hover-icons { - opacity: 1; - } -`; - -const TitleWrapper = styled.p` - ${getTypographyByKey("p4")} - color: var(--ads-v2-color-fg); -`; - -const StatusText = styled.p` - font-size: 13px; - display: flex; - & .hover-icons { - transform: translate(3px, 0px); - opacity: 0; - } -`; - -const ProgressContainer = styled.div` - background-color: ${(props) => - props.active - ? "var(--ads-v2-color-bg-brand-emphasis-plus)" - : "var(--ads-v2-color-bg-subtle)"}; - border-radius: var(--ads-v2-border-radius); - overflow: hidden; - margin-top: 12px; -`; - -const Progressbar = styled.div` - width: ${(props) => props.percentage}%; - height: 6px; - background: ${(props) => - props.active - ? "var(--ads-v2-color-bg)" - : "var(--ads-v2-color-bg-brand-emphasis-plus)"}; - transition: width 0.3s ease, background 0.3s ease; - border-radius: var(--ads-v2-border-radius); -`; - -const StyledClose = styled(Button)` - position: absolute !important; - top: 9px; - right: 9px; - opacity: 0; - cursor: pointer; -`; - -type StatusProgressbarType = { - percentage: number; - active: boolean; -}; -type StatusProgressbarContainerType = { - active: boolean; -}; - -export function StatusProgressbar(props: StatusProgressbarType) { - return ( - - - - ); -} - -const useStatus = (): { percentage: number; content: string } => { - const datasources = useSelector(getDatasources); +const useStatusListener = () => { + const datasources = useSelector(getSavedDatasources); const pageId = useSelector(getCurrentPageId); const actions = useSelector(getPageActions(pageId)); const widgets = useSelector(getCanvasWidgets); @@ -137,38 +31,9 @@ const useStatus = (): { percentage: number; content: string } => { const isFirstTimeUserOnboardingComplete = useSelector( getFirstTimeUserOnboardingComplete, ); - const inOnboardingWidgetSelection = - useSelector(getInOnboardingWidgetSelection) && - Object.keys(widgets).length === 1; + const dispatch = useDispatch(); - if (isFirstTimeUserOnboardingComplete) { - return { - percentage: 100, - content: createMessage(ONBOARDING_STATUS_STEPS_SIXTH), - }; - } - - let content = ""; let percentage = 0; - if (!datasources.length && !actions.length && !inOnboardingWidgetSelection) { - content = - Object.keys(widgets).length === 1 - ? createMessage(ONBOARDING_STATUS_STEPS_FIRST) - : createMessage(ONBOARDING_STATUS_STEPS_FIRST_ALT); - } else if (!actions.length && !inOnboardingWidgetSelection) { - content = createMessage(ONBOARDING_STATUS_STEPS_SECOND); - } else if (Object.keys(widgets).length === 1) { - content = - !datasources.length && !actions.length - ? createMessage(ONBOARDING_STATUS_STEPS_THIRD_ALT) - : createMessage(ONBOARDING_STATUS_STEPS_THIRD); - } else if (!isConnectionPresent) { - content = createMessage(ONBOARDING_STATUS_STEPS_FOURTH); - } else if (!isDeployed) { - content = createMessage(ONBOARDING_STATUS_STEPS_FIVETH); - } else { - content = createMessage(ONBOARDING_STATUS_STEPS_SIXTH); - } if (datasources.length || actions.length) { percentage += 20; @@ -190,72 +55,65 @@ const useStatus = (): { percentage: number; content: string } => { percentage += 20; } - return { - percentage, - content, - }; + useEffect(() => { + dispatch( + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CONNECT_A_DATASOURCE, + completed: !!(datasources.length || actions.length), + }), + ); + }, [datasources.length, actions.length]); + + useEffect(() => { + dispatch( + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CREATE_A_QUERY, + completed: !!actions.length, + }), + ); + }, [actions.length]); + + useEffect(() => { + dispatch( + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.ADD_WIDGETS, + completed: Object.keys(widgets).length > 1, + }), + ); + }, [Object.keys(widgets).length]); + + useEffect(() => { + dispatch( + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.CONNECT_DATA_TO_WIDGET, + completed: isConnectionPresent, + }), + ); + }, [isConnectionPresent]); + + useEffect(() => { + dispatch( + signpostingStepUpdateInit({ + step: SIGNPOSTING_STEP.DEPLOY_APPLICATIONS, + completed: isDeployed, + }), + ); + }, [isDeployed]); + + useEffect(() => { + if (percentage === 100 && !isFirstTimeUserOnboardingComplete) { + dispatch({ + type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_COMPLETE, + payload: true, + }); + } + }, [percentage, isFirstTimeUserOnboardingComplete]); }; -export function OnboardingStatusbar(props: RouteComponentProps) { - const dispatch = useDispatch(); - const pageId = useSelector(getCurrentPageId); - const { content, percentage } = useStatus(); - const isChecklistPage = props.location.pathname.indexOf("/checklist") > -1; - const isGenerateAppPage = - props.location.pathname.indexOf("/generate-page/form") > -1; - const isFirstTimeUserOnboardingComplete = useSelector( - getFirstTimeUserOnboardingComplete, - ); - if (isGenerateAppPage) { - return null; - } - const endFirstTimeUserOnboarding = (event?: SyntheticEvent) => { - event?.stopPropagation(); - dispatch({ - type: ReduxActionTypes.END_FIRST_TIME_USER_ONBOARDING, - }); - }; - if (percentage === 100 && !isFirstTimeUserOnboardingComplete) { - dispatch({ - type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_COMPLETE, - payload: true, - }); - } +export function OnboardingStatusbar() { + useStatusListener(); - return ( - { - history.push(onboardingCheckListUrl({ pageId })); - }} - > - - - {createMessage(ONBOARDING_STATUS_GET_STARTED)} - - - {content}   - {!isChecklistPage && ( - - )} - - - - ); + return null; } -export default withRouter(OnboardingStatusbar); +export default OnboardingStatusbar; diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.test.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.test.tsx deleted file mode 100644 index d2346835c7..0000000000 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -const dispatch = jest.fn(); -const history = jest.fn(); - -import { integrationEditorURL } from "RouteBuilder"; -import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; -import { INTEGRATION_TABS } from "constants/routes"; -import React from "react"; -import { Provider } from "react-redux"; -import { fireEvent, render, screen } from "test/testUtils"; -import OnboardingTasks from "./Tasks"; -import { getStore, initialState } from "./testUtils"; -import urlBuilder from "entities/URLRedirect/URLAssembly"; - -jest.mock("react-redux", () => { - const originalModule = jest.requireActual("react-redux"); - return { - ...originalModule, - useDispatch: () => dispatch, - }; -}); - -jest.mock("utils/history", () => { - return { - push: history, - listen: jest.fn(), - }; -}); - -let container: any; - -function renderComponent(store: any) { - render( - - - , - container, - ); -} - -describe("Tasks", () => { - beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); - urlBuilder.updateURLParams( - { - applicationSlug: initialState.ui.applications.currentApplication.slug, - applicationId: initialState.entities.pageList.applicationId, - applicationVersion: 2, - }, - [ - { - pageSlug: initialState.entities.pageList.pages[0].slug, - pageId: initialState.entities.pageList.currentPageId, - }, - ], - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("is rendered", async () => { - renderComponent(getStore(0)); - const wrapper = await screen.findAllByTestId("onboarding-tasks-wrapper"); - expect(wrapper.length).not.toBe(0); - }); - - it("is showing `Add a datasource` task", async () => { - const store = getStore(0); - renderComponent(store); - const text = await screen.findAllByTestId( - "onboarding-tasks-datasource-text", - ); - expect(text.length).toBe(1); - const button = await screen.findAllByTestId( - "onboarding-tasks-datasource-button", - ); - expect(button.length).toBe(1); - fireEvent.click(button[0]); - expect(history).toHaveBeenCalledWith( - integrationEditorURL({ - pageId: initialState.entities.pageList.currentPageId, - selectedTab: INTEGRATION_TABS.NEW, - }), - ); - const alt = await screen.findAllByTestId("onboarding-tasks-datasource-alt"); - expect(alt.length).toBe(1); - fireEvent.click(alt[0]); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.TOGGLE_ONBOARDING_WIDGET_SELECTION, - payload: true, - }); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.SET_FORCE_WIDGET_PANEL_OPEN, - payload: true, - }); - }); - - it("is showing `Add a Query` task", async () => { - const store = getStore(1); - renderComponent(store); - const text = await screen.findAllByTestId("onboarding-tasks-action-text"); - expect(text.length).toBe(1); - const button = await screen.findAllByTestId( - "onboarding-tasks-action-button", - ); - expect(button.length).toBe(1); - fireEvent.click(button[0]); - expect(history).toHaveBeenCalledWith( - integrationEditorURL({ - pageId: initialState.entities.pageList.currentPageId, - selectedTab: INTEGRATION_TABS.ACTIVE, - }), - ); - const alt = await screen.findAllByTestId("onboarding-tasks-action-alt"); - expect(alt.length).toBe(1); - fireEvent.click(alt[0]); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.TOGGLE_ONBOARDING_WIDGET_SELECTION, - payload: true, - }); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.SET_FORCE_WIDGET_PANEL_OPEN, - payload: true, - }); - }); - - it("is showing `Add a widget` task", async () => { - const store = getStore(2); - renderComponent(store); - const text = await screen.findAllByTestId("onboarding-tasks-widget-text"); - expect(text.length).toBe(1); - const button = await screen.findAllByTestId( - "onboarding-tasks-widget-button", - ); - expect(button.length).toBe(1); - fireEvent.click(button[0]); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.TOGGLE_ONBOARDING_WIDGET_SELECTION, - payload: true, - }); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.SET_FORCE_WIDGET_PANEL_OPEN, - payload: true, - }); - const alt = await screen.findAllByTestId("onboarding-tasks-widget-alt"); - expect(alt.length).toBe(1); - fireEvent.click(alt[0]); - expect(dispatch).toHaveBeenCalledWith({ - type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, - payload: { - applicationId: initialState.entities.pageList.applicationId, - }, - }); - }); -}); diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.tsx deleted file mode 100644 index f615e03735..0000000000 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Tasks.tsx +++ /dev/null @@ -1,344 +0,0 @@ -import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions"; -import { forceOpenWidgetPanel } from "actions/widgetSidebarActions"; -import { Button, Link } from "design-system"; -import { - ONBOARDING_TASK_DATASOURCE_BODY, - ONBOARDING_TASK_DATASOURCE_HEADER, - ONBOARDING_TASK_DATASOURCE_BUTTON, - ONBOARDING_TASK_DATASOURCE_FOOTER_ACTION, - ONBOARDING_TASK_DATASOURCE_FOOTER, - ONBOARDING_TASK_QUERY_HEADER, - ONBOARDING_TASK_QUERY_BODY, - ONBOARDING_TASK_QUERY_BUTTON, - ONBOARDING_TASK_QUERY_FOOTER_ACTION, - ONBOARDING_TASK_WIDGET_HEADER, - ONBOARDING_TASK_WIDGET_BODY, - ONBOARDING_TASK_WIDGET_BUTTON, - ONBOARDING_TASK_WIDGET_FOOTER_ACTION, - ONBOARDING_TASK_FOOTER, - createMessage, -} from "@appsmith/constants/messages"; -import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; -import { INTEGRATION_TABS } from "constants/routes"; -import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; -import React, { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; - -import { - getCurrentApplicationId, - getCurrentPageId, -} from "selectors/editorSelectors"; -import { - getCanvasWidgets, - getDatasources, - getPageActions, -} from "selectors/entitiesSelector"; -import styled from "styled-components"; -import AnalyticsUtil from "utils/AnalyticsUtil"; -import history from "utils/history"; -import { integrationEditorURL } from "RouteBuilder"; -import { getAssetUrl, isAirgapped } from "@appsmith/utils/airgapHelpers"; -import AnonymousDataPopup from "./AnonymousDataPopup"; -import { - getFirstTimeUserOnboardingComplete, - getFirstTimeUserOnboardingModal, - getIsFirstTimeUserOnboardingEnabled, -} from "selectors/onboardingSelectors"; -import { getCurrentUser } from "selectors/usersSelectors"; -import { - getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown, - setFirstTimeUserOnboardingTelemetryCalloutVisibility, -} from "utils/storage"; -import { ANONYMOUS_DATA_POPOP_TIMEOUT } from "./constants"; -import { DatasourceCreateEntryPoints } from "constants/Datasource"; -import IntroductionModal from "./IntroductionModal"; - -const Wrapper = styled.div` - width: 100%; - display: flex; - align-items: center; - justify-content: center; - height: calc(100vh - 36px); - margin: 0 auto; - background-color: #fff; -`; - -const CenteredContainer = styled.div` - text-align: center; - width: 529px; -`; - -const TaskImageContainer = styled.div` - width: 180px; - margin: 0 auto; -`; - -const TaskImage = styled.img` - width: 100%; -`; - -const TaskHeader = styled.h5` - font-size: 20px; - margin-top: 16px; - margin-bottom: 16px; -`; - -const TaskSubText = styled.p` - width: 100%; -`; - -const TaskButtonWrapper = styled.div` - margin-top: 30px; -`; - -const Taskfootnote = styled.p` - margin-top: 30px; - display: flex; - justify-content: center; -`; - -const getOnboardingDatasourceImg = () => - `${ASSETS_CDN_URL}/onboarding-datasource.svg`; -const getOnboardingQueryImg = () => `${ASSETS_CDN_URL}/onboarding-query.svg`; -const getOnboardingWidgetImg = () => `${ASSETS_CDN_URL}/onboarding-widget.svg`; - -export default function OnboardingTasks() { - const [isAnonymousDataPopupOpen, setisAnonymousDataPopupOpen] = - useState(false); - - const isFirstTimeUserOnboardingEnabled = useSelector( - getIsFirstTimeUserOnboardingEnabled, - ); - const applicationId = useSelector(getCurrentApplicationId); - const pageId = useSelector(getCurrentPageId); - let content; - const datasources = useSelector(getDatasources); - const actions = useSelector(getPageActions(pageId)); - const widgets = useSelector(getCanvasWidgets); - const dispatch = useDispatch(); - const user = useSelector(getCurrentUser); - const isAdmin = user?.isSuperUser || false; - const isOnboardingCompleted = useSelector(getFirstTimeUserOnboardingComplete); - const showModal = useSelector(getFirstTimeUserOnboardingModal); - - const hideAnonymousDataPopup = () => { - setisAnonymousDataPopupOpen(false); - setFirstTimeUserOnboardingTelemetryCalloutVisibility(true); - }; - - const showShowAnonymousDataPopup = async () => { - const shouldPopupShow = - !isAirgapped() && - isFirstTimeUserOnboardingEnabled && - isAdmin && - !isOnboardingCompleted; - if (shouldPopupShow) { - const isAnonymousDataPopupAlreadyOpen = - await getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown(); - //true if the modal was already shown else show the modal and set to already shown, also hide the modal after 10 secs - if (isAnonymousDataPopupAlreadyOpen) { - setisAnonymousDataPopupOpen(false); - } else { - setisAnonymousDataPopupOpen(true); - setTimeout(() => { - hideAnonymousDataPopup(); - }, ANONYMOUS_DATA_POPOP_TIMEOUT); - await setFirstTimeUserOnboardingTelemetryCalloutVisibility(true); - } - } else { - setisAnonymousDataPopupOpen(shouldPopupShow); - } - }; - - useEffect(() => { - showShowAnonymousDataPopup(); - }, []); - - if (!datasources.length && !actions.length) { - content = ( - - - - - - {createMessage(ONBOARDING_TASK_DATASOURCE_HEADER)} - - - {createMessage(ONBOARDING_TASK_DATASOURCE_BODY)} - - - - - - {createMessage(ONBOARDING_TASK_FOOTER)}  - { - AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", { - from: "CANVAS", - }); - dispatch(toggleInOnboardingWidgetSelection(true)); - dispatch(forceOpenWidgetPanel(true)); - }} - > - {createMessage(ONBOARDING_TASK_DATASOURCE_FOOTER_ACTION)} - -  {createMessage(ONBOARDING_TASK_DATASOURCE_FOOTER)} - - - ); - } else if (!actions.length) { - content = ( - - - - - - {createMessage(ONBOARDING_TASK_QUERY_HEADER)} - - {createMessage(ONBOARDING_TASK_QUERY_BODY)} - - - - - {createMessage(ONBOARDING_TASK_FOOTER)}  - { - AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", { - from: "CANVAS", - }); - dispatch(toggleInOnboardingWidgetSelection(true)); - dispatch(forceOpenWidgetPanel(true)); - }} - > - {createMessage(ONBOARDING_TASK_QUERY_FOOTER_ACTION)} - - - - ); - } else if (Object.keys(widgets).length === 1) { - content = ( - - - - - - {createMessage(ONBOARDING_TASK_WIDGET_HEADER)} - - {createMessage(ONBOARDING_TASK_WIDGET_BODY)} - - - - - {createMessage(ONBOARDING_TASK_FOOTER)}  - { - AnalyticsUtil.logEvent("SIGNPOSTING_PUBLISH_CLICK", { - from: "CANVAS", - }); - dispatch({ - type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, - payload: { - applicationId, - }, - }); - }} - > - {createMessage(ONBOARDING_TASK_WIDGET_FOOTER_ACTION)} - - . - - - ); - } - return ( - - {content} - {isAnonymousDataPopupOpen && ( - - )} - {!isAdmin && showModal && ( - { - dispatch({ - type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL, - payload: false, - }); - }} - /> - )} - - ); -} diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/TooltipContent.tsx b/app/client/src/pages/Editor/FirstTimeUserOnboarding/TooltipContent.tsx new file mode 100644 index 0000000000..c16a1bd2b7 --- /dev/null +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/TooltipContent.tsx @@ -0,0 +1,131 @@ +import { + createMessage, + ONBOARDING_CHECKLIST_HEADER, + SIGNPOSTING_TOOLTIP, + SIGNPOSTING_LAST_STEP_TOOLTIP, + SIGNPOSTING_SUCCESS_POPUP, +} from "@appsmith/constants/messages"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { getEvaluationInverseDependencyMap } from "selectors/dataTreeSelectors"; +import { + getCurrentPageId, + getApplicationLastDeployedAt, +} from "selectors/editorSelectors"; +import { + getPageActions, + getCanvasWidgets, + getSavedDatasources, +} from "selectors/entitiesSelector"; +import { useIsWidgetActionConnectionPresent } from "../utils"; +import { showSignpostingTooltip } from "actions/onboardingActions"; +import { SIGNPOSTING_STEP } from "./Utils"; + +const SIGNPOSTING_STEPS = [ + SIGNPOSTING_STEP.CONNECT_A_DATASOURCE, + SIGNPOSTING_STEP.CREATE_A_QUERY, + SIGNPOSTING_STEP.ADD_WIDGETS, + SIGNPOSTING_STEP.CONNECT_DATA_TO_WIDGET, + SIGNPOSTING_STEP.DEPLOY_APPLICATIONS, +]; + +function TooltipContent(props: { showSignpostingTooltip: boolean }) { + const datasources = useSelector(getSavedDatasources); + const pageId = useSelector(getCurrentPageId); + const actions = useSelector(getPageActions(pageId)); + const widgets = useSelector(getCanvasWidgets); + const deps = useSelector(getEvaluationInverseDependencyMap); + const isConnectionPresent = useIsWidgetActionConnectionPresent( + widgets, + actions, + deps, + ); + const isDeployed = !!useSelector(getApplicationLastDeployedAt); + const dispatch = useDispatch(); + + let title = createMessage(ONBOARDING_CHECKLIST_HEADER); + let content = createMessage(SIGNPOSTING_TOOLTIP.DEFAULT.content); + const lastStepContent = createMessage(SIGNPOSTING_LAST_STEP_TOOLTIP); + let completedTasks = 0; + + useEffect(() => { + const handleClickOutside = () => { + dispatch(showSignpostingTooltip(false)); + }; + + document.addEventListener("click", handleClickOutside, true); + return () => { + document.removeEventListener("click", handleClickOutside, true); + }; + }, []); + + useEffect(() => { + if (!props.showSignpostingTooltip) return; + + const timer = setTimeout(() => { + // After a step is completed we want to show the tooltip for 8 seconds and then hide it. + dispatch(showSignpostingTooltip(false)); + }, 8000); + + return () => { + clearTimeout(timer); + }; + }, [props.showSignpostingTooltip]); + + if (datasources.length) { + completedTasks++; + } + if (actions.length) { + completedTasks++; + } + if (Object.keys(widgets).length > 1) { + completedTasks++; + } + if (isConnectionPresent) { + completedTasks++; + } + if (isDeployed) { + completedTasks++; + } + + if (completedTasks > 0) { + title = `🎉${completedTasks}/5 Steps completed`; + } + + if (!datasources.length && !actions.length) { + content = createMessage(SIGNPOSTING_TOOLTIP.CONNECT_A_DATASOURCE.content); + } else if (!actions.length && datasources.length) { + content = createMessage(SIGNPOSTING_TOOLTIP.CREATE_QUERY.content); + } else if (Object.keys(widgets).length === 1 && actions.length) { + content = createMessage(SIGNPOSTING_TOOLTIP.ADD_WIDGET.content); + } else if (!isConnectionPresent) { + content = createMessage(SIGNPOSTING_TOOLTIP.CONNECT_DATA_TO_WIDGET.content); + } else if (!isDeployed) { + content = createMessage(SIGNPOSTING_TOOLTIP.DEPLOY_APPLICATION.content); + } + + if (completedTasks === 0) { + title = createMessage(ONBOARDING_CHECKLIST_HEADER); + content = createMessage(SIGNPOSTING_TOOLTIP.DEFAULT.content); + } + + if (completedTasks === SIGNPOSTING_STEPS.length) + return <>{createMessage(SIGNPOSTING_SUCCESS_POPUP.title)}; + + return ( + <> + {title} +
+
+ {completedTasks === SIGNPOSTING_STEPS.length - 1 && ( + <> + {lastStepContent} +
+ + )} + {content} + + ); +} + +export default TooltipContent; diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Utils.ts b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Utils.ts index bf6bf3f960..b9723d75bb 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/Utils.ts +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/Utils.ts @@ -1,17 +1,18 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; -import { removeFirstTimeUserOnboardingApplicationId } from "actions/onboardingActions"; import { APPLICATIONS_URL } from "constants/routes"; import type { Dispatch } from "react"; -import AnalyticsUtil from "utils/AnalyticsUtil"; import history from "utils/history"; -export const triggerWelcomeTour = ( - dispatch: Dispatch, - applicationId: string, -) => { - AnalyticsUtil.logEvent("SIGNPOSTING_WELCOME_TOUR_CLICK"); +export const triggerWelcomeTour = (dispatch: Dispatch) => { history.push(APPLICATIONS_URL); - dispatch(removeFirstTimeUserOnboardingApplicationId(applicationId)); dispatch({ type: ReduxActionTypes.ONBOARDING_CREATE_APPLICATION, }); }; + +export enum SIGNPOSTING_STEP { + CONNECT_A_DATASOURCE = "CONNECT_A_DATASOURCE", + CREATE_A_QUERY = "CREATE_A_QUERY", + ADD_WIDGETS = "ADD_WIDGETS", + CONNECT_DATA_TO_WIDGET = "CONNECT_DATA_TO_WIDGET", + DEPLOY_APPLICATIONS = "DEPLOY_APPLICATIONS", +} diff --git a/app/client/src/pages/Editor/FirstTimeUserOnboarding/testUtils.ts b/app/client/src/pages/Editor/FirstTimeUserOnboarding/testUtils.ts index f0de37c5d9..7fb72ac448 100644 --- a/app/client/src/pages/Editor/FirstTimeUserOnboarding/testUtils.ts +++ b/app/client/src/pages/Editor/FirstTimeUserOnboarding/testUtils.ts @@ -38,6 +38,7 @@ export const initialState: any = { firstTimeUserOnboardingComplete: false, showFirstTimeUserOnboardingModal: true, firstTimeUserOnboardingApplicationIds: ["1"], + stepState: [], }, theme: { theme: { diff --git a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx index e09a5bd1e4..8d5b8f44e4 100644 --- a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx +++ b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx @@ -54,16 +54,10 @@ import { } from "../constants"; import { Bold, Label, SelectWrapper } from "./styles"; import type { GeneratePagePayload } from "./types"; -import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { getCurrentApplicationId } from "selectors/editorSelectors"; -import { - getFirstTimeUserOnboardingComplete, - getIsFirstTimeUserOnboardingEnabled, -} from "selectors/onboardingSelectors"; import { datasourcesEditorIdURL, integrationEditorURL } from "RouteBuilder"; import { PluginPackageName } from "entities/Action"; -import { removeFirstTimeUserOnboardingApplicationId } from "actions/onboardingActions"; import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors"; import { hasCreateDatasourcePermission } from "@appsmith/utils/permissionHelpers"; import { getPluginImages } from "selectors/entitiesSelector"; @@ -293,13 +287,6 @@ function GeneratePageForm() { const { bucketList, failedFetchingBucketList, isFetchingBucketList } = useS3BucketList(); - const isFirstTimeUserOnboardingEnabled = useSelector( - getIsFirstTimeUserOnboardingEnabled, - ); - const isFirstTimeUserOnboardingComplete = useSelector( - getFirstTimeUserOnboardingComplete, - ); - const onSelectDataSource = useCallback( ( datasource: string | undefined, @@ -568,15 +555,6 @@ function GeneratePageForm() { AnalyticsUtil.logEvent("GEN_CRUD_PAGE_FORM_SUBMIT"); dispatch(generateTemplateToUpdatePage(payload)); - if (isFirstTimeUserOnboardingEnabled) { - dispatch(removeFirstTimeUserOnboardingApplicationId(applicationId)); - } - if (isFirstTimeUserOnboardingComplete) { - dispatch({ - type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_COMPLETE, - payload: false, - }); - } }; const handleFormSubmit = () => { diff --git a/app/client/src/pages/Editor/HelpButton.tsx b/app/client/src/pages/Editor/HelpButton.tsx index f19401fd4a..2700500469 100644 --- a/app/client/src/pages/Editor/HelpButton.tsx +++ b/app/client/src/pages/Editor/HelpButton.tsx @@ -25,6 +25,19 @@ import { import { getAppsmithConfigs } from "@appsmith/configs"; import moment from "moment/moment"; import styled from "styled-components"; +import { + getFirstTimeUserOnboardingModal, + getIsFirstTimeUserOnboardingEnabled, + getSignpostingSetOverlay, + getSignpostingTooltipVisible, + getSignpostingUnreadSteps, + inGuidedTour, +} from "selectors/onboardingSelectors"; +import SignpostingPopup from "pages/Editor/FirstTimeUserOnboarding/Modal"; +import { showSignpostingModal } from "actions/onboardingActions"; +import { triggerWelcomeTour } from "./FirstTimeUserOnboarding/Utils"; +import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import TooltipContent from "./FirstTimeUserOnboarding/TooltipContent"; import { getInstanceId } from "@appsmith/selectors/tenantSelectors"; import { updateIntercomConsent, updateUserDetails } from "actions/userActions"; @@ -36,6 +49,15 @@ const HelpFooter = styled.div` justify-content: space-between; font-size: 8px; `; +const UnreadSteps = styled.div` + position: absolute; + height: 6px; + width: 6px; + border-radius: 50%; + top: 10px; + left: 22px; + background-color: var(--ads-v2-color-fg-error); +`; const ConsentContainer = styled.div` padding: 10px; `; @@ -45,6 +67,7 @@ const ActionsRow = styled.div` align-items: center; margin-bottom: 8px; `; + type HelpItem = { label: string; link?: string; @@ -54,7 +77,7 @@ type HelpItem = { const HELP_MENU_ITEMS: HelpItem[] = [ { - icon: "file-line", + icon: "book-line", label: "Documentation", link: "https://docs.appsmith.com/", }, @@ -63,11 +86,6 @@ const HELP_MENU_ITEMS: HelpItem[] = [ label: "Report a bug", link: "https://github.com/appsmithorg/appsmith/issues/new/choose", }, - { - icon: "discord", - label: "Join our discord", - link: "https://discord.gg/rBTTVJp", - }, ]; if (intercomAppID && window.Intercom) { @@ -78,7 +96,7 @@ if (intercomAppID && window.Intercom) { }); } -function IntercomConsent({ +export function IntercomConsent({ showIntercomConsent, }: { showIntercomConsent: (val: boolean) => void; @@ -121,9 +139,48 @@ function IntercomConsent({ ); } +function HelpButtonTooltip(props: { + isFirstTimeUserOnboardingEnabled: boolean; + showSignpostingTooltip: boolean; +}) { + if (props.isFirstTimeUserOnboardingEnabled) { + return ( + + ); + } + + return <>{createMessage(HELP_RESOURCE_TOOLTIP)}; +} + function HelpButton() { - const user = useSelector(getCurrentUser); const [showIntercomConsent, setShowIntercomConsent] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); + const user = useSelector(getCurrentUser); + const dispatch = useDispatch(); + const isFirstTimeUserOnboardingEnabled = useSelector( + getIsFirstTimeUserOnboardingEnabled, + ); + const guidedTourEnabled = useSelector(inGuidedTour); + const showSignpostingTooltip = useSelector(getSignpostingTooltipVisible); + const onboardingModalOpen = useSelector(getFirstTimeUserOnboardingModal); + const unreadSteps = useSelector(getSignpostingUnreadSteps); + const setOverlay = useSelector(getSignpostingSetOverlay); + const isAirgappedInstance = isAirgapped(); + const showUnreadSteps = + !!unreadSteps.length && + isFirstTimeUserOnboardingEnabled && + !onboardingModalOpen; + const menuProps = isFirstTimeUserOnboardingEnabled + ? { + open: onboardingModalOpen, + } + : {}; + const tooltipProps = isFirstTimeUserOnboardingEnabled + ? { + visible: showTooltip || showSignpostingTooltip, + onVisibleChange: setShowTooltip, + } + : {}; useEffect(() => { bootIntercom(user); @@ -133,16 +190,38 @@ function HelpButton() { { if (open) { + if (isFirstTimeUserOnboardingEnabled) { + dispatch(showSignpostingModal(true)); + setShowTooltip(false); + } setShowIntercomConsent(false); - AnalyticsUtil.logEvent("OPEN_HELP", { page: "Editor" }); + AnalyticsUtil.logEvent("OPEN_HELP", { + page: "Editor", + signpostingActive: isFirstTimeUserOnboardingEnabled, + }); } }} + {...menuProps} > -
+
+ } + destroyTooltipOnHide={isFirstTimeUserOnboardingEnabled} + isDisabled={onboardingModalOpen} + mouseLeaveDelay={0} placement="bottomRight" + {...tooltipProps} >
- - {showIntercomConsent ? ( - - ) : ( - HELP_MENU_ITEMS.map((item) => ( - { - if (item.link) { - window.open(item.link, "_blank"); - } - if (item.id === "intercom-trigger") { - e?.preventDefault(); - if (intercomAppID && window.Intercom) { - if (user?.isIntercomConsentGiven || cloudHosting) { - window.Intercom("show"); - } else { - setShowIntercomConsent(true); + {isFirstTimeUserOnboardingEnabled ? ( + + ) : ( + + {showIntercomConsent ? ( + + ) : ( + <> + {!isAirgappedInstance && !guidedTourEnabled && ( + <> + { + triggerWelcomeTour(dispatch); + AnalyticsUtil.logEvent("HELP_MENU_WELCOME_TOUR_CLICK"); + }} + startIcon="guide" + > + Try guided tour + + + + )} + {HELP_MENU_ITEMS.map((item) => ( + { + if (item.link) { + window.open(item.link, "_blank"); } - } - } - }} - startIcon={item.icon} - > - {item.label} - - )) - )} - {appVersion.id && ( - <> - - - - - {createMessage( - APPSMITH_DISPLAY_VERSION, - appVersion.edition, - appVersion.id, - cloudHosting, - )} - - Released {moment(appVersion.releaseDate).fromNow()} - - - - )} - + if (item.id === "intercom-trigger") { + e?.preventDefault(); + if (intercomAppID && window.Intercom) { + if (user?.isIntercomConsentGiven || cloudHosting) { + window.Intercom("show"); + } else { + setShowIntercomConsent(true); + } + } + } + }} + startIcon={item.icon} + > + {item.label} + + ))} + + )} + + {appVersion.id && ( + <> + + + + + {createMessage( + APPSMITH_DISPLAY_VERSION, + appVersion.edition, + appVersion.id, + cloudHosting, + )} + + + Released {moment(appVersion.releaseDate).fromNow()} + + + + + )} + + )}
); } diff --git a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx index 468741933b..f326aaca07 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx @@ -34,6 +34,7 @@ import { import Canvas from "../Canvas"; import { CanvasResizer } from "widgets/CanvasResizer"; import type { AppState } from "@appsmith/reducers"; +import { getIsAnonymousDataPopupVisible } from "selectors/onboardingSelectors"; type CanvasContainerProps = { isPreviewMode: boolean; @@ -117,6 +118,7 @@ function CanvasContainer(props: CanvasContainerProps) { pages.length > 1; const isAppThemeChanging = useSelector(getAppThemeIsChanging); const showCanvasTopSection = useSelector(showCanvasTopSectionSelector); + const showAnonymousDataPopup = useSelector(getIsAnonymousDataPopupVisible); const isLayoutingInitialized = useDynamicAppLayout(); const isPageInitializing = isFetchingPage || !isLayoutingInitialized; @@ -188,12 +190,15 @@ function CanvasContainer(props: CanvasContainerProps) { className={classNames({ [`${getCanvasClassName()} scrollbar-thin`]: true, "mt-0": shouldShowSnapShotBanner || !shouldHaveTopMargin, - "mt-4": !shouldShowSnapShotBanner && showCanvasTopSection, + "mt-4": + !shouldShowSnapShotBanner && + (showCanvasTopSection || showAnonymousDataPopup), "mt-8": !shouldShowSnapShotBanner && shouldHaveTopMargin && !showCanvasTopSection && - !isPreviewingNavigation, + !isPreviewingNavigation && + !showAnonymousDataPopup, "mt-24": shouldShowSnapShotBanner, })} id={"canvas-viewport"} diff --git a/app/client/src/pages/Editor/WidgetsEditor/index.tsx b/app/client/src/pages/Editor/WidgetsEditor/index.tsx index 77a21252e4..322a8053c2 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/index.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/index.tsx @@ -13,7 +13,6 @@ import AnalyticsUtil from "utils/AnalyticsUtil"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; -import OnboardingTasks from "../FirstTimeUserOnboarding/Tasks"; import CrudInfoModal from "../GeneratePage/components/CrudInfoModal"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { @@ -25,10 +24,7 @@ import { import { setCanvasSelectionFromEditor } from "actions/canvasSelectionActions"; import { closePropertyPane, closeTableFilterPane } from "actions/widgetActions"; import { useAllowEditorDragToSelect } from "utils/hooks/useAllowEditorDragToSelect"; -import { - getIsOnboardingTasksView, - inGuidedTour, -} from "selectors/onboardingSelectors"; +import { inGuidedTour } from "selectors/onboardingSelectors"; import EditorContextProvider from "components/editorComponents/EditorContextProvider"; import Guide from "../GuidedTour/Guide"; import CanvasContainer from "./CanvasContainer"; @@ -49,6 +45,7 @@ import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; import classNames from "classnames"; import { getSnapshotUpdatedTime } from "selectors/autoLayoutSelectors"; import { getReadableSnapShotDetails } from "utils/autoLayout/AutoLayoutUtils"; +import AnonymousDataPopup from "../FirstTimeUserOnboarding/AnonymousDataPopup"; function WidgetsEditor() { const { deselectAll, focusWidget } = useWidgetSelection(); @@ -56,7 +53,6 @@ function WidgetsEditor() { const currentPageId = useSelector(getCurrentPageId); const currentPageName = useSelector(getCurrentPageName); const currentApp = useSelector(getCurrentApplication); - const showOnboardingTasks = useSelector(getIsOnboardingTasksView); const guidedTourEnabled = useSelector(inGuidedTour); const isPreviewMode = useSelector(previewModeSelector); const lastUpdatedTime = useSelector(getSnapshotUpdatedTime); @@ -167,78 +163,69 @@ function WidgetsEditor() { PerformanceTracker.stopTracking(); return ( - {showOnboardingTasks ? ( - - ) : ( - <> - {guidedTourEnabled && } + {guidedTourEnabled && } +
+
+ {!isAppSettingsPaneWithNavigationTabOpen && } + +
+ {showNavigation()} -
-
- {!isAppSettingsPaneWithNavigationTabOpen && } + {shouldShowSnapShotBanner && ( +
+ +
+ )} + + -
- {showNavigation()} - - - {shouldShowSnapShotBanner && ( -
- -
- )} - -
- - -
- -
- +
- - )} + +
+ +
); } diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 25f2455b2c..0d2119f5bf 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -40,6 +40,7 @@ import { GIT_BRANCH_QUERY_KEY } from "constants/routes"; import TemplatesModal from "pages/Templates/TemplatesModal"; import ReconnectDatasourceModal from "./gitSync/ReconnectDatasourceModal"; import { Spinner } from "design-system"; +import SignpostingOverlay from "pages/Editor/FirstTimeUserOnboarding/Overlay"; type EditorProps = { currentApplicationId?: string; @@ -187,6 +188,7 @@ class Editor extends Component { +
diff --git a/app/client/src/reducers/uiReducers/onBoardingReducer.ts b/app/client/src/reducers/uiReducers/onBoardingReducer.ts index 66476e69fd..fb25bce30c 100644 --- a/app/client/src/reducers/uiReducers/onBoardingReducer.ts +++ b/app/client/src/reducers/uiReducers/onBoardingReducer.ts @@ -1,5 +1,6 @@ import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import type { SIGNPOSTING_STEP } from "pages/Editor/FirstTimeUserOnboarding/Utils"; import { createReducer } from "utils/ReducerUtils"; const initialState: OnboardingState = { @@ -9,6 +10,16 @@ const initialState: OnboardingState = { firstTimeUserOnboardingApplicationIds: [], firstTimeUserOnboardingComplete: false, showFirstTimeUserOnboardingModal: false, + setOverlay: false, + stepState: [], + showSignpostingTooltip: false, + showAnonymousDataPopup: false, +}; + +export type StepState = { + step: SIGNPOSTING_STEP; + completed: boolean; + read?: boolean; }; export interface OnboardingState { @@ -17,6 +28,10 @@ export interface OnboardingState { firstTimeUserOnboardingApplicationIds: string[]; firstTimeUserOnboardingComplete: boolean; showFirstTimeUserOnboardingModal: boolean; + stepState: StepState[]; + setOverlay: boolean; + showSignpostingTooltip: boolean; + showAnonymousDataPopup: boolean; } const onboardingReducer = createReducer(initialState, { @@ -62,6 +77,65 @@ const onboardingReducer = createReducer(initialState, { ) => { return { ...state, forceOpenWidgetPanel: action.payload }; }, + [ReduxActionTypes.SIGNPOSTING_STEP_UPDATE]: ( + state: OnboardingState, + action: ReduxAction, + ) => { + const index = state.stepState.findIndex( + (stepState) => stepState.step === action.payload.step, + ); + const newArray = [...state.stepState]; + if (index >= 0) { + newArray[index] = action.payload; + } else { + newArray.push(action.payload); + } + return { + ...state, + stepState: newArray, + }; + }, + [ReduxActionTypes.SIGNPOSTING_MARK_ALL_READ]: (state: OnboardingState) => { + return { + ...state, + stepState: state.stepState.map((step) => { + if (step.completed) { + return { + ...step, + read: true, + }; + } + return step; + }), + }; + }, + [ReduxActionTypes.SET_SIGNPOSTING_OVERLAY]: ( + state: OnboardingState, + action: ReduxAction, + ) => { + return { + ...state, + setOverlay: action.payload, + }; + }, + [ReduxActionTypes.SIGNPOSTING_SHOW_TOOLTIP]: ( + state: OnboardingState, + action: ReduxAction, + ) => { + return { + ...state, + showSignpostingTooltip: action.payload, + }; + }, + [ReduxActionTypes.SHOW_ANONYMOUS_DATA_POPUP]: ( + state: OnboardingState, + action: ReduxAction, + ) => { + return { + ...state, + showAnonymousDataPopup: action.payload, + }; + }, }); export default onboardingReducer; diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 72c544d49c..6ece6e89f1 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -38,6 +38,7 @@ import type { ApplicationPagePayload } from "@appsmith/api/ApplicationApi"; import { updateSlugNamesInURL } from "utils/helpers"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; import { safeCrashAppRequest } from "../actions/errorActions"; +import { resetSnipingMode } from "actions/propertyPaneActions"; export const URL_CHANGE_ACTIONS = [ ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE, @@ -107,6 +108,7 @@ function* resetEditorSaga() { // might end up in preview mode if they were in preview mode // previously yield put(setPreviewModeAction(false)); + yield put(resetSnipingMode()); yield put(resetEditorSuccess()); } diff --git a/app/client/src/sagas/OnboardingSagas.ts b/app/client/src/sagas/OnboardingSagas.ts index b28c243243..516bbdc0aa 100644 --- a/app/client/src/sagas/OnboardingSagas.ts +++ b/app/client/src/sagas/OnboardingSagas.ts @@ -14,6 +14,7 @@ import { } from "redux-saga/effects"; import { getFirstTimeUserOnboardingApplicationIds, + getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown, removeAllFirstTimeUserOnboardingApplicationIds, removeFirstTimeUserOnboardingApplicationId, setEnableStartSignposting, @@ -29,6 +30,7 @@ import { getHadReachedStep, getOnboardingWorkspaces, getQueryAction, + getSignpostingStepStateByStep, getTableWidget, } from "selectors/onboardingSelectors"; import type { Workspaces } from "@appsmith/constants/workspaceConstants"; @@ -39,6 +41,9 @@ import { loadGuidedTour, removeFirstTimeUserOnboardingApplicationId as removeFirstTimeUserOnboardingApplicationIdAction, setCurrentStep, + setSignpostingOverlay, + showSignpostingTooltip, + signpostingStepUpdate, toggleLoader, } from "actions/onboardingActions"; import { @@ -77,13 +82,11 @@ import { builderURL, queryEditorIdURL } from "RouteBuilder"; import { GuidedTourEntityNames } from "pages/Editor/GuidedTour/constants"; import type { GuidedTourState } from "reducers/uiReducers/guidedTourReducer"; import { sessionStorage } from "utils/localStorage"; -import store from "store"; -import { - createMessage, - ONBOARDING_SKIPPED_FIRST_TIME_USER, -} from "@appsmith/constants/messages"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; -import { toast } from "design-system"; +import type { SIGNPOSTING_STEP } from "pages/Editor/FirstTimeUserOnboarding/Utils"; +import type { StepState } from "reducers/uiReducers/onBoardingReducer"; +import { isUndefined } from "lodash"; +import { isAirgapped } from "@appsmith/utils/airgapHelpers"; const GUIDED_TOUR_STORAGE_KEY = "GUIDED_TOUR_STORAGE_KEY"; @@ -416,25 +419,6 @@ function* endFirstTimeUserOnboardingSaga() { firstTimeUserExperienceAppId, ), ); - toast.show(createMessage(ONBOARDING_SKIPPED_FIRST_TIME_USER), { - kind: "success", - action: { - text: "undo", - effect: () => { - store.dispatch({ - type: ReduxActionTypes.UNDO_END_FIRST_TIME_USER_ONBOARDING, - payload: firstTimeUserExperienceAppId, - }); - }, - }, - }); -} - -function* undoEndFirstTimeUserOnboardingSaga(action: ReduxAction) { - yield put({ - type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID, - payload: action.payload, - }); } function* firstTimeUserOnboardingInitSaga( @@ -445,15 +429,38 @@ function* firstTimeUserOnboardingInitSaga( type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID, payload: action.payload.applicationId, }); - yield put({ - type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL, - payload: true, - }); history.replace( builderURL({ pageId: action.payload.pageId, }), ); + + const isEditorInitialised: boolean = yield select(getIsEditorInitialized); + if (!isEditorInitialised) { + yield take(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS); + } + + let showOverlay = true; + + // We don't want to show the signposting overlay when we intend to show the + // telemetry callout + const currentUser: User | undefined = yield select(getCurrentUser); + if (currentUser?.isSuperUser && !isAirgapped()) { + const isAnonymousDataPopupAlreadyOpen: unknown = yield call( + getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown, + ); + if (!isAnonymousDataPopupAlreadyOpen) { + showOverlay = false; + } + } + + yield put(setSignpostingOverlay(showOverlay)); + // Show the modal once the editor is loaded. The delay is to grab user attention back once the editor + yield delay(1000); + yield put({ + type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL, + payload: true, + }); } function* setFirstTimeUserOnboardingCompleteSaga(action: ReduxAction) { @@ -467,6 +474,38 @@ function* disableStartFirstTimeUserOnboardingSaga() { yield call(setEnableStartSignposting, false); } +function* setSignpostingStepStateSaga( + action: ReduxAction<{ step: SIGNPOSTING_STEP; completed: boolean }>, +) { + const { completed, step } = action.payload; + const stepState: StepState | undefined = yield select( + getSignpostingStepStateByStep, + step, + ); + + // No changes to update so we ignore + if (stepState && stepState.completed === completed) return; + + const readProps = completed + ? { + read: false, + } + : {}; + yield put( + signpostingStepUpdate({ + ...action.payload, + ...readProps, + }), + ); + + // Show tooltip when a step is completed + if (!isUndefined(readProps.read) && !readProps.read) { + // Show tooltip after a small delay to not be abrupt + yield delay(1000); + yield put(showSignpostingTooltip(true)); + } +} + export default function* onboardingActionSagas() { yield all([ takeLatest( @@ -500,10 +539,6 @@ export default function* onboardingActionSagas() { ReduxActionTypes.END_FIRST_TIME_USER_ONBOARDING, endFirstTimeUserOnboardingSaga, ), - takeLatest( - ReduxActionTypes.UNDO_END_FIRST_TIME_USER_ONBOARDING, - undoEndFirstTimeUserOnboardingSaga, - ), takeLatest( ReduxActionTypes.FIRST_TIME_USER_ONBOARDING_INIT, firstTimeUserOnboardingInitSaga, @@ -516,5 +551,9 @@ export default function* onboardingActionSagas() { ReduxActionTypes.DISABLE_START_SIGNPOSTING, disableStartFirstTimeUserOnboardingSaga, ), + takeLatest( + ReduxActionTypes.SIGNPOSTING_STEP_UPDATE_INIT, + setSignpostingStepStateSaga, + ), ]); } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 1693c72aab..b94a233b4c 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -60,6 +60,7 @@ import { denormalize } from "utils/canvasStructureHelpers"; import { isAutoHeightEnabledForWidget } from "widgets/WidgetUtils"; import WidgetFactory from "utils/WidgetFactory"; import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import { getIsAnonymousDataPopupVisible } from "./onboardingSelectors"; const getIsDraggingOrResizing = (state: AppState) => state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; @@ -978,14 +979,16 @@ export const showCanvasTopSectionSelector = createSelector( getCanvasWidgets, previewModeSelector, getCurrentPageId, - (canvasWidgets, inPreviewMode, pageId) => { + getIsAnonymousDataPopupVisible, + (canvasWidgets, inPreviewMode, pageId, isAnonymousDataPopupVisible) => { const state = JSON.parse( localStorage.getItem(LOCAL_STORAGE_KEYS.CANVAS_CARDS_STATE) ?? "{}", ); if ( !state[pageId] || Object.keys(canvasWidgets).length > 1 || - inPreviewMode + inPreviewMode || + isAnonymousDataPopupVisible ) return false; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 508b982d4e..122822646e 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -51,6 +51,13 @@ export const getDatasources = (state: AppState): Datasource[] => { return state.entities.datasources.list; }; +// Returns non temp datasources +export const getSavedDatasources = (state: AppState): Datasource[] => { + return state.entities.datasources.list.filter( + (datasource) => datasource.id !== TEMP_DATASOURCE_ID, + ); +}; + export const getRecentDatasourceIds = (state: AppState): string[] => { return state.entities.datasources.recentDatasources; }; diff --git a/app/client/src/selectors/onboardingSelectors.tsx b/app/client/src/selectors/onboardingSelectors.tsx index b6c5595249..f0852b9705 100644 --- a/app/client/src/selectors/onboardingSelectors.tsx +++ b/app/client/src/selectors/onboardingSelectors.tsx @@ -3,13 +3,11 @@ import type { AppState } from "@appsmith/reducers"; import { createSelector } from "reselect"; import { getUserApplicationsWorkspaces } from "@appsmith/selectors/applicationSelectors"; import { getWidgets } from "sagas/selectors"; -import { - getActionResponses, - getActions, - getCanvasWidgets, -} from "./entitiesSelector"; +import { getActionResponses, getActions } from "./entitiesSelector"; import { getLastSelectedWidget } from "./ui"; import { GuidedTourEntityNames } from "pages/Editor/GuidedTour/constants"; +import type { SIGNPOSTING_STEP } from "pages/Editor/FirstTimeUserOnboarding/Utils"; +import { isBoolean } from "lodash"; // Signposting selectors @@ -38,29 +36,28 @@ export const getInOnboardingWidgetSelection = (state: AppState) => export const getIsOnboardingWidgetSelection = (state: AppState) => state.ui.onBoarding.inOnboardingWidgetSelection; -const previewModeSelector = (state: AppState) => { - return state.ui.editor.isPreviewMode; -}; - -export const getIsOnboardingTasksView = createSelector( - getCanvasWidgets, - getIsFirstTimeUserOnboardingEnabled, - getIsOnboardingWidgetSelection, - previewModeSelector, - ( - widgets, - enableFirstTimeUserOnboarding, - isOnboardingWidgetSelection, - inPreviewMode, - ) => { - return ( - Object.keys(widgets).length == 1 && - enableFirstTimeUserOnboarding && - !isOnboardingWidgetSelection && - !inPreviewMode - ); +export const getSignpostingStepState = (state: AppState) => + state.ui.onBoarding.stepState; +export const getSignpostingStepStateByStep = createSelector( + getSignpostingStepState, + (_state: AppState, step: SIGNPOSTING_STEP) => step, + (stepState, step) => { + return stepState.find((state) => state.step === step); }, ); +export const getSignpostingUnreadSteps = createSelector( + getSignpostingStepState, + (stepState) => { + if (!stepState.length) return []; + return stepState.filter((state) => isBoolean(state.read) && !state.read); + }, +); +export const getSignpostingSetOverlay = (state: AppState) => + state.ui.onBoarding.setOverlay; +export const getSignpostingTooltipVisible = (state: AppState) => + state.ui.onBoarding.showSignpostingTooltip; +export const getIsAnonymousDataPopupVisible = (state: AppState) => + state.ui.onBoarding.showAnonymousDataPopup; // Guided Tour selectors export const isExploringSelector = (state: AppState) => diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index 552910e8d6..ff1ed7eb59 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -187,13 +187,14 @@ export type EventName = | "SNIPPET_COPIED" | "SNIPPET_LOOKUP" | "SIGNPOSTING_SKIP" - | "SIGNPOSTING_CREATE_DATASOURCE_CLICK" - | "SIGNPOSTING_CREATE_QUERY_CLICK" - | "SIGNPOSTING_ADD_WIDGET_CLICK" - | "SIGNPOSTING_CONNECT_WIDGET_CLICK" - | "SIGNPOSTING_PUBLISH_CLICK" - | "SIGNPOSTING_BUILD_APP_CLICK" + | "SIGNPOSTING_MODAL_CREATE_DATASOURCE_CLICK" + | "SIGNPOSTING_MODAL_CREATE_QUERY_CLICK" + | "SIGNPOSTING_MODAL_ADD_WIDGET_CLICK" + | "SIGNPOSTING_MODAL_CONNECT_WIDGET_CLICK" + | "SIGNPOSTING_MODAL_PUBLISH_CLICK" | "SIGNPOSTING_WELCOME_TOUR_CLICK" + | "SIGNPOSTING_MODAL_CLOSE_CLICK" + | "SIGNPOSTING_INFO_CLICK" | "GS_BRANCH_MORE_MENU_OPEN" | "GIT_DISCARD_WARNING" | "GIT_DISCARD_CANCEL" @@ -322,6 +323,7 @@ export type EventName = | "GOOGLE_SHEET_FILE_PICKER_CANCEL" | "GOOGLE_SHEET_FILE_PICKER_PICKED" | "TELEMETRY_DISABLED" + | "HELP_MENU_WELCOME_TOUR_CLICK" | "DISPLAY_TELEMETRY_CALLOUT" | "VISIT_ADMIN_SETTINGS_TELEMETRY_CALLOUT" | "LEARN_MORE_TELEMETRY_CALLOUT" diff --git a/app/client/src/utils/lazyLottie.ts b/app/client/src/utils/lazyLottie.ts index c29062a5d0..dc4633e9aa 100644 --- a/app/client/src/utils/lazyLottie.ts +++ b/app/client/src/utils/lazyLottie.ts @@ -8,7 +8,7 @@ let cachedLottie: LottiePlayer | null = null; export type LazyAnimationItem = Pick< AnimationItem, - "play" | "addEventListener" | "destroy" + "play" | "addEventListener" | "destroy" | "goToAndStop" >; const lazyLottie = { @@ -32,7 +32,7 @@ const lazyLottie = { const abortController = new AbortController(); const queuedCommands: Array<{ - commandName: "play" | "addEventListener"; + commandName: "play" | "addEventListener" | "goToAndStop"; args: any[]; }> = []; @@ -60,6 +60,9 @@ const lazyLottie = { throw new Error("Not implemented"); }; }, + goToAndStop(...args) { + queuedCommands.push({ commandName: "goToAndStop", args }); + }, destroy() { abortController.abort(); }, diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index 8cdc6f44f4..8ae2aa402c 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -22,6 +22,7 @@ export const STORAGE_KEYS: { APP_THEMING_BETA_SHOWN: "APP_THEMING_BETA_SHOWN", FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY: "FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY", + SIGNPOSTING_APP_STATE: "SIGNPOSTING_APP_STATE", }; const store = localforage.createInstance({