feat: signposting update (#24389)

This commit is contained in:
akash-codemonk 2023-06-22 18:35:01 +05:30 committed by GitHub
parent 6df0810cc9
commit d9155b67e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1756 additions and 1907 deletions

View File

@ -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 () {

View File

@ -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);
});
});

View File

@ -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");
});

View File

@ -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']"
}

View File

@ -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");
}

View File

@ -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);
}
});
}
}

View File

@ -2151,5 +2151,4 @@ Cypress.Commands.add("SelectFromMultiSelect", (options) => {
Cypress.Commands.add("skipSignposting", () => {
onboarding.closeIntroModal();
onboarding.skipSignposting();
});

View File

@ -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,

View File

@ -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",

View File

@ -982,24 +982,33 @@ export const ONBOARDING_CHECKLIST_BODY = () =>
"Lets 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! Youve 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 youve 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";

View File

@ -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,

View File

@ -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 (
<div className="absolute top-5">
<Wrapper className="z-[1] self-center">
<Callout
isClosable
kind="info"
@ -42,12 +112,10 @@ export default function AnonymousDataPopup(props: {
onClick: () => handleLinkClick(TELEMETRY_DOCS_PAGE_URL),
},
]}
onClose={() => {
props.onCloseCallout();
}}
onClose={hideAnonymousDataPopup}
>
{createMessage(ONBOARDING_TELEMETRY_POPUP)}
</Callout>
</div>
</Wrapper>
);
}

View File

@ -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(
<Provider store={store}>
@ -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,

View File

@ -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<HTMLDivElement>(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 (
<div className="flex pt-0.5 flex-1 flex-col">
<ListItem
className={classNames({
"flex items-center justify-between": true,
})}
completed={props.completed}
data-testid={props.testid}
disabled={props.disabled}
onClick={
props.completed
? () => null
: () => {
props.onClick();
}
}
>
<Sibling
className="flex flex-1 items-center gap-2.5"
disabled={props.disabled}
>
{props.completed ? (
<LottieAnimationWrapper>
<LottieAnimationContainer ref={tickMarkRef} />
</LottieAnimationWrapper>
) : (
<PrefixCircle disabled={props.disabled} />
)}
<div>
<Text
className={classNames({
"signposting-strikethrough-bold":
props.completed && !stepState?.read,
"signposting-strikethrough-static":
props.completed && stepState?.read,
"signposting-strikethrough": true,
})}
color={
!props.disabled
? "var(--ads-v2-color-bg-brand-secondary)"
: "var(--ads-v2-color-fg-subtle)"
}
kind="heading-xs"
>
{props.boldText}
{props.normalPrefixText && (
<Text
color={!props.disabled ? "" : "var(--ads-v2-color-fg-subtle)"}
>
&nbsp;{props.normalPrefixText}
</Text>
)}
</Text>
<br />
<Text
className={classNames({
"signposting-strikethrough-normal":
props.completed && !stepState?.read,
"signposting-strikethrough-static":
props.completed && stepState?.read,
"signposting-strikethrough": true,
})}
color={!props.disabled ? "" : "var(--ads-v2-color-fg-subtle)"}
>
{props.normalText}
</Text>
</div>
</Sibling>
<Tooltip
align={{
targetOffset: [13, 0],
}}
content={createMessage(SIGNPOSTING_TOOLTIP.DOCUMENTATION.content)}
placement={"bottomLeft"}
>
<div className="absolute right-3">
<Button
isDisabled={props.disabled}
isIconButton
kind="tertiary"
onClick={(e) => {
AnalyticsUtil.logEvent("SIGNPOSTING_INFO_CLICK", {
step: props.step,
});
window.open(
props.docLink ?? "https://docs.appsmith.com/",
"_blank",
);
e.stopPropagation();
}}
startIcon="book-line"
/>
</div>
</Tooltip>
</ListItem>
<StyledDivider className="mt-0.5" />
</div>
);
}
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 <Redirect to={builderURL({ pageId })} />;
}
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 (
<Wrapper data-testid="checklist-wrapper">
<Link
className="t--checklist-back"
onClick={() => history.push(builderURL({ pageId }))}
startIcon="back-control"
>
Back
</Link>
{isCompleted && (
<Banner data-testid="checklist-completion-banner">
<BannerHeader>
{createMessage(ONBOARDING_CHECKLIST_BANNER_HEADER)}
</BannerHeader>
<BannerText>
{createMessage(ONBOARDING_CHECKLIST_BANNER_BODY)}
</BannerText>
<Button onClick={() => history.push(APPLICATIONS_URL)} size="md">
{createMessage(ONBOARDING_CHECKLIST_BANNER_BUTTON)}
</Button>
</Banner>
)}
<Pageheader className="font-bold py-6">
{createMessage(ONBOARDING_CHECKLIST_HEADER)}
</Pageheader>
<PageSubHeader>{createMessage(ONBOARDING_CHECKLIST_BODY)}</PageSubHeader>
<StatusWrapper>
<span
className="t--checklist-complete-status"
data-testid="checklist-completion-info"
useEffect(() => {
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 (
<>
<div
className="flex justify-between pb-3 items-center"
data-testid="checklist-completion-banner"
>
{completedTasks} of 5
</span>
&nbsp;{createMessage(ONBOARDING_CHECKLIST_COMPLETE_TEXT)}
</StatusWrapper>
<StyledList>
<StyledListItem>
<StyledListItemTextWrapper>
<StyledCompleteMarker>
<Icon
className="flex"
color={
datasources.length || actions.length
? "var(--ads-v2-color-fg-success)"
: ""
}
data-testid="checklist-datasource-complete-icon"
name="oval-check"
size="lg"
/>
</StyledCompleteMarker>
<ChecklistText active={!!datasources.length || !!actions.length}>
<span>
{createMessage(ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.bold)}
</span>
&nbsp;
{createMessage(ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.normal)}
</ChecklistText>
</StyledListItemTextWrapper>
{!datasources.length && !actions.length && (
<Button
className="t--checklist-datasource-button"
data-testid="checklist-datasource-button"
kind={
suggestedNextAction ===
createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.CONNECT_A_DATASOURCE,
)
? "primary"
: "secondary"
}
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_CREATE_DATASOURCE_CLICK", {
from: "CHECKLIST",
});
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.NEW,
}),
);
}}
size="md"
>
{createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.CONNECT_A_DATASOURCE,
)}
</Button>
)}
</StyledListItem>
<StyledListItem>
<StyledListItemTextWrapper>
<StyledCompleteMarker>
<Icon
className="flex"
color={actions.length ? "var(--ads-v2-color-fg-success)" : ""}
data-testid="checklist-action-complete-icon"
name="oval-check"
size="lg"
/>
</StyledCompleteMarker>
<ChecklistText active={!!actions.length}>
<span>
{createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.bold)}
</span>
&nbsp;{createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.normal)}
</ChecklistText>
</StyledListItemTextWrapper>
{!actions.length && (
<Button
className="t--checklist-action-button"
data-testid="checklist-action-button"
isDisabled={!datasources.length}
kind={
suggestedNextAction ===
createMessage(() => ONBOARDING_CHECKLIST_ACTIONS.CREATE_A_QUERY)
? "primary"
: "secondary"
}
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_CREATE_QUERY_CLICK", {
from: "CHECKLIST",
});
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,
},
);
}}
size="md"
>
{createMessage(() => ONBOARDING_CHECKLIST_ACTIONS.CREATE_A_QUERY)}
</Button>
)}
</StyledListItem>
<StyledListItem>
<StyledListItemTextWrapper>
<StyledCompleteMarker>
<Icon
className="flex"
color={
Object.keys(widgets).length > 1
? "var(--ads-v2-color-fg-success)"
: ""
}
data-testid="checklist-widget-complete-icon"
name="oval-check"
size="lg"
/>
</StyledCompleteMarker>
<ChecklistText active={Object.keys(widgets).length > 1}>
<span>
{createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.bold)}
</span>
&nbsp;{createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.normal)}
</ChecklistText>
</StyledListItemTextWrapper>
{Object.keys(widgets).length === 1 && (
<Button
className="t--checklist-widget-button"
data-testid="checklist-widget-button"
kind={
suggestedNextAction ===
createMessage(() => ONBOARDING_CHECKLIST_ACTIONS.ADD_WIDGETS)
? "primary"
: "secondary"
}
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", {
from: "CHECKLIST",
});
dispatch(toggleInOnboardingWidgetSelection(true));
dispatch(forceOpenWidgetPanel(true));
history.push(builderURL({ pageId }));
}}
size="md"
>
{createMessage(() => ONBOARDING_CHECKLIST_ACTIONS.ADD_WIDGETS)}
</Button>
)}
</StyledListItem>
<StyledListItem>
<StyledListItemTextWrapper>
<StyledCompleteMarker>
<Icon
className="flex"
color={
isConnectionPresent ? "var(--ads-v2-color-fg-success)" : ""
}
data-testid="checklist-connection-complete-icon"
name="oval-check"
size="lg"
/>
</StyledCompleteMarker>
<ChecklistText active={!!isConnectionPresent}>
<span>
{createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.bold,
)}
</span>
&nbsp;
{createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.normal,
)}
</ChecklistText>
</StyledListItemTextWrapper>
{!isConnectionPresent && (
<Button
className="t--checklist-connection-button"
data-testid="checklist-connection-button"
isDisabled={Object.keys(widgets).length === 1 || !actions.length}
kind={
suggestedNextAction ===
createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.CONNECT_DATA_TO_WIDGET,
)
? "primary"
: "secondary"
}
onClick={onconnectYourWidget}
size="md"
>
{createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.CONNECT_DATA_TO_WIDGET,
)}
</Button>
)}
</StyledListItem>
<StyledListItem>
<StyledListItemTextWrapper>
<StyledCompleteMarker>
<Icon
className="flex"
color={isDeployed ? "var(--ads-v2-color-fg-success)" : ""}
data-testid="checklist-deploy-complete-icon"
name="oval-check"
size="lg"
/>
</StyledCompleteMarker>
<ChecklistText active={!!isDeployed}>
<span>
{createMessage(ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.bold)}
</span>
&nbsp;
{createMessage(ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.normal)}
</ChecklistText>
</StyledListItemTextWrapper>
{!isDeployed && (
<Button
className="t--checklist-deploy-button"
data-testid="checklist-deploy-button"
kind={
suggestedNextAction ===
createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.DEPLOY_APPLICATIONS,
)
? "primary"
: "secondary"
}
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_PUBLISH_CLICK", {
from: "CHECKLIST",
});
dispatch({
type: ReduxActionTypes.PUBLISH_APPLICATION_INIT,
payload: {
applicationId,
},
});
}}
size="md"
>
{createMessage(
() => ONBOARDING_CHECKLIST_ACTIONS.DEPLOY_APPLICATIONS,
)}
</Button>
)}
</StyledListItem>
</StyledList>
{!isAirgappedInstance && (
<StyledFooter
className="flex"
onClick={() => triggerWelcomeTour(dispatch, applicationId)}
>
<StyledCompleteMarker>
<Icon name="rocket" size="lg" />
</StyledCompleteMarker>
<Text style={{ lineHeight: "14px" }} type={TextType.P1}>
{createMessage(ONBOARDING_CHECKLIST_FOOTER)}
<Text
className="flex-1"
color="var(--ads-v2-color-fg-emphasis)"
kind="heading-m"
>
{createMessage(SIGNPOSTING_SUCCESS_POPUP.title)}
</Text>
<Icon name="arrow-forward" size="md" />
</StyledFooter>
)}
</Wrapper>
<Button
isIconButton
kind="tertiary"
onClick={() => {
dispatch(showSignpostingModal(false));
}}
startIcon={"close-line"}
/>
</div>
<Text color="var(--ads-v2-color-bg-brand-secondary)" kind="heading-xs">
{createMessage(SIGNPOSTING_SUCCESS_POPUP.subtitle)}
</Text>
<StyledDivider className="mt-4" />
</>
);
}
return (
<>
<div className="flex-1">
<div className="flex justify-between pb-3 items-center">
<Text color="var(--ads-v2-color-fg-emphasis)" kind="heading-m">
{createMessage(ONBOARDING_CHECKLIST_HEADER)}
</Text>
<Button
data-testid="signposting-modal-close-btn"
isIconButton
kind="tertiary"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_CLOSE_CLICK");
dispatch(showSignpostingModal(false));
}}
startIcon={"close-line"}
/>
</div>
<Text color="var(--ads-v2-color-bg-brand-secondary)" kind="heading-xs">
{createMessage(SIGNPOSTING_POPUP_SUBTITLE)}
</Text>
<div className="mt-5">
<Text
color="var(--ads-v2-color-bg-brand-secondary)"
data-testid="checklist-completion-info"
kind="heading-xs"
>
{completedTasks} of 5{" "}
</Text>
<Text>complete</Text>
</div>
<StyledDivider className="mt-1" />
</div>
<div
className="overflow-auto min-h-[60px]"
data-testid="checklist-wrapper"
>
<CheckListItem
boldText={createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.bold,
)}
completed={!!(datasources.length || actions.length)}
disabled={false}
docLink="https://docs.appsmith.com/core-concepts/connecting-to-data-sources"
normalText={createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_SOURCE.normal,
)}
onClick={() => {
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"}
/>
<CheckListItem
boldText={createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.bold)}
completed={!!actions.length}
disabled={!datasources.length && !actions.length}
docLink="https://docs.appsmith.com/core-concepts/data-access-and-binding/querying-a-database"
normalPrefixText={createMessage(
ONBOARDING_CHECKLIST_CREATE_A_QUERY.normalPrefix,
)}
normalText={createMessage(ONBOARDING_CHECKLIST_CREATE_A_QUERY.normal)}
onClick={() => {
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"}
/>
<CheckListItem
boldText={createMessage(ONBOARDING_CHECKLIST_ADD_WIDGETS.bold)}
completed={Object.keys(widgets).length > 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"}
/>
<CheckListItem
boldText={createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.bold,
)}
completed={isConnectionPresent}
disabled={Object.keys(widgets).length === 1 || !actions.length}
docLink="https://docs.appsmith.com/core-concepts/data-access-and-binding/displaying-data-read"
normalText={createMessage(
ONBOARDING_CHECKLIST_CONNECT_DATA_TO_WIDGET.normal,
)}
onClick={onconnectYourWidget}
step={SIGNPOSTING_STEP.CONNECT_DATA_TO_WIDGET}
testid={"checklist-connection"}
/>
<CheckListItem
boldText={createMessage(
ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.bold,
)}
completed={isDeployed}
disabled={false}
normalText={createMessage(
ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS.normal,
)}
onClick={() => {
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"}
/>
</div>
</>
);
}

View File

@ -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 (
<div
className={classNames({
"mt-3.5": !props.showIntercomConsent,
"flex-1": true,
})}
>
{props.showIntercomConsent ? (
<IntercomConsent showIntercomConsent={props.setShowIntercomConsent} />
) : (
<>
<Text
color="var(--ads-v2-color-bg-brand-secondary)"
kind="heading-xs"
>
Help & Resources
</Text>
<div className="flex gap-2 flex-wrap mt-2">
<Button
data-testid="editor-welcome-tour"
kind="secondary"
onClick={() => {
triggerWelcomeTour(dispatch);
AnalyticsUtil.logEvent("SIGNPOSTING_WELCOME_TOUR_CLICK");
}}
startIcon={"guide"}
>
Try guided tour
</Button>
{HELP_MENU_ITEMS.map((item) => {
return (
<Button
key={item.label}
kind="secondary"
onClick={(e: any) => {
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 {
props.setShowIntercomConsent(true);
}
}
}
}}
startIcon={item.icon}
>
{item.label}
</Button>
);
})}
</div>
</>
)}
{appVersion.id && (
<HelpFooter className="mt-2">
<StyledText color="var(--ads-v2-color-fg-muted)" kind={"action-s"}>
{createMessage(
APPSMITH_DISPLAY_VERSION,
appVersion.edition,
appVersion.id,
cloudHosting,
)}
</StyledText>
<StyledText color="var(--ads-v2-color-fg-muted)" kind={"action-s"}>
Released {moment(appVersion.releaseDate).fromNow()}
</StyledText>
</HelpFooter>
)}
</div>
);
}
export default HelpMenu;

View File

@ -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 (
<Modal onOpenChange={closeModal} open={modalAlwaysOpen}>
<ModalContent
onEscapeKeyDown={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
style={{ width: "920px" }}
>
<ModalHeader className="t--how-appsmith-works-modal-header">
{createMessage(WELCOME_TO_APPSMITH)}
</ModalHeader>
<ModalBody>
<ModalSubHeader>{createMessage(HOW_APPSMITH_WORKS)}</ModalSubHeader>
<ModalContentWrapper>
<ModalContentRow border>
<ModalContentTextWrapper>
<div>
<StyledCount>1</StyledCount>
</div>
<ModalContentItem>
<ModalContentHeader>
{createMessage(ONBOARDING_INTRO_CONNECT_YOUR_DATABASE)}
</ModalContentHeader>
<ModalContentDescription>
{createMessage(QUERY_YOUR_DATABASE)}
</ModalContentDescription>
</ModalContentItem>
</ModalContentTextWrapper>
<StyledImgWrapper>
<StyledImg
alt="connect-data-image"
src={getAssetUrl(getConnectDataImg())}
/>
</StyledImgWrapper>
</ModalContentRow>
<ModalContentRow border>
<ModalContentTextWrapper>
<div>
<StyledCount>2</StyledCount>
</div>
<ModalContentItem>
<ModalContentHeader>
{createMessage(DRAG_AND_DROP)}
</ModalContentHeader>
<ModalContentDescription>
{createMessage(CUSTOMIZE_WIDGET_STYLING)}
</ModalContentDescription>
</ModalContentItem>
</ModalContentTextWrapper>
<StyledImgWrapper>
<StyledImg
alt="drag-and-drop-img"
src={getAssetUrl(getDragAndDropImg())}
/>
</StyledImgWrapper>
</ModalContentRow>
<ModalContentRow className="border-b-0">
<ModalContentTextWrapper>
<div>
<StyledCount>3</StyledCount>
</div>
<ModalContentItem>
<ModalContentHeader>
{createMessage(ONBOARDING_INTRO_PUBLISH)}
</ModalContentHeader>
<ModalContentDescription>
{createMessage(CHOOSE_ACCESS_CONTROL_ROLES)}
</ModalContentDescription>
</ModalContentItem>
</ModalContentTextWrapper>
<StyledImgWrapper>
<StyledImg
alt="publish-image"
src={getAssetUrl(getPublishAppsImg())}
/>
</StyledImgWrapper>
</ModalContentRow>
</ModalContentWrapper>
<ModalFooterText>
{createMessage(ONBOARDING_INTRO_FOOTER)}
</ModalFooterText>
</ModalBody>
<ModalFooter>
{!isAirgappedInstance && (
<Button
className="t--introduction-modal-welcome-tour-button"
kind="secondary"
onClick={() => triggerWelcomeTour(dispatch, applicationId)}
size="md"
>
{createMessage(START_TUTORIAL)}
</Button>
)}
<Button
className="t--introduction-modal-build-button"
onClick={onBuildApp}
size="md"
>
{createMessage(BUILD_MY_FIRST_APP)}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@ -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 (
<StyledMenuContent
animate={props.setOverlay}
collisionPadding={10}
data-testid="signposting-modal"
onInteractOutside={() => {
dispatch(showSignpostingModal(false));
}}
width={SIGNPOSTING_POPUP_WIDTH}
>
<Wrapper>
{!props.showIntercomConsent && <Checklist />}
<HelpMenu
setShowIntercomConsent={props.setShowIntercomConsent}
showIntercomConsent={props.showIntercomConsent}
/>
</Wrapper>
</StyledMenuContent>
);
}
export default OnboardingModal;

View File

@ -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 (
<StyledOverlay
className="fixed top-0 w-full h-full overflow-hidden"
onClick={() => {
dispatch(showSignpostingModal(false));
}}
/>
);
}
return null;
}
export default Overlay;

View File

@ -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(
<Provider store={store}>
@ -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", () => {

View File

@ -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<StatusProgressbarContainerType>`
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<StatusProgressbarType>`
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 (
<ProgressContainer {...props}>
<Progressbar {...props} />
</ProgressContainer>
);
}
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 (
<Wrapper
active={isChecklistPage}
className="sticky top-0 t--onboarding-statusbar"
data-testid="statusbar-container"
onClick={() => {
history.push(onboardingCheckListUrl({ pageId }));
}}
>
<StyledClose
className="hover-icons"
data-testid="statusbar-skip"
isIconButton
kind={isChecklistPage ? "primary" : "tertiary"}
onClick={endFirstTimeUserOnboarding}
size="sm"
startIcon="close-control"
/>
<TitleWrapper>
{createMessage(ONBOARDING_STATUS_GET_STARTED)}
</TitleWrapper>
<StatusText className="mt-1">
<span data-testid="statusbar-text">{content}</span>&nbsp;&nbsp;
{!isChecklistPage && (
<Icon className="hover-icons" name="right-arrow-2" size="md" />
)}
</StatusText>
<StatusProgressbar
active={isChecklistPage}
data-testid="statusbar-text"
percentage={percentage}
/>
</Wrapper>
);
return null;
}
export default withRouter(OnboardingStatusbar);
export default OnboardingStatusbar;

View File

@ -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(
<Provider store={store}>
<OnboardingTasks />
</Provider>,
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,
},
});
});
});

View File

@ -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 = (
<CenteredContainer>
<TaskImageContainer>
<TaskImage src={getAssetUrl(getOnboardingDatasourceImg())} />
</TaskImageContainer>
<TaskHeader
className="t--tasks-datasource-header"
data-testid="onboarding-tasks-datasource-text"
>
{createMessage(ONBOARDING_TASK_DATASOURCE_HEADER)}
</TaskHeader>
<TaskSubText>
{createMessage(ONBOARDING_TASK_DATASOURCE_BODY)}
</TaskSubText>
<TaskButtonWrapper>
<Button
className="t--tasks-datasource-button"
data-testid="onboarding-tasks-datasource-button"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_CREATE_DATASOURCE_CLICK", {
from: "CANVAS",
});
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.NEW,
}),
);
// Event for datasource creation click
const entryPoint = DatasourceCreateEntryPoints.ONBOARDING;
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
entryPoint,
});
}}
size="md"
startIcon="plus"
>
{createMessage(ONBOARDING_TASK_DATASOURCE_BUTTON)}
</Button>
</TaskButtonWrapper>
<Taskfootnote>
{createMessage(ONBOARDING_TASK_FOOTER)}&nbsp;
<Link
className="t--tasks-datasource-alternate-button"
data-testid="onboarding-tasks-datasource-alt"
kind="primary"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", {
from: "CANVAS",
});
dispatch(toggleInOnboardingWidgetSelection(true));
dispatch(forceOpenWidgetPanel(true));
}}
>
{createMessage(ONBOARDING_TASK_DATASOURCE_FOOTER_ACTION)}
</Link>
&nbsp;{createMessage(ONBOARDING_TASK_DATASOURCE_FOOTER)}
</Taskfootnote>
</CenteredContainer>
);
} else if (!actions.length) {
content = (
<CenteredContainer>
<TaskImageContainer>
<TaskImage src={getAssetUrl(getOnboardingQueryImg())} />
</TaskImageContainer>
<TaskHeader
className="t--tasks-datasource-header"
data-testid="onboarding-tasks-action-text"
>
{createMessage(ONBOARDING_TASK_QUERY_HEADER)}
</TaskHeader>
<TaskSubText>{createMessage(ONBOARDING_TASK_QUERY_BODY)}</TaskSubText>
<TaskButtonWrapper>
<Button
className="t--tasks-action-button"
data-testid="onboarding-tasks-action-button"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_CREATE_QUERY_CLICK", {
from: "CANVAS",
});
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
);
}}
size="md"
startIcon="plus"
>
{createMessage(ONBOARDING_TASK_QUERY_BUTTON)}
</Button>
</TaskButtonWrapper>
<Taskfootnote>
{createMessage(ONBOARDING_TASK_FOOTER)}&nbsp;
<Link
className="t--tasks-action-alternate-button"
data-testid="onboarding-tasks-action-alt"
kind="primary"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", {
from: "CANVAS",
});
dispatch(toggleInOnboardingWidgetSelection(true));
dispatch(forceOpenWidgetPanel(true));
}}
>
{createMessage(ONBOARDING_TASK_QUERY_FOOTER_ACTION)}
</Link>
</Taskfootnote>
</CenteredContainer>
);
} else if (Object.keys(widgets).length === 1) {
content = (
<CenteredContainer>
<TaskImageContainer>
<TaskImage src={getAssetUrl(getOnboardingWidgetImg())} />
</TaskImageContainer>
<TaskHeader
className="t--tasks-datasource-header"
data-testid="onboarding-tasks-widget-text"
>
{createMessage(ONBOARDING_TASK_WIDGET_HEADER)}
</TaskHeader>
<TaskSubText>{createMessage(ONBOARDING_TASK_WIDGET_BODY)}</TaskSubText>
<TaskButtonWrapper>
<Button
className="t--tasks-widget-button"
data-testid="onboarding-tasks-widget-button"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_ADD_WIDGET_CLICK", {
from: "CANVAS",
});
dispatch(toggleInOnboardingWidgetSelection(true));
dispatch(forceOpenWidgetPanel(true));
}}
size="md"
startIcon="plus"
>
{createMessage(ONBOARDING_TASK_WIDGET_BUTTON)}
</Button>
</TaskButtonWrapper>
<Taskfootnote>
{createMessage(ONBOARDING_TASK_FOOTER)}&nbsp;
<Link
className="t--tasks-widget-alternate-button"
data-testid="onboarding-tasks-widget-alt"
kind="primary"
onClick={() => {
AnalyticsUtil.logEvent("SIGNPOSTING_PUBLISH_CLICK", {
from: "CANVAS",
});
dispatch({
type: ReduxActionTypes.PUBLISH_APPLICATION_INIT,
payload: {
applicationId,
},
});
}}
>
{createMessage(ONBOARDING_TASK_WIDGET_FOOTER_ACTION)}
</Link>
.
</Taskfootnote>
</CenteredContainer>
);
}
return (
<Wrapper data-testid="onboarding-tasks-wrapper">
{content}
{isAnonymousDataPopupOpen && (
<AnonymousDataPopup onCloseCallout={hideAnonymousDataPopup} />
)}
{!isAdmin && showModal && (
<IntroductionModal
close={() => {
dispatch({
type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
payload: false,
});
}}
/>
)}
</Wrapper>
);
}

View File

@ -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}
<br />
<br />
{completedTasks === SIGNPOSTING_STEPS.length - 1 && (
<>
{lastStepContent}
<br />
</>
)}
{content}
</>
);
}
export default TooltipContent;

View File

@ -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<any>,
applicationId: string,
) => {
AnalyticsUtil.logEvent("SIGNPOSTING_WELCOME_TOUR_CLICK");
export const triggerWelcomeTour = (dispatch: Dispatch<any>) => {
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",
}

View File

@ -38,6 +38,7 @@ export const initialState: any = {
firstTimeUserOnboardingComplete: false,
showFirstTimeUserOnboardingModal: true,
firstTimeUserOnboardingApplicationIds: ["1"],
stepState: [],
},
theme: {
theme: {

View File

@ -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 = () => {

View File

@ -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 (
<TooltipContent showSignpostingTooltip={props.showSignpostingTooltip} />
);
}
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() {
<Menu
onOpenChange={(open) => {
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}
>
<MenuTrigger>
<div>
<div className="relative">
<Tooltip
content={createMessage(HELP_RESOURCE_TOOLTIP)}
align={{
targetOffset: [5, 0],
}}
content={
<HelpButtonTooltip
isFirstTimeUserOnboardingEnabled={
isFirstTimeUserOnboardingEnabled
}
showSignpostingTooltip={showSignpostingTooltip}
/>
}
destroyTooltipOnHide={isFirstTimeUserOnboardingEnabled}
isDisabled={onboardingModalOpen}
mouseLeaveDelay={0}
placement="bottomRight"
{...tooltipProps}
>
<Button
data-testid="t--help-button"
@ -153,56 +232,85 @@ function HelpButton() {
Help
</Button>
</Tooltip>
{showUnreadSteps && <UnreadSteps className="unread" />}
</div>
</MenuTrigger>
<MenuContent collisionPadding={10} style={{ width: HELP_MODAL_WIDTH }}>
{showIntercomConsent ? (
<IntercomConsent showIntercomConsent={setShowIntercomConsent} />
) : (
HELP_MENU_ITEMS.map((item) => (
<MenuItem
id={item.id}
key={item.label}
onSelect={(e) => {
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 ? (
<SignpostingPopup
setOverlay={setOverlay}
setShowIntercomConsent={setShowIntercomConsent}
showIntercomConsent={showIntercomConsent}
/>
) : (
<MenuContent collisionPadding={10} style={{ width: HELP_MODAL_WIDTH }}>
{showIntercomConsent ? (
<IntercomConsent showIntercomConsent={setShowIntercomConsent} />
) : (
<>
{!isAirgappedInstance && !guidedTourEnabled && (
<>
<MenuItem
data-testid="editor-welcome-tour"
onSelect={() => {
triggerWelcomeTour(dispatch);
AnalyticsUtil.logEvent("HELP_MENU_WELCOME_TOUR_CLICK");
}}
startIcon="guide"
>
Try guided tour
</MenuItem>
<MenuSeparator />
</>
)}
{HELP_MENU_ITEMS.map((item) => (
<MenuItem
id={item.id}
key={item.label}
onSelect={(e) => {
if (item.link) {
window.open(item.link, "_blank");
}
}
}
}}
startIcon={item.icon}
>
{item.label}
</MenuItem>
))
)}
{appVersion.id && (
<>
<MenuSeparator />
<MenuItem className="menuitem-nohover">
<HelpFooter>
<span>
{createMessage(
APPSMITH_DISPLAY_VERSION,
appVersion.edition,
appVersion.id,
cloudHosting,
)}
</span>
<span>Released {moment(appVersion.releaseDate).fromNow()}</span>
</HelpFooter>
</MenuItem>
</>
)}
</MenuContent>
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}
</MenuItem>
))}
</>
)}
{appVersion.id && (
<>
<MenuSeparator />
<MenuItem className="menuitem-nohover">
<HelpFooter>
<span>
{createMessage(
APPSMITH_DISPLAY_VERSION,
appVersion.edition,
appVersion.id,
cloudHosting,
)}
</span>
<span>
Released {moment(appVersion.releaseDate).fromNow()}
</span>
</HelpFooter>
</MenuItem>
</>
)}
</MenuContent>
)}
</Menu>
);
}

View File

@ -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"}

View File

@ -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 (
<EditorContextProvider renderMode="CANVAS">
{showOnboardingTasks ? (
<OnboardingTasks />
) : (
<>
{guidedTourEnabled && <Guide />}
{guidedTourEnabled && <Guide />}
<div className="relative flex flex-row w-full overflow-hidden">
<div
className={classNames({
"relative flex flex-col w-full overflow-hidden": true,
"m-8 border border-gray-200":
isAppSettingsPaneWithNavigationTabOpen,
})}
>
{!isAppSettingsPaneWithNavigationTabOpen && <CanvasTopSection />}
<AnonymousDataPopup />
<div
className="relative flex flex-row w-full overflow-hidden"
data-testid="widgets-editor"
draggable
id="widgets-editor"
onClick={handleWrapperClick}
onDragStart={onDragStart}
style={{
fontFamily: fontFamily,
}}
>
{showNavigation()}
<div className="relative flex flex-row w-full overflow-hidden">
<div
<PageViewContainer
className={classNames({
"relative flex flex-col w-full overflow-hidden": true,
"m-8 border border-gray-200":
"relative flex flex-row w-full justify-center overflow-hidden":
true,
"select-none pointer-events-none":
isAppSettingsPaneWithNavigationTabOpen,
})}
hasPinnedSidebar={
isPreviewingNavigation && !isMobile
? currentApplicationDetails?.applicationDetail
?.navigationSetting?.orientation ===
NAVIGATION_SETTINGS.ORIENTATION.SIDE && isAppSidebarPinned
: false
}
isPreviewMode={isPreviewMode}
isPublished={isPublished}
sidebarWidth={isPreviewingNavigation ? sidebarWidth : 0}
>
{!isAppSettingsPaneWithNavigationTabOpen && <CanvasTopSection />}
{shouldShowSnapShotBanner && (
<div className="absolute top-0 z-1 w-full">
<SnapShotBannerCTA />
</div>
)}
<CanvasContainer
isAppSettingsPaneWithNavigationTabOpen={
AppSettingsTabs.Navigation === appSettingsPaneContext?.type
}
isPreviewMode={isPreviewMode}
navigationHeight={navigationHeight}
shouldShowSnapShotBanner={shouldShowSnapShotBanner}
/>
</PageViewContainer>
<div
className="relative flex flex-row w-full overflow-hidden"
data-testid="widgets-editor"
draggable
id="widgets-editor"
onClick={handleWrapperClick}
onDragStart={onDragStart}
style={{
fontFamily: fontFamily,
}}
>
{showNavigation()}
<PageViewContainer
className={classNames({
"relative flex flex-row w-full justify-center overflow-hidden":
true,
"select-none pointer-events-none":
isAppSettingsPaneWithNavigationTabOpen,
})}
hasPinnedSidebar={
isPreviewingNavigation && !isMobile
? currentApplicationDetails?.applicationDetail
?.navigationSetting?.orientation ===
NAVIGATION_SETTINGS.ORIENTATION.SIDE &&
isAppSidebarPinned
: false
}
isPreviewMode={isPreviewMode}
isPublished={isPublished}
sidebarWidth={isPreviewingNavigation ? sidebarWidth : 0}
>
{shouldShowSnapShotBanner && (
<div className="absolute top-0 z-1 w-full">
<SnapShotBannerCTA />
</div>
)}
<CanvasContainer
isAppSettingsPaneWithNavigationTabOpen={
AppSettingsTabs.Navigation ===
appSettingsPaneContext?.type
}
isPreviewMode={isPreviewMode}
navigationHeight={navigationHeight}
shouldShowSnapShotBanner={shouldShowSnapShotBanner}
/>
</PageViewContainer>
<CrudInfoModal />
</div>
<Debugger />
</div>
<PropertyPaneContainer />
<CrudInfoModal />
</div>
</>
)}
<Debugger />
</div>
<PropertyPaneContainer />
</div>
</EditorContextProvider>
);
}

View File

@ -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<Props> {
<TemplatesModal />
<ImportedApplicationSuccessModal />
<ReconnectDatasourceModal />
<SignpostingOverlay />
</GlobalHotKeys>
</div>
<RequestConfirmationModal />

View File

@ -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<StepState>,
) => {
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<boolean>,
) => {
return {
...state,
setOverlay: action.payload,
};
},
[ReduxActionTypes.SIGNPOSTING_SHOW_TOOLTIP]: (
state: OnboardingState,
action: ReduxAction<boolean>,
) => {
return {
...state,
showSignpostingTooltip: action.payload,
};
},
[ReduxActionTypes.SHOW_ANONYMOUS_DATA_POPUP]: (
state: OnboardingState,
action: ReduxAction<boolean>,
) => {
return {
...state,
showAnonymousDataPopup: action.payload,
};
},
});
export default onboardingReducer;

View File

@ -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());
}

View File

@ -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<string>) {
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<boolean>) {
@ -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,
),
]);
}

View File

@ -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;

View File

@ -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;
};

View File

@ -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) =>

View File

@ -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"

View File

@ -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();
},

View File

@ -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({