Onboarding flow (#1960)

Co-authored-by: Hetu Nandu <hetunandu@gmail.com>
This commit is contained in:
akash-codemonk 2020-12-18 18:48:47 +05:30 committed by GitHub
parent 5a36d17f7a
commit e84699e7ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1925 additions and 290 deletions

View File

@ -0,0 +1,67 @@
const onboarding = require("../../../locators/Onboarding.json");
const explorer = require("../../../locators/explorerlocators.json");
const homePage = require("../../../locators/HomePage.json");
const loginPage = require("../../../locators/LoginPage.json");
describe("Onboarding", function() {
it("Onboarding flow", function() {
cy.LogOut();
cy.visit("/user/signup");
cy.get("input[name='email']").type(Cypress.env("USERNAME"));
cy.get(loginPage.password).type(Cypress.env("PASSWORD"));
cy.get(loginPage.submitBtn).click();
cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
cy.get(homePage.createNew)
.first()
.click({ force: true });
cy.wait("@createNewApplication").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
cy.get("#loading").should("not.exist");
//Onboarding
cy.contains(".t--create-database", "Explore Appsmith").click();
cy.get(onboarding.tooltipAction).click();
// Add widget
cy.get(".t--add-widget").click();
cy.dragAndDropToCanvas("tablewidget", { x: 30, y: -30 });
cy.get(onboarding.tooltipSnippet).click({ force: true });
cy.get(".t--property-control-tabledata" + " .CodeMirror textarea")
.first()
.focus({ force: true })
.type("{uparrow}", { force: true })
.type("{ctrl}{shift}{downarrow}", { force: true });
cy.focused().then(() => {
cy.get(".t--property-control-tabledata" + " .CodeMirror")
.first()
.then(editor => {
editor[0].CodeMirror.setValue("{{ExampleQuery.data}}");
});
});
cy.closePropertyPane();
cy.get(explorer.closeWidgets).click();
cy.openPropertyPane("tablewidget");
cy.get(onboarding.tooltipAction).click({ force: true });
cy.PublishtheApp();
cy.get(".t--continue-on-my-own").click();
});
after(() => {
localStorage.removeItem("OnboardingState");
cy.window().then(window => {
window.indexedDB.deleteDatabase("Appsmith");
});
cy.log("Cleared");
});
});

View File

@ -13,6 +13,7 @@ describe("Create new org and an app within the same", function() {
cy.createOrg(orgid);
cy.CreateAppForOrg(orgid, appid);
cy.NavigateToHome();
cy.CreateApp(appid);
});
});

View File

@ -0,0 +1,4 @@
{
"tooltipAction": ".tooltip-action",
"tooltipSnippet": ".tooltip-snippet"
}

View File

@ -249,6 +249,7 @@ Cypress.Commands.add("CreateApp", appname => {
);
cy.get("#loading").should("not.exist");
cy.wait(1000);
cy.get(homePage.applicationName).type(appname + "{enter}");
cy.wait("@updateApplication").should(
"have.nested.property",
@ -950,11 +951,18 @@ Cypress.Commands.add("PublishtheApp", () => {
// Wait before publish
cy.wait(2000);
cy.assertPageSave();
// Stubbing window.open to open in the same tab
cy.window().then(window => {
cy.stub(window, "open").callsFake(url => {
window.location.href = Cypress.config().baseUrl + url.substring(1);
window.location.target = "_self";
});
});
cy.get(homePage.publishButton).click();
cy.wait("@publishApp");
cy.get('a[class="bp3-button"]')
.invoke("removeAttr", "target")
.click({ force: true });
cy.url().should("include", "/pages");
cy.log("pagename: " + localStorage.getItem("PageName"));
});

View File

@ -70,6 +70,7 @@
"localforage": "^1.7.3",
"lodash": "^4.17.19",
"loglevel": "^1.6.7",
"lottie-web": "^5.7.4",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
"nanoid": "^2.0.4",

View File

@ -0,0 +1,43 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { Action } from "entities/Action";
export const createOnboardingActionInit = (payload: Partial<Action>) => {
return {
type: ReduxActionTypes.CREATE_ONBOARDING_ACTION_INIT,
payload,
};
};
export const createOnboardingActionSuccess = (payload: Action) => {
return {
type: ReduxActionTypes.CREATE_ONBOARDING_ACTION_SUCCESS,
payload,
};
};
export const showTooltip = (payload: number) => {
return {
type: ReduxActionTypes.SHOW_ONBOARDING_TOOLTIP,
payload,
};
};
export const endOnboarding = () => {
return {
type: ReduxActionTypes.END_ONBOARDING,
};
};
export const setCurrentStep = (payload: number) => {
return {
type: ReduxActionTypes.SET_CURRENT_STEP,
payload,
};
};
export const setOnboardingState = (payload: boolean) => {
return {
type: ReduxActionTypes.SET_ONBOARDING_STATE,
payload,
};
};

View File

@ -5,7 +5,7 @@ import {
DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
} from "constants/ApiConstants";
import axios, { AxiosPromise, CancelTokenSource } from "axios";
import { RestAction } from "entities/Action";
import { Action, RestAction } from "entities/Action";
export interface CreateActionRequest<T> extends APIRequest {
datasourceId: string;
@ -114,7 +114,7 @@ class ActionAPI extends API {
}
static createAPI(
apiConfig: RestAction,
apiConfig: Partial<Action>,
): AxiosPromise<ActionCreateUpdateResponse> {
return API.post(ActionAPI.url, apiConfig);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
import { OnboardingStep } from "constants/OnboardingConstants";
import React, { ReactNode } from "react";
import { useSelector } from "react-redux";
import { getCurrentStep, inOnboarding } from "sagas/OnboardingSagas";
type BoxedProps = {
// child nodes are not visible until this step is reached
step: OnboardingStep;
// Any additional conditions to hide the children
show?: boolean;
children: ReactNode;
};
// Boxed(or hidden).
const Boxed: React.FC<BoxedProps> = (props: BoxedProps) => {
const currentStep = useSelector(getCurrentStep);
const onboarding = useSelector(inOnboarding);
if (onboarding && currentStep < props.step && !props.show) {
return null;
}
return <>{props.children}</>;
};
Boxed.defaultProps = {
show: false,
};
export default Boxed;

View File

@ -0,0 +1,213 @@
import { Dialog, Icon } from "@blueprintjs/core";
import { IconWrapper } from "constants/IconConstants";
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
import { HeaderIcons } from "icons/HeaderIcons";
import AppInviteUsersForm from "pages/organization/AppInviteUsersForm";
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "store";
import styled from "styled-components";
import { ReactComponent as BookIcon } from "assets/icons/ads/app-icons/book.svg";
import { FormDialogComponent } from "../form/FormDialogComponent";
import { getCurrentOrgId } from "selectors/organizationSelectors";
import { getCurrentApplication } from "selectors/applicationSelectors";
import { getCurrentPageId } from "selectors/editorSelectors";
import { endOnboarding } from "actions/onboardingActions";
import { getQueryParams } from "utils/AppsmithUtils";
import { getOnboardingState } from "utils/storage";
const StyledDialog = styled(Dialog)`
&& {
width: 850px;
background-color: white;
}
`;
const ApplicationPublishedWrapper = styled.div`
padding: 33px;
`;
const Title = styled.div`
font-weight: bold;
font-size: 36px;
`;
const ContentWrapper = styled.div`
display: flex;
margin-top: 18px;
`;
const DescriptionWrapper = styled.div`
flex: 1;
font-size: 17px;
`;
const DescriptionTitle = styled.div`
font-weight: 500;
font-size: 17px;
`;
const DescriptionList = styled.div`
margin-top: 8px;
`;
const DescriptionItem = styled.div`
margin-top: 12px;
`;
const QuickLinksWrapper = styled.div`
margin-left: 83px;
color: #716e6e;
`;
const QuickLinksTitle = styled.div`
font-size: 14px;
`;
const QuickLinksItem = styled.div`
font-size: 17px;
border-bottom: 1px solid #716e6e;
display: flex;
align-items: center;
cursor: pointer;
margin-top: 13px;
.text {
margin-left: 3px;
}
`;
const StyledButton = styled.button`
color: white;
background-color: #f3672a;
font-weight: 600;
font-size: 17px;
padding: 12px 24px;
border: none;
cursor: pointer;
margin-top: 30px;
`;
const CompletionDialog = () => {
const [isOpen, setIsOpen] = useState(false);
const orgId = useSelector(getCurrentOrgId);
const currentApplication = useSelector(getCurrentApplication);
const pageId = useSelector(getCurrentPageId);
const dispatch = useDispatch();
useEffect(() => {
const params = getQueryParams();
const showCompletionDialog = async () => {
const inOnboarding = await getOnboardingState();
if (params.onboardingComplete && inOnboarding) {
setIsOpen(true);
}
};
showCompletionDialog();
}, []);
const onClose = () => {
setIsOpen(false);
dispatch(endOnboarding());
};
return (
<StyledDialog
isOpen={isOpen}
canOutsideClickClose={true}
canEscapeKeyClose={true}
onClose={onClose}
>
<ApplicationPublishedWrapper>
<Title>
<span role="img" aria-label="raising hands">
🙌
</span>{" "}
Youre Awesome!
</Title>
<ContentWrapper>
<DescriptionWrapper>
<DescriptionTitle>
Youve completed this tutorial. Heres a quick recap of things you
learnt -
</DescriptionTitle>
<DescriptionList>
<DescriptionItem>
<span role="img" aria-label="pointing right">
👉
</span>{" "}
Querying a database
</DescriptionItem>
<DescriptionItem>
<span role="img" aria-label="pointing right">
👉
</span>{" "}
Building UI using widgets.
</DescriptionItem>
<DescriptionItem>
<span role="img" aria-label="pointing right">
👉
</span>{" "}
Connecting widgets to queries using {"{{}}"} bindings
</DescriptionItem>
<DescriptionItem>
<span role="img" aria-label="pointing right">
👉
</span>{" "}
Deploying your application
</DescriptionItem>
</DescriptionList>
<StyledButton className="t--continue-on-my-own" onClick={onClose}>
Continue on my own
</StyledButton>
</DescriptionWrapper>
<QuickLinksWrapper>
<QuickLinksTitle>Quick Links:</QuickLinksTitle>
<QuickLinksItem
onClick={() =>
window.open("https://docs.appsmith.com/", "_blank")
}
>
<IconWrapper color="#716E6E" width={14} height={17}>
<BookIcon />
</IconWrapper>
<span className="text">Read our documentation</span>
</QuickLinksItem>
<FormDialogComponent
trigger={
<QuickLinksItem>
<HeaderIcons.SHARE color={"#716E6E"} width={14} height={17} />
<span className="text">Invite users to your app</span>
</QuickLinksItem>
}
canOutsideClickClose={true}
Form={AppInviteUsersForm}
orgId={orgId}
applicationId={currentApplication?.id}
title={
currentApplication
? currentApplication.name
: "Share Application"
}
/>
<QuickLinksItem
onClick={() => {
window.open(
DATA_SOURCES_EDITOR_URL(currentApplication?.id, pageId),
"_blank",
);
}}
>
<Icon icon="plus" color="#716E6E" iconSize={15} />
<span className="text">Connect your database</span>
</QuickLinksItem>
</QuickLinksWrapper>
</ContentWrapper>
</ApplicationPublishedWrapper>
</StyledDialog>
);
};
export default CompletionDialog;

View File

@ -0,0 +1,238 @@
import React, {
MutableRefObject,
ReactNode,
RefObject,
useEffect,
useRef,
useState,
} from "react";
import { Classes, Icon, Popover, Position } from "@blueprintjs/core";
import { useSelector } from "store";
import { getTooltipConfig } from "sagas/OnboardingSagas";
import { useDispatch } from "react-redux";
import styled from "styled-components";
import useClipboard from "utils/hooks/useClipboard";
import { endOnboarding, showTooltip } from "actions/onboardingActions";
import { Colors } from "constants/Colors";
import {
OnboardingStep,
OnboardingTooltip,
} from "constants/OnboardingConstants";
enum TooltipClassNames {
TITLE = "tooltip-title",
DESCRIPTION = "tooltip-description",
SKIP = "tooltip-skip",
ACTION = "tooltip-action",
SNIPPET = "tooltip-snippet",
}
const Wrapper = styled.div<{ isFinalStep: boolean }>`
width: 280px;
background-color: ${props => (props.isFinalStep ? "#F86A2B" : "#457ae6")};
color: white;
padding: 10px;
.${TooltipClassNames.TITLE} {
font-weight: 500;
font-size: 14px;
}
.${TooltipClassNames.DESCRIPTION} {
font-size: 12px;
margin-top: 8px;
}
.${TooltipClassNames.SKIP} {
font-size: 10px;
opacity: 0.7;
span {
text-decoration: underline;
cursor: pointer;
}
}
.${TooltipClassNames.SNIPPET} {
background-color: #2c59b4;
color: white;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0px;
position: relative;
cursor: pointer;
& > span {
padding: 6px;
}
& div.clipboard-message {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 1;
&.success {
background: #2c59b4;
}
&.error {
background: ${Colors.RED};
}
}
.${Classes.ICON} {
opacity: 0.7;
}
}
.${TooltipClassNames.ACTION} {
padding: 6px 10px;
cursor: pointer;
color: white;
border: none;
font-size: 12px;
background-color: #2c59b4;
}
`;
const Container = styled.div<{ isFinalStep: boolean }>`
div.${Classes.POPOVER_ARROW} {
display: block;
}
.bp3-popover-arrow-fill {
fill: ${props => (props.isFinalStep ? "#F86A2B" : "#457ae6")};
}
`;
const ActionWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
`;
type OnboardingToolTipProps = {
step: OnboardingStep[];
children: ReactNode;
show?: boolean;
position?: Position;
};
const OnboardingToolTip: React.FC<OnboardingToolTipProps> = (
props: OnboardingToolTipProps,
) => {
const [isOpen, setIsOpen] = useState(false);
const showingTooltip = useSelector(
state => state.ui.onBoarding.showingTooltip,
);
const popoverRef: RefObject<Popover> = useRef(null);
const tooltipConfig = useSelector(getTooltipConfig);
const { isFinalStep = false } = tooltipConfig;
useEffect(() => {
if (props.step.includes(showingTooltip) && props.show) {
setIsOpen(true);
} else {
setIsOpen(false);
}
if (popoverRef.current) {
popoverRef.current.reposition();
}
}, [props.step, props.show, showingTooltip, popoverRef]);
if (isOpen) {
return (
<Container className="t--onboarding-tooltip" isFinalStep={isFinalStep}>
<Popover
ref={popoverRef}
isOpen={true}
autoFocus={false}
enforceFocus={false}
boundary={"viewport"}
usePortal={false}
position={props.position || Position.TOP}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
flip: { enabled: false },
}}
>
{props.children}
<ToolTipContent details={tooltipConfig} />
</Popover>
</Container>
);
}
return <>{props.children}</>;
};
OnboardingToolTip.defaultProps = {
show: true,
};
type ToolTipContentProps = {
details: OnboardingTooltip;
};
const ToolTipContent = (props: ToolTipContentProps) => {
const dispatch = useDispatch();
const {
title,
description,
snippet,
action,
isFinalStep = false,
} = props.details;
const snippetRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const write = useClipboard(snippetRef);
const copyBindingToClipboard = () => {
snippet && write(snippet);
};
const finishOnboarding = () => {
dispatch(endOnboarding());
};
return (
<Wrapper isFinalStep={isFinalStep}>
<div className={TooltipClassNames.TITLE}>{title}</div>
<div className={TooltipClassNames.DESCRIPTION}>{description}</div>
{snippet && (
<div
className={TooltipClassNames.SNIPPET}
onClick={copyBindingToClipboard}
ref={snippetRef}
>
<span>{snippet}</span>
<Icon icon="duplicate" iconSize={14} color={Colors.WHITE} />
</div>
)}
<ActionWrapper>
<span className={TooltipClassNames.SKIP}>
Done? <span onClick={finishOnboarding}>Click here to End</span>
</span>
{action && (
<button
onClick={() => {
if (action.action) {
dispatch(action.action);
dispatch(showTooltip(action.action.payload));
return;
}
dispatch(showTooltip(OnboardingStep.NONE));
}}
className={TooltipClassNames.ACTION}
>
{action.label}
</button>
)}
</ActionWrapper>
</Wrapper>
);
};
export default OnboardingToolTip;

View File

@ -0,0 +1,129 @@
import { setCurrentStep } from "actions/onboardingActions";
import { ReduxAction, ReduxActionTypes } from "./ReduxActionConstants";
import { EventName } from "../utils/AnalyticsUtil";
export enum OnboardingStep {
NONE = -1,
WELCOME = 0,
EXAMPLE_DATABASE = 1,
ADD_WIDGET = 2,
SUCCESSFUL_BINDING = 3,
DEPLOY = 4,
}
export type OnboardingTooltip = {
title: string;
description: string;
action?: {
label: string;
action?: ReduxAction<OnboardingStep>;
};
snippet?: string;
isFinalStep?: boolean;
};
export type OnboardingStepConfig = {
setup: () => { type: string; payload?: any }[];
tooltip: OnboardingTooltip;
eventName?: EventName;
};
export const OnboardingConfig: Record<OnboardingStep, OnboardingStepConfig> = {
[OnboardingStep.NONE]: {
setup: () => {
return [];
},
tooltip: {
title: "",
description: "",
},
},
[OnboardingStep.WELCOME]: {
setup: () => {
// To setup the state if any
// Return action that needs to be dispatched
return [
{
type: ReduxActionTypes.SHOW_WELCOME,
},
];
},
tooltip: {
title: "",
description: "",
},
eventName: "ONBOARDING_WELCOME",
},
[OnboardingStep.EXAMPLE_DATABASE]: {
setup: () => {
return [
{
type: ReduxActionTypes.CREATE_ONBOARDING_DBQUERY_INIT,
},
{
type: ReduxActionTypes.LISTEN_FOR_ADD_WIDGET,
},
{
type: ReduxActionTypes.LISTEN_FOR_TABLE_WIDGET_BINDING,
},
];
},
tooltip: {
title: "Say hello to your example database",
description:
"Go ahead, check it out. You can also create a new query or connect to your own db as well.",
action: {
label: "Got It!",
},
},
eventName: "ONBOARDING_EXAMPLE_DATABASE",
},
[OnboardingStep.ADD_WIDGET]: {
setup: () => {
return [];
},
tooltip: {
title:
"Wohoo! Your first widget. 🎉 Go ahead and connect this to a Query",
description:
"Copy the example binding below and paste inside TableData input",
snippet: "{{ExampleQuery.data}}",
},
eventName: "ONBOARDING_ADD_WIDGET",
},
[OnboardingStep.SUCCESSFUL_BINDING]: {
setup: () => {
return [
{
type: ReduxActionTypes.LISTEN_FOR_WIDGET_UNSELECTION,
},
];
},
tooltip: {
title: "This table is now connected to Example Query",
description:
"You can connect properties to variables on Appsmith with {{ }} bindings",
action: {
label: "Next",
action: setCurrentStep(OnboardingStep.DEPLOY),
},
},
eventName: "ONBOARDING_SUCCESSFUL_BINDING",
},
[OnboardingStep.DEPLOY]: {
setup: () => {
return [
{
type: ReduxActionTypes.LISTEN_FOR_DEPLOY,
},
];
},
tooltip: {
title: "Youre almost done! Just Hit Deploy",
description:
"Deploying your apps is a crucial step to building on appsmith.",
isFinalStep: true,
},
eventName: "ONBOARDING_DEPLOY",
},
};

View File

@ -72,6 +72,15 @@ export const ReduxActionTypes: { [key: string]: string } = {
CANCEL_RUN_ACTION_CONFIRM_MODAL: "CANCEL_RUN_ACTION_CONFIRM_MODAL",
ACCEPT_RUN_ACTION_CONFIRM_MODAL: "ACCEPT_RUN_ACTION_CONFIRM_MODAL",
CREATE_QUERY_INIT: "CREATE_QUERY_INIT",
CREATE_ONBOARDING_ACTION_INIT: "CREATE_ONBOARDING_ACTION_INIT",
CREATE_ONBOARDING_ACTION_SUCCESS: "CREATE_ONBOARDING_ACTION_SUCCESS",
CREATE_ONBOARDING_DBQUERY_SUCCESS: "CREATE_ONBOARDING_DBQUERY_SUCCESS",
END_ONBOARDING: "END_ONBOARDING",
SET_CURRENT_STEP: "SET_CURRENT_STEP",
SET_ONBOARDING_STATE: "SET_ONBOARDING_STATE",
NEXT_ONBOARDING_STEP: "NEXT_ONBOARDING_STEP",
INCREMENT_STEP: "INCREMENT_STEP",
SHOW_WELCOME: "SHOW_WELCOME",
FETCH_DATASOURCES_INIT: "FETCH_DATASOURCES_INIT",
FETCH_DATASOURCES_SUCCESS: "FETCH_DATASOURCES_SUCCESS",
SAVE_DATASOURCE_NAME: "SAVE_DATASOURCE_NAME",
@ -92,6 +101,14 @@ export const ReduxActionTypes: { [key: string]: string } = {
TEST_DATASOURCE_SUCCESS: "TEST_DATASOURCE_SUCCESS",
DELETE_DATASOURCE_DRAFT: "DELETE_DATASOURCE_DRAFT",
UPDATE_DATASOURCE_DRAFT: "UPDATE_DATASOURCE_DRAFT",
SHOW_ONBOARDING_TOOLTIP: "SHOW_ONBOARDING_TOOLTIP",
SHOW_ONBOARDING_COMPLETION_DIALOG: "SHOW_ONBOARDING_COMPLETION_DIALOG",
CREATE_ONBOARDING_DBQUERY_INIT: "CREATE_ONBOARDING_DBQUERY_INIT",
ADD_WIDGET_COMPLETE: "ADD_WIDGET_COMPLETE",
LISTEN_FOR_ADD_WIDGET: "LISTEN_FOR_ADD_WIDGET",
LISTEN_FOR_WIDGET_UNSELECTION: "LISTEN_FOR_WIDGET_UNSELECTION",
LISTEN_FOR_DEPLOY: "LISTEN_FOR_DEPLOY",
LISTEN_FOR_TABLE_WIDGET_BINDING: "LISTEN_FOR_TABLE_WIDGET_BINDING",
FETCH_PUBLISHED_PAGE_INIT: "FETCH_PUBLISHED_PAGE_INIT",
FETCH_PUBLISHED_PAGE_SUCCESS: "FETCH_PUBLISHED_PAGE_SUCCESS",
DELETE_DATASOURCE_INIT: "DELETE_DATASOURCE_INIT",
@ -323,6 +340,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
DELETE_ACTION_ERROR: "DELETE_ACTION_ERROR",
RUN_ACTION_ERROR: "RUN_ACTION_ERROR",
EXECUTE_ACTION_ERROR: "EXECUTE_ACTION_ERROR",
CREATE_ONBOARDING_ACTION_ERROR: "CREATE_ONBOARDING_ACTION_ERROR",
FETCH_DATASOURCES_ERROR: "FETCH_DATASOURCES_ERROR",
SEARCH_APIORPROVIDERS_ERROR: "SEARCH_APIORPROVIDERS_ERROR",
UPDATE_DATASOURCE_ERROR: "UPDATE_DATASOURCE_ERROR",
@ -331,6 +349,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
DELETE_DATASOURCE_ERROR: "DELETE_DATASOURCE_ERROR",
FETCH_DATASOURCE_STRUCTURE_ERROR: "FETCH_DATASOURCE_STRUCTURE_ERROR",
REFRESH_DATASOURCE_STRUCTURE_ERROR: "REFRESH_DATASOURCE_STRUCTURE_ERROR",
CREATE_ONBOARDING_DBQUERY_ERROR: "CREATE_ONBOARDING_DBQUERY_ERROR",
FETCH_PUBLISHED_PAGE_ERROR: "FETCH_PUBLISHED_PAGE_ERROR",
PUBLISH_APPLICATION_ERROR: "PUBLISH_APPLICATION_ERROR",
FETCH_USER_DETAILS_ERROR: "FETCH_USER_DETAILS_ERROR",

View File

@ -15,6 +15,7 @@ import {
getCanvasWidgetDsl,
getCurrentPageName,
} from "selectors/editorSelectors";
import OnboardingCompletionDialog from "components/editorComponents/Onboarding/CompletionDialog";
import ConfirmRunModal from "pages/Editor/ConfirmRunModal";
import { getCurrentApplication } from "selectors/applicationSelectors";
import {
@ -114,6 +115,7 @@ class AppViewerPageContainer extends Component<AppViewerPageContainerProps> {
pageName={this.props.currentPageName}
/>
<ConfirmRunModal />
<OnboardingCompletionDialog />
</Section>
);
}

View File

@ -25,6 +25,8 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { convertArrayToSentence } from "utils/helpers";
import BackButton from "./BackButton";
import { PluginType } from "entities/Action";
import Boxed from "components/editorComponents/Onboarding/Boxed";
import { OnboardingStep } from "constants/OnboardingConstants";
const { cloudHosting } = getAppsmithConfigs();
@ -60,7 +62,7 @@ const DBForm = styled.div`
padding: 20px;
margin-left: 10px;
margin-right: 0px;
max-height: 93vh;
height: calc(100vh - ${props => props.theme.headerHeight});
overflow: auto;
.backBtn {
padding-bottom: 1px;
@ -337,20 +339,22 @@ class DatasourceDBEditor extends React.Component<
<FormTitle focusOnMount={this.props.isNewDatasource} />
</FormTitleContainer>
{viewMode && (
<ActionButton
className="t--edit-datasource"
text="EDIT"
accent="secondary"
onClick={() => {
this.props.setDatasourceEditorMode(
this.props.datasourceId,
false,
);
}}
/>
<Boxed step={OnboardingStep.SUCCESSFUL_BINDING}>
<ActionButton
className="t--edit-datasource"
text="EDIT"
accent="secondary"
onClick={() => {
this.props.setDatasourceEditorMode(
this.props.datasourceId,
false,
);
}}
/>
</Boxed>
)}
</Header>
{cloudHosting && pluginType === PluginType.DB && (
{cloudHosting && pluginType === PluginType.DB && !viewMode && (
<CollapsibleWrapper>
<CollapsibleHelp>
<span>{`Whitelist the IP ${convertArrayToSentence(

View File

@ -14,6 +14,7 @@ import { getDataTree } from "selectors/dataTreeSelectors";
import { isNameValid } from "utils/helpers";
import { saveDatasourceName } from "actions/datasourceActions";
import { Spinner } from "@blueprintjs/core";
import { getCurrentStep, inOnboarding } from "sagas/OnboardingSagas";
const Wrapper = styled.div`
margin-left: 10px;
@ -52,6 +53,14 @@ const FormTitle = (props: FormTitleProps) => {
};
});
// For onboarding
const hideEditIcon = useSelector((state: AppState) => {
const currentStep = getCurrentStep(state);
const isInOnboarding = inOnboarding(state);
return isInOnboarding && currentStep < 3;
});
const hasNameConflict = React.useCallback(
(name: string) => {
const datasourcesNames: Record<string, any> = {};
@ -104,13 +113,14 @@ const FormTitle = (props: FormTitleProps) => {
<EditableText
className="t--edit-datasource-name"
type="text"
hideEditIcon={hideEditIcon}
forceDefault={forceUpdate}
defaultValue={currentDatasource ? currentDatasource.name : ""}
isInvalid={isInvalidDatasourceName}
onTextChanged={handleDatasourceNameChange}
placeholder="Datasource Name"
editInteractionKind={EditInteractionKind.SINGLE}
isEditingDefault={props.focusOnMount}
isEditingDefault={props.focusOnMount && !hideEditIcon}
updating={saveStatus.isSaving}
/>
{saveStatus.isSaving && <Spinner size={16} />}

View File

@ -135,7 +135,6 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const formData = getFormValues(DATASOURCE_DB_FORM)(state) as Datasource;
const pluginId = _.get(datasource, "pluginId", "");
const plugin = getPlugin(state, pluginId);
return {
pluginImages: getPluginImages(state),
formData,

View File

@ -36,6 +36,11 @@ import {
getIsSavingAppName,
} from "selectors/applicationSelectors";
import EditableTextWrapper from "components/ads/EditableTextWrapper";
import Boxed from "components/editorComponents/Onboarding/Boxed";
import OnboardingToolTip from "components/editorComponents/Onboarding/Tooltip";
import { Position } from "@blueprintjs/core";
import { inOnboarding } from "sagas/OnboardingSagas";
import { OnboardingStep } from "constants/OnboardingConstants";
const HeaderWrapper = styled(StyledHeader)`
background: ${Colors.BALTIC_SEA};
@ -134,6 +139,7 @@ type EditorHeaderProps = {
applicationId?: string;
currentApplication?: ApplicationPayload;
isSaving: boolean;
isInOnboarding: boolean;
publishApplication: (appId: string) => void;
};
@ -148,6 +154,7 @@ export const EditorHeader = (props: EditorHeaderProps) => {
applicationId,
pageName,
publishApplication,
isInOnboarding,
} = props;
const dispatch = useDispatch();
@ -209,93 +216,109 @@ export const EditorHeader = (props: EditorHeaderProps) => {
/>
</Link>
</HeaderSection>
<HeaderSection flex-direction={"row"}>
{currentApplication ? (
<EditableTextWrapper
variant="UNDERLINE"
defaultValue={currentApplication.name || ""}
editInteractionKind={EditInteractionKind.SINGLE}
hideEditIcon={true}
className="t--application-name"
fill={false}
savingState={
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
}
isNewApp={
applicationList.filter(el => el.id === applicationId).length > 0
}
onBlur={(value: string) =>
updateApplicationDispatch(applicationId || "", {
name: value,
currentApp: true,
})
}
/>
) : null}
{/* <PageName>{pageName}&nbsp;</PageName> */}
</HeaderSection>
<HeaderSection>
<SaveStatusContainer className={"t--save-status-container"}>
{saveStatusIcon}
</SaveStatusContainer>
<ShareButton
target="_blank"
href="https://mail.google.com/mail/u/0/?view=cm&fs=1&to=feedback@appsmith.com&tf=1"
text="Feedback"
intent="none"
outline
size="small"
className="t--application-feedback-btn"
icon={
<HeaderIcons.FEEDBACK color={Colors.WHITE} width={13} height={13} />
}
/>
<FormDialogComponent
trigger={
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
<Boxed step={OnboardingStep.DEPLOY}>
<HeaderSection flex-direction={"row"}>
{currentApplication ? (
<EditableTextWrapper
variant="UNDERLINE"
defaultValue={currentApplication.name || ""}
editInteractionKind={EditInteractionKind.SINGLE}
hideEditIcon={true}
className="t--application-name"
fill={false}
savingState={
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
}
isNewApp={
!isInOnboarding &&
applicationList.filter(el => el.id === applicationId).length > 0
}
onBlur={(value: string) =>
updateApplicationDispatch(applicationId || "", {
name: value,
currentApp: true,
})
}
/>
}
canOutsideClickClose={true}
Form={AppInviteUsersForm}
orgId={orgId}
applicationId={applicationId}
title={
currentApplication ? currentApplication.name : "Share Application"
}
/>
<DeploySection>
<DeployButton
onClick={handlePublish}
text="Deploy"
loading={isPublishing}
intent="primary"
filled
) : null}
{/* <PageName>{pageName}&nbsp;</PageName> */}
</HeaderSection>
<HeaderSection>
<SaveStatusContainer className={"t--save-status-container"}>
{saveStatusIcon}
</SaveStatusContainer>
<ShareButton
target="_blank"
href="https://mail.google.com/mail/u/0/?view=cm&fs=1&to=feedback@appsmith.com&tf=1"
text="Feedback"
intent="none"
outline
size="small"
className="t--application-publish-btn"
className="t--application-feedback-btn"
icon={
<HeaderIcons.DEPLOY color={Colors.WHITE} width={13} height={13} />
<HeaderIcons.FEEDBACK
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
<DeployLinkButtonDialog
<FormDialogComponent
trigger={
<DeployLinkButton icon="caret-down" filled intent="primary" />
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
}
canOutsideClickClose={true}
Form={AppInviteUsersForm}
orgId={orgId}
applicationId={applicationId}
title={
currentApplication ? currentApplication.name : "Share Application"
}
link={getApplicationViewerPageURL(applicationId, pageId)}
/>
</DeploySection>
</HeaderSection>
<DeploySection>
<OnboardingToolTip
step={[OnboardingStep.DEPLOY]}
position={Position.BOTTOM_RIGHT}
>
<DeployButton
onClick={handlePublish}
text="Deploy"
loading={isPublishing}
intent="primary"
filled
size="small"
className="t--application-publish-btn"
icon={
<HeaderIcons.DEPLOY
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
</OnboardingToolTip>
<DeployLinkButtonDialog
trigger={
<DeployLinkButton icon="caret-down" filled intent="primary" />
}
link={getApplicationViewerPageURL(applicationId, pageId)}
/>
</DeploySection>
</HeaderSection>
</Boxed>
<HelpModal page={"Editor"} />
</HeaderWrapper>
);
@ -309,6 +332,7 @@ const mapStateToProps = (state: AppState) => ({
currentApplication: state.ui.applications.currentApplication,
isPublishing: getIsPublishingApplication(state),
pageId: getCurrentPageId(state),
isInOnboarding: inOnboarding(state),
});
const mapDispatchToProps = (dispatch: any) => ({

View File

@ -97,8 +97,9 @@ export const getPluginGroups = (
datasources: Datasource[],
plugins: Plugin[],
searchKeyword?: string,
actionPluginMap = ACTION_PLUGIN_MAP,
) => {
return ACTION_PLUGIN_MAP?.map((config?: ActionGroupConfig) => {
return actionPluginMap?.map((config?: ActionGroupConfig) => {
if (!config) return null;
const entries = actions?.filter(
@ -108,6 +109,7 @@ export const getPluginGroups = (
const filteredPlugins = plugins.filter(
plugin => plugin.type === config.type,
);
const filteredPluginIds = filteredPlugins.map(plugin => plugin.id);
const filteredDatasources = datasources.filter(datasource => {
return filteredPluginIds.includes(datasource.pluginId);

View File

@ -0,0 +1,112 @@
import React, { useRef, MutableRefObject, useCallback, useEffect } from "react";
import styled from "styled-components";
import Divider from "components/editorComponents/Divider";
import {
useFilteredEntities,
useWidgets,
useActions,
useFilteredDatasources,
} from "./hooks";
import Search from "./ExplorerSearch";
import ExplorerPageGroup from "./Pages/PageGroup";
import { scrollbarDark } from "constants/DefaultTheme";
import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core";
import WidgetSidebar from "../WidgetSidebar";
import { BUILDER_PAGE_URL } from "constants/routes";
import history from "utils/history";
import { useParams } from "react-router";
import { ExplorerURLParams } from "./helpers";
import JSDependencies from "./JSDependencies";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { useSelector } from "react-redux";
import { getPlugins } from "selectors/entitiesSelector";
const Wrapper = styled.div`
height: 100%;
overflow-y: scroll;
${scrollbarDark};
`;
const NoResult = styled(NonIdealState)`
&.${Classes.NON_IDEAL_STATE} {
height: auto;
}
`;
const StyledDivider = styled(Divider)`
border-bottom-color: rgba(255, 255, 255, 0.1);
`;
const EntityExplorer = (props: IPanelProps) => {
const { applicationId } = useParams<ExplorerURLParams>();
const searchInputRef: MutableRefObject<HTMLInputElement | null> = useRef(
null,
);
PerformanceTracker.startTracking(PerformanceTransactionName.ENTITY_EXPLORER);
useEffect(() => {
PerformanceTracker.stopTracking();
});
const explorerRef = useRef<HTMLDivElement | null>(null);
const { searchKeyword, clearSearch } = useFilteredEntities(searchInputRef);
const datasources = useFilteredDatasources(searchKeyword);
const plugins = useSelector(getPlugins);
const widgets = useWidgets(searchKeyword);
const actions = useActions(searchKeyword);
let noResults = false;
if (searchKeyword) {
const noWidgets = Object.values(widgets).filter(Boolean).length === 0;
const noActions =
Object.values(actions).filter(actions => actions && actions.length > 0)
.length === 0;
const noDatasource =
Object.values(datasources).filter(
datasources => datasources && datasources.length > 0,
).length === 0;
noResults = noWidgets && noActions && noDatasource;
}
const { openPanel } = props;
const showWidgetsSidebar = useCallback(
(pageId: string) => {
history.push(BUILDER_PAGE_URL(applicationId, pageId));
openPanel({ component: WidgetSidebar });
},
[openPanel, applicationId],
);
return (
<Wrapper ref={explorerRef}>
<Search ref={searchInputRef} clear={clearSearch} />
<ExplorerPageGroup
searchKeyword={searchKeyword}
step={0}
widgets={widgets}
actions={actions}
datasources={datasources}
plugins={plugins}
showWidgetsSidebar={showWidgetsSidebar}
/>
{noResults && (
<NoResult
className={Classes.DARK}
description="Try modifying the search keyword."
title="No entities found"
icon="search"
/>
)}
<StyledDivider />
<JSDependencies />
</Wrapper>
);
};
EntityExplorer.displayName = "EntityExplorer";
EntityExplorer.whyDidYouRender = {
logOnDifferentValues: false,
};
export default EntityExplorer;

View File

@ -0,0 +1,57 @@
import { PluginType } from "entities/Action";
import React from "react";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
import { getPlugins } from "selectors/entitiesSelector";
import styled from "styled-components";
import { getPluginGroups, ACTION_PLUGIN_MAP } from "../Actions/helpers";
import { useActions, useFilteredDatasources } from "../hooks";
const AddWidget = styled.button`
margin: 25px 0px;
padding: 6px 38px;
background-color: transparent;
border: 1px solid #f3672a;
color: #f3672a;
font-weight: bold;
cursor: pointer;
`;
const Wrapper = styled.div`
display: flex;
justify-content: center;
`;
const DBQueryGroup = (props: any) => {
const pages = useSelector((state: AppState) => {
return state.entities.pageList.pages;
});
const currentPage = pages[0];
const actions = useActions("");
const datasources = useFilteredDatasources("");
const plugins = useSelector(getPlugins);
const dbPluginMap = ACTION_PLUGIN_MAP.filter(
plugin => plugin?.type === PluginType.DB,
);
return (
<>
<Wrapper>
<AddWidget className="t--add-widget" onClick={props.showWidgetsSidebar}>
Add Widget
</AddWidget>
</Wrapper>
{getPluginGroups(
currentPage,
0,
actions[currentPage.pageId] || [],
datasources[currentPage.pageId] || [],
plugins,
"",
dbPluginMap,
)}
</>
);
};
export default DBQueryGroup;

View File

@ -0,0 +1,21 @@
import React from "react";
import styled from "styled-components";
import { Classes } from "@blueprintjs/core";
const SkeletonRows = styled.div<{ size: number }>`
height: 20px;
width: ${props => props.size}%;
margin-top: 12px;
`;
const Loading = () => {
return (
<>
<SkeletonRows size={90} className={Classes.SKELETON} />
<SkeletonRows size={60} className={Classes.SKELETON} />
<SkeletonRows size={30} className={Classes.SKELETON} />
</>
);
};
export default Loading;

View File

@ -0,0 +1,40 @@
import React, { useCallback } from "react";
import styled from "styled-components";
import { scrollbarDark } from "constants/DefaultTheme";
import Loading from "./Loading";
import DBQueryGroup from "./DBQueryGroup";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
import { IPanelProps } from "@blueprintjs/core";
import { BUILDER_PAGE_URL } from "constants/routes";
import WidgetSidebar from "pages/Editor/WidgetSidebar";
import { useParams } from "react-router";
import { ExplorerURLParams } from "../helpers";
import history from "utils/history";
const Wrapper = styled.div`
height: 100%;
overflow-y: scroll;
${scrollbarDark};
`;
const OnboardingExplorer = (props: IPanelProps) => {
let node = <Loading />;
const { applicationId, pageId } = useParams<ExplorerURLParams>();
const { openPanel } = props;
const showWidgetsSidebar = useCallback(() => {
history.push(BUILDER_PAGE_URL(applicationId, pageId));
openPanel({ component: WidgetSidebar });
}, [openPanel, applicationId, pageId]);
const createdDBQuery = useSelector(
(state: AppState) => state.ui.onBoarding.createdDBQuery,
);
if (createdDBQuery) {
node = <DBQueryGroup showWidgetsSidebar={showWidgetsSidebar} />;
}
return <Wrapper>{node}</Wrapper>;
};
export default OnboardingExplorer;

View File

@ -12,6 +12,8 @@ import ExplorerDatasourceEntity from "../Datasources/DatasourceEntity";
import Entity from "../Entity";
import EntityPlaceholder from "../Entity/Placeholder";
import { ExplorerURLParams } from "../helpers";
import OnboardingTooltip from "components/editorComponents/Onboarding/Tooltip";
import { OnboardingStep } from "constants/OnboardingConstants";
type ExplorerPluginGroupProps = {
step: number;
@ -82,16 +84,21 @@ const ExplorerPluginGroup = memo((props: ExplorerPluginGroupProps) => {
config={props.actionConfig}
plugins={pluginGroups}
/>
{props.datasources.map((datasource: Datasource) => {
{props.datasources.map((datasource: Datasource, index: number) => {
return (
<ExplorerDatasourceEntity
plugin={pluginGroups[datasource.pluginId]}
<OnboardingTooltip
step={[OnboardingStep.EXAMPLE_DATABASE]}
key={datasource.id}
datasource={datasource}
step={props.step + 1}
searchKeyword={props.searchKeyword}
pageId={props.page.pageId}
/>
show={index === 0}
>
<ExplorerDatasourceEntity
plugin={pluginGroups[datasource.pluginId]}
datasource={datasource}
step={props.step + 1}
searchKeyword={props.searchKeyword}
pageId={props.page.pageId}
/>
</OnboardingTooltip>
);
})}
</>

View File

@ -1,112 +1,19 @@
import React, { useRef, MutableRefObject, useCallback, useEffect } from "react";
import styled from "styled-components";
import Divider from "components/editorComponents/Divider";
import {
useFilteredEntities,
useWidgets,
useActions,
useFilteredDatasources,
} from "./hooks";
import Search from "./ExplorerSearch";
import ExplorerPageGroup from "./Pages/PageGroup";
import { scrollbarDark } from "constants/DefaultTheme";
import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core";
import WidgetSidebar from "../WidgetSidebar";
import { BUILDER_PAGE_URL } from "constants/routes";
import history from "utils/history";
import { useParams } from "react-router";
import { ExplorerURLParams } from "./helpers";
import JSDependencies from "./JSDependencies";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { IPanelProps } from "@blueprintjs/core";
import React from "react";
import { useSelector } from "react-redux";
import { getPlugins } from "selectors/entitiesSelector";
import { inOnboarding, isAddWidgetComplete } from "sagas/OnboardingSagas";
import EntityExplorer from "./EntityExplorer";
import OnboardingExplorer from "./Onboarding";
const Wrapper = styled.div`
height: 100%;
overflow-y: scroll;
${scrollbarDark};
`;
const ExplorerContent = (props: IPanelProps) => {
const isInOnboarding = useSelector(inOnboarding);
const addWidgetComplete = useSelector(isAddWidgetComplete);
const NoResult = styled(NonIdealState)`
&.${Classes.NON_IDEAL_STATE} {
height: auto;
if (isInOnboarding && !addWidgetComplete) {
return <OnboardingExplorer {...props} />;
}
`;
const StyledDivider = styled(Divider)`
border-bottom-color: rgba(255, 255, 255, 0.1);
`;
const EntityExplorer = (props: IPanelProps) => {
const { applicationId, pageId } = useParams<ExplorerURLParams>();
const searchInputRef: MutableRefObject<HTMLInputElement | null> = useRef(
null,
);
PerformanceTracker.startTracking(PerformanceTransactionName.ENTITY_EXPLORER);
useEffect(() => {
PerformanceTracker.stopTracking();
});
const explorerRef = useRef<HTMLDivElement | null>(null);
const { searchKeyword, clearSearch } = useFilteredEntities(searchInputRef);
const datasources = useFilteredDatasources(searchKeyword);
const plugins = useSelector(getPlugins);
const widgets = useWidgets(searchKeyword);
const actions = useActions(searchKeyword);
let noResults = false;
if (searchKeyword) {
const noWidgets = Object.values(widgets).filter(Boolean).length === 0;
const noActions =
Object.values(actions).filter(actions => actions && actions.length > 0)
.length === 0;
const noDatasource =
Object.values(datasources).filter(
datasources => datasources && datasources.length > 0,
).length === 0;
noResults = noWidgets && noActions && noDatasource;
}
const { openPanel } = props;
const showWidgetsSidebar = useCallback(
(pageId: string) => {
history.push(BUILDER_PAGE_URL(applicationId, pageId));
openPanel({ component: WidgetSidebar });
},
[openPanel, applicationId],
);
return (
<Wrapper ref={explorerRef}>
<Search ref={searchInputRef} clear={clearSearch} />
<ExplorerPageGroup
searchKeyword={searchKeyword}
step={0}
widgets={widgets}
actions={actions}
datasources={datasources}
plugins={plugins}
showWidgetsSidebar={showWidgetsSidebar}
/>
{noResults && (
<NoResult
className={Classes.DARK}
description="Try modifying the search keyword."
title="No entities found"
icon="search"
/>
)}
<StyledDivider />
<JSDependencies />
</Wrapper>
);
return <EntityExplorer {...props} />;
};
EntityExplorer.displayName = "EntityExplorer";
EntityExplorer.whyDidYouRender = {
logOnDifferentValues: false,
};
export default EntityExplorer;
export default ExplorerContent;

View File

@ -15,6 +15,9 @@ import {
isPathADynamicProperty,
isPathADynamicTrigger,
} from "../../../utils/DynamicBindingUtils";
import OnboardingToolTip from "components/editorComponents/Onboarding/Tooltip";
import { Position } from "@blueprintjs/core";
import { OnboardingStep } from "constants/OnboardingConstants";
type Props = {
widgetProperties: WidgetProps;
@ -113,13 +116,22 @@ const PropertyControl = (props: Props) => {
</JSToggleButton>
)}
</ControlPropertyLabelContainer>
{PropertyControlFactory.createControl(
config,
{
onPropertyChange: onPropertyChange,
},
isDynamic,
)}
<OnboardingToolTip
step={[
OnboardingStep.ADD_WIDGET,
OnboardingStep.SUCCESSFUL_BINDING,
]}
show={propertyName === "tableData"}
position={Position.LEFT_TOP}
>
{PropertyControlFactory.createControl(
config,
{
onPropertyChange: onPropertyChange,
},
isDynamic,
)}
</OnboardingToolTip>
</ControlWrapper>
);
} catch (e) {

View File

@ -84,7 +84,7 @@ class TemplateMenu extends React.Component<Props> {
onClick={() => createTemplate("")}
>
<div style={{ fontSize: 14 }}>
Press enter to start with a blank state or select a template.
Click here to start with a blank state or select a template.
</div>
<div style={{ marginTop: "6px" }}>
{Object.entries(pluginTemplates).map(template => {

View File

@ -0,0 +1,139 @@
import React from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import Spinner from "components/ads/Spinner";
import { Classes } from "components/ads/common";
import { AppState } from "reducers";
import { endOnboarding, setCurrentStep } from "actions/onboardingActions";
const Wrapper = styled.div`
height: 100%;
padding: 85px 55px;
flex: 1;
display: flex;
`;
const Container = styled.div`
align-self: stretch;
flex: 1;
display: flex;
background-color: white;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 80px 0px;
`;
const WelcomeText = styled.div`
font-size: 36px;
font-weight: bold;
color: #090707;
text-align: center;
`;
const Description = styled.div`
font-size: 17px;
color: #716e6e;
margin-top: 16px;
text-align: center;
`;
const NotNewUserText = styled.span`
color: #716e6e;
margin-top: 24px;
text-align: center;
span {
color: #457ae6;
cursor: pointer;
}
`;
const StyledButton = styled.button`
color: white;
background-color: #f3672a;
font-weight: bold;
font-size: 17px;
padding: 12px 24px;
border: none;
cursor: pointer;
`;
const LoadingContainer = styled(Container)`
justify-content: center;
padding: 0px;
.${Classes.SPINNER} {
width: 43px;
height: 43px;
circle {
stroke: #f3672a;
}
}
span {
font-size: 17px;
margin-top: 23px;
}
`;
const Welcome = () => {
const dispatch = useDispatch();
const creatingDatabase = useSelector(
(state: AppState) => state.ui.onBoarding.creatingDatabase,
);
if (creatingDatabase) {
return (
<Wrapper>
<LoadingContainer>
<Spinner />
<span>Creating Example Database</span>
</LoadingContainer>
</Wrapper>
);
}
return (
<Wrapper>
<Container>
<div>
<WelcomeText>
<span role="img" aria-label="hello">
👋
</span>{" "}
Welcome
</WelcomeText>
<Description>
Appsmith helps you build quality internal tools, fast!
</Description>
</div>
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column",
}}
>
<StyledButton
className="t--create-database"
onClick={() => {
dispatch(setCurrentStep(1));
}}
>
Explore Appsmith
</StyledButton>
<NotNewUserText>
Not your first time with Appsmith?{" "}
<span onClick={() => dispatch(endOnboarding())}>
Skip this tutorial
</span>
</NotNewUserText>
</div>
</Container>
</Wrapper>
);
};
export default Welcome;

View File

@ -11,6 +11,8 @@ import ExplorerSearch from "./Explorer/ExplorerSearch";
import { debounce } from "lodash";
import produce from "immer";
import { WIDGET_SIDEBAR_CAPTION } from "constants/messages";
import Boxed from "components/editorComponents/Onboarding/Boxed";
import { OnboardingStep } from "constants/OnboardingConstants";
const MainWrapper = styled.div`
text-transform: capitalize;
@ -143,10 +145,18 @@ const WidgetSidebar = (props: IPanelProps) => {
</Header>
{groups.map((group: string) => (
<React.Fragment key={group}>
<h5>{group}</h5>
<Boxed step={OnboardingStep.ADD_WIDGET}>
<h5>{group}</h5>
</Boxed>
<CardsWrapper>
{filteredCards[group].map((card: WidgetCardProps) => (
<WidgetCard details={card} key={card.key} />
<Boxed
step={OnboardingStep.ADD_WIDGET}
show={card.type === "TABLE_WIDGET"}
key={card.key}
>
<WidgetCard details={card} />
</Boxed>
))}
</CardsWrapper>
</React.Fragment>

View File

@ -2,6 +2,7 @@ import React, { useEffect, ReactNode, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import styled from "styled-components";
import Canvas from "./Canvas";
import Welcome from "./Welcome";
import {
getIsFetchingPage,
getCurrentPageId,
@ -21,6 +22,7 @@ import { fetchPage } from "actions/pageActions";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { AppState } from "reducers";
import { getCurrentApplication } from "selectors/applicationSelectors";
const EditorWrapper = styled.div`
@ -60,6 +62,9 @@ const WidgetsEditor = () => {
const currentPageId = useSelector(getCurrentPageId);
const currentPageName = useSelector(getCurrentPageName);
const currentApp = useSelector(getCurrentApplication);
const showWelcomeScreen = useSelector(
(state: AppState) => state.ui.onBoarding.showWelcomeScreen,
);
useEffect(() => {
PerformanceTracker.stopTracking(PerformanceTransactionName.EDITOR_MOUNT);
@ -112,6 +117,11 @@ const WidgetsEditor = () => {
if (!isFetchingPage && widgets) {
node = <Canvas dsl={widgets} />;
}
if (showWelcomeScreen) {
return <Welcome />;
}
log.debug("Canvas rendered");
PerformanceTracker.stopTracking();
return (

View File

@ -2,10 +2,7 @@ import React, { Component } from "react";
import { Helmet } from "react-helmet";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import {
BuilderRouteParams,
getApplicationViewerPageURL,
} from "constants/routes";
import { BuilderRouteParams } from "constants/routes";
import { AppState } from "reducers";
import MainContainer from "./MainContainer";
import { DndProvider } from "react-dnd";
@ -18,14 +15,7 @@ import {
getIsPublishingApplication,
getPublishingError,
} from "selectors/editorSelectors";
import {
AnchorButton,
Classes,
Dialog,
Hotkey,
Hotkeys,
Spinner,
} from "@blueprintjs/core";
import { Hotkey, Hotkeys, Spinner } from "@blueprintjs/core";
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
import { initEditor } from "actions/initActions";
import { editorInitializer } from "utils/EditorUtils";
@ -90,7 +80,7 @@ class Editor extends Component<Props> {
combo="mod + c"
label="Copy Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
this.props.copySelectedWidget();
}}
preventDefault
@ -101,7 +91,7 @@ class Editor extends Component<Props> {
combo="mod + v"
label="Paste Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
this.props.pasteCopiedWidget();
}}
preventDefault
@ -112,7 +102,7 @@ class Editor extends Component<Props> {
combo="del"
label="Delete Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
if (!isMac()) this.props.deleteSelectedWidget();
}}
preventDefault
@ -123,7 +113,7 @@ class Editor extends Component<Props> {
combo="backspace"
label="Delete Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
if (isMac()) this.props.deleteSelectedWidget();
}}
preventDefault
@ -134,7 +124,7 @@ class Editor extends Component<Props> {
combo="del"
label="Delete Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
this.props.deleteSelectedWidget();
}}
preventDefault
@ -145,7 +135,7 @@ class Editor extends Component<Props> {
combo="mod + x"
label="Cut Widget"
group="Canvas"
onKeyDown={(e: any) => {
onKeyDown={() => {
this.props.cutSelectedWidget();
}}
preventDefault
@ -155,7 +145,6 @@ class Editor extends Component<Props> {
);
}
public state = {
isDialogOpen: false,
registered: false,
};
@ -168,21 +157,8 @@ class Editor extends Component<Props> {
this.props.initEditor(applicationId, pageId);
}
}
componentDidUpdate(previously: Props) {
if (
previously.isPublishing &&
!(this.props.isPublishing || this.props.errorPublishing)
) {
this.setState({
isDialogOpen: true,
});
}
}
shouldComponentUpdate(
nextProps: Props,
nextState: { isDialogOpen: boolean; registered: boolean },
) {
shouldComponentUpdate(nextProps: Props, nextState: { registered: boolean }) {
return (
nextProps.currentPageId !== this.props.currentPageId ||
nextProps.currentApplicationId !== this.props.currentApplicationId ||
@ -192,16 +168,10 @@ class Editor extends Component<Props> {
nextProps.errorPublishing !== this.props.errorPublishing ||
nextProps.isEditorInitializeError !==
this.props.isEditorInitializeError ||
nextState.isDialogOpen !== this.state.isDialogOpen ||
nextState.registered !== this.state.registered
);
}
handleDialogClose = () => {
this.setState({
isDialogOpen: false,
});
};
public render() {
if (!this.props.isEditorInitialized || !this.state.registered) {
return (
@ -223,32 +193,6 @@ class Editor extends Component<Props> {
<title>Editor | Appsmith</title>
</Helmet>
<MainContainer />
<Dialog
isOpen={this.state.isDialogOpen}
canOutsideClickClose={true}
canEscapeKeyClose={true}
title="Application Published"
onClose={this.handleDialogClose}
icon="tick-circle"
>
<div className={Classes.DIALOG_BODY}>
<p>
{"Your application is now published with the current changes!"}
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<AnchorButton
target={this.props.currentApplicationId}
href={getApplicationViewerPageURL(
this.props.currentApplicationId,
this.props.currentPageId,
)}
text="View Application"
/>
</div>
</div>
</Dialog>
</div>
<ConfirmRunModal />
</DndProvider>

View File

@ -53,6 +53,7 @@ import { AppState } from "reducers";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { setOnboardingState } from "utils/storage";
const {
enableGithubOAuth,
enableGoogleOAuth,
@ -160,6 +161,7 @@ export const SignUp = (props: SignUpFormProps) => {
PerformanceTracker.startTracking(
PerformanceTransactionName.SIGN_UP,
);
setOnboardingState(true);
}}
/>
</FormActions>

View File

@ -10,6 +10,7 @@ import { useLocation } from "react-router-dom";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { setOnboardingState } from "utils/storage";
const ThirdPartyAuthWrapper = styled.div`
display: flex;
@ -86,6 +87,9 @@ const SocialLoginButton = (props: {
let eventName: EventName = "LOGIN_CLICK";
if (props.type === "SIGNUP") {
eventName = "SIGNUP_CLICK";
// Set onboarding flag on signup
setOnboardingState(true);
}
PerformanceTracker.startTracking(
eventName === "SIGNUP_CLICK"

View File

@ -397,6 +397,38 @@ const actionsReducer = createReducer(initialState, {
return action;
});
},
[ReduxActionTypes.CREATE_ONBOARDING_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
): ActionDataState =>
state.map(a => {
if (
a.config.pageId === action.payload.pageId &&
a.config.id === action.payload.name
) {
return { ...a, config: action.payload };
}
return a;
}),
[ReduxActionTypes.CREATE_ONBOARDING_ACTION_INIT]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
): ActionDataState =>
state.concat([
{
config: { ...action.payload, id: action.payload.name },
isLoading: false,
},
]),
[ReduxActionTypes.CREATE_ONBOARDING_ACTION_ERROR]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
): ActionDataState =>
state.filter(
a =>
a.config.name !== action.payload.name &&
a.config.id !== action.payload.name,
),
});
export default actionsReducer;

View File

@ -38,6 +38,7 @@ import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer";
import { EvaluatedTreeState } from "./evalutationReducers/treeReducer";
import { EvaluationDependencyState } from "./evalutationReducers/dependencyReducer";
import { PageWidgetsReduxState } from "./uiReducers/pageWidgetsReducer";
import { OnboardingState } from "./uiReducers/onBoardingReducer";
const appReducer = combineReducers({
entities: entityReducer,
@ -74,6 +75,7 @@ export interface AppState {
confirmRunAction: ConfirmRunActionReduxState;
datasourceName: DatasourceNameReduxState;
theme: ThemeState;
onBoarding: OnboardingState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;

View File

@ -23,6 +23,7 @@ import themeReducer from "./themeReducer";
import datasourceNameReducer from "./datasourceNameReducer";
import pageCanvasStructureReducer from "./pageCanvasStructure";
import pageWidgetsReducer from "./pageWidgetsReducer";
import onBoardingReducer from "./onBoardingReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -49,5 +50,6 @@ const uiReducer = combineReducers({
pageWidgets: pageWidgetsReducer,
theme: themeReducer,
confirmRunAction: confirmRunActionReducer,
onBoarding: onBoardingReducer,
});
export default uiReducer;

View File

@ -0,0 +1,100 @@
import { OnboardingStep } from "constants/OnboardingConstants";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { createReducer } from "utils/AppsmithUtils";
const initialState: OnboardingState = {
currentStep: OnboardingStep.NONE,
showWelcomeScreen: false,
creatingDatabase: false,
showCompletionDialog: false,
inOnboarding: false,
createdDBQuery: false,
addedWidget: false,
showingTooltip: OnboardingStep.NONE,
};
export interface OnboardingState {
currentStep: OnboardingStep;
showWelcomeScreen: boolean;
creatingDatabase: boolean;
showCompletionDialog: boolean;
inOnboarding: boolean;
createdDBQuery: boolean;
addedWidget: boolean;
// Tooltip is shown when the step matches this value
showingTooltip: OnboardingStep;
}
const onboardingReducer = createReducer(initialState, {
[ReduxActionTypes.SHOW_WELCOME]: (state: OnboardingState) => {
return { ...state, showWelcomeScreen: true };
},
[ReduxActionTypes.CREATE_ONBOARDING_DBQUERY_INIT]: (
state: OnboardingState,
) => {
return { ...state, creatingDatabase: true };
},
[ReduxActionTypes.CREATE_ONBOARDING_DBQUERY_SUCCESS]: (
state: OnboardingState,
) => {
return {
...state,
creatingDatabase: false,
showWelcomeScreen: false,
createdDBQuery: true,
};
},
[ReduxActionErrorTypes.CREATE_ONBOARDING_DBQUERY_ERROR]: (
state: OnboardingState,
) => {
return { ...state, creatingDatabase: false };
},
[ReduxActionTypes.INCREMENT_STEP]: (state: OnboardingState) => {
return { ...state, currentStep: state.currentStep + 1 };
},
[ReduxActionTypes.SET_CURRENT_STEP]: (
state: OnboardingState,
action: ReduxAction<number>,
) => {
return { ...state, currentStep: action.payload };
},
[ReduxActionTypes.SET_ONBOARDING_STATE]: (
state: OnboardingState,
action: ReduxAction<boolean>,
) => {
return {
...initialState,
inOnboarding: action.payload,
};
},
[ReduxActionTypes.ADD_WIDGET_COMPLETE]: (state: OnboardingState) => {
return {
...state,
addedWidget: true,
};
},
[ReduxActionTypes.SHOW_ONBOARDING_TOOLTIP]: (
state: OnboardingState,
action: ReduxAction<number>,
) => {
return {
...state,
showingTooltip: action.payload,
};
},
[ReduxActionTypes.SHOW_ONBOARDING_COMPLETION_DIALOG]: (
state: OnboardingState,
action: ReduxAction<boolean>,
) => {
return {
...state,
showCompletionDialog: action.payload,
};
},
});
export default onboardingReducer;

View File

@ -67,7 +67,9 @@ import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
export function* createActionSaga(
actionPayload: ReduxAction<Partial<Action> & { eventData: any }>,
) {
try {
const response: ActionCreateUpdateResponse = yield ActionAPI.createAPI(
actionPayload.payload,

View File

@ -26,7 +26,10 @@ import { validateResponse } from "./ErrorSagas";
import { getUserApplicationsOrgsList } from "selectors/applicationSelectors";
import { ApiResponse } from "api/ApiResponses";
import history from "utils/history";
import { BUILDER_PAGE_URL } from "constants/routes";
import {
BUILDER_PAGE_URL,
getApplicationViewerPageURL,
} from "constants/routes";
import { AppState } from "reducers";
import {
FetchApplicationPayload,
@ -43,6 +46,11 @@ import { Organization } from "constants/orgConstants";
import { Variant } from "components/ads/common";
import { AppIconName } from "components/ads/AppIcon";
import { AppColorCode } from "constants/DefaultTheme";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
import { showCompletionDialog } from "./OnboardingSagas";
const getDefaultPageId = (
pages?: ApplicationPagePayload[],
@ -71,6 +79,20 @@ export function* publishApplicationSaga(
yield put({
type: ReduxActionTypes.PUBLISH_APPLICATION_SUCCESS,
});
const applicationId = yield select(getCurrentApplicationId);
const currentPageId = yield select(getCurrentPageId);
let appicationViewPageUrl = getApplicationViewerPageURL(
applicationId,
currentPageId,
);
const showOnboardingCompletionDialog = yield select(showCompletionDialog);
if (showOnboardingCompletionDialog) {
appicationViewPageUrl += "?onboardingComplete=true";
}
window.open(appicationViewPageUrl, "_blank");
}
} catch (error) {
yield put({

View File

@ -0,0 +1,343 @@
import { GenericApiResponse } from "api/ApiResponses";
import DatasourcesApi, { Datasource } from "api/DatasourcesApi";
import { Plugin } from "api/PluginApi";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import { AppState } from "reducers";
import { all, delay, put, select, take, takeEvery } from "redux-saga/effects";
import { getCurrentPageId } from "selectors/editorSelectors";
import { getDatasources, getPlugins } from "selectors/entitiesSelector";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getCurrentOrgId } from "selectors/organizationSelectors";
import { getOnboardingState, setOnboardingState } from "utils/storage";
import { validateResponse } from "./ErrorSagas";
import { getSelectedWidget } from "./selectors";
import ActionAPI, {
ActionApiResponse,
ActionCreateUpdateResponse,
} from "api/ActionAPI";
import {
createOnboardingActionInit,
createOnboardingActionSuccess,
setCurrentStep,
setOnboardingState as setOnboardingReduxState,
showTooltip,
} from "actions/onboardingActions";
import {
changeDatasource,
expandDatasourceEntity,
} from "actions/datasourceActions";
import { playOnboardingAnimation } from "utils/helpers";
import { QueryAction } from "entities/Action";
import { getActionTimeout } from "./ActionExecutionSagas";
import {
OnboardingConfig,
OnboardingStep,
} from "constants/OnboardingConstants";
import AnalyticsUtil from "../utils/AnalyticsUtil";
export const getCurrentStep = (state: AppState) =>
state.ui.onBoarding.currentStep;
export const inOnboarding = (state: AppState) =>
state.ui.onBoarding.inOnboarding;
export const isAddWidgetComplete = (state: AppState) =>
state.ui.onBoarding.addedWidget;
export const getTooltipConfig = (state: AppState) => {
const currentStep = getCurrentStep(state);
if (currentStep >= 0) {
return OnboardingConfig[currentStep].tooltip;
}
return OnboardingConfig[OnboardingStep.NONE].tooltip;
};
export const showCompletionDialog = (state: AppState) => {
const isInOnboarding = inOnboarding(state);
const currentStep = getCurrentStep(state);
return isInOnboarding && currentStep === OnboardingStep.DEPLOY;
};
function* listenForWidgetAdditions() {
while (true) {
yield take();
const { payload } = yield take("WIDGET_ADD_CHILD");
if (payload.type === "TABLE_WIDGET") {
yield put(setCurrentStep(OnboardingStep.ADD_WIDGET));
yield put({
type: ReduxActionTypes.ADD_WIDGET_COMPLETE,
});
yield put(showTooltip(OnboardingStep.ADD_WIDGET));
return;
}
}
}
function* listenForSuccessfullBinding() {
while (true) {
yield take();
let bindSuccessfull = true;
const selectedWidget = yield select(getSelectedWidget);
if (selectedWidget && selectedWidget.type === "TABLE_WIDGET") {
const dataTree = yield select(getDataTree);
if (dataTree[selectedWidget.widgetName]) {
const widgetProperties = dataTree[selectedWidget.widgetName];
const dynamicBindingPathList =
dataTree[selectedWidget.widgetName].dynamicBindingPathList;
const hasBinding = !!dynamicBindingPathList.length;
if (hasBinding) {
yield put(showTooltip(OnboardingStep.NONE));
}
bindSuccessfull = bindSuccessfull && hasBinding;
if (widgetProperties.invalidProps) {
bindSuccessfull =
bindSuccessfull && !("tableData" in widgetProperties.invalidProps);
}
if (bindSuccessfull) {
yield put(setCurrentStep(OnboardingStep.SUCCESSFUL_BINDING));
// Show tooltip now
yield put(showTooltip(OnboardingStep.SUCCESSFUL_BINDING));
yield delay(1000);
playOnboardingAnimation();
return;
}
}
}
}
}
function* hideDatabaseTooltip() {
yield take([ReduxActionTypes.QUERY_PANE_CHANGE]);
yield put(showTooltip(OnboardingStep.NONE));
}
function* createOnboardingDatasource() {
try {
const organizationId = yield select(getCurrentOrgId);
const plugins = yield select(getPlugins);
const postgresPlugin = plugins.find(
(plugin: Plugin) => plugin.name === "PostgreSQL",
);
const datasources: Datasource[] = yield select(getDatasources);
let onboardingDatasource = datasources.find(
datasource => datasource.name === "ExampleDatabase",
);
if (!onboardingDatasource) {
const datasourceConfig: any = {
pluginId: postgresPlugin.id,
name: "ExampleDatabase",
organizationId,
datasourceConfiguration: {
endpoints: [
{
host: "fake-api.cvuydmurdlas.us-east-1.rds.amazonaws.com",
port: 5432,
},
],
authentication: {
databaseName: "fakeapi",
username: "fakeapi",
password: "LimitedAccess123#",
},
},
};
const datasourceResponse: GenericApiResponse<Datasource> = yield DatasourcesApi.createDatasource(
datasourceConfig,
);
yield validateResponse(datasourceResponse);
yield put({
type: ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
payload: datasourceResponse.data,
});
onboardingDatasource = datasourceResponse.data;
}
const currentPageId = yield select(getCurrentPageId);
const queryactionConfiguration: Partial<QueryAction> = {
actionConfiguration: { body: "select * from public.users limit 10" },
};
const actionPayload = {
name: "ExampleQuery",
pageId: currentPageId,
datasource: {
id: onboardingDatasource.id,
},
...queryactionConfiguration,
eventData: {},
};
yield put(createOnboardingActionInit(actionPayload));
const response: ActionCreateUpdateResponse = yield ActionAPI.createAPI(
actionPayload,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
const newAction = {
...response.data,
datasource: onboardingDatasource,
};
yield put(expandDatasourceEntity(onboardingDatasource.id));
yield put(createOnboardingActionSuccess(newAction));
// Run query
const timeout = yield select(getActionTimeout, newAction.id);
const executeActionResponse: ActionApiResponse = yield ActionAPI.executeAction(
{
actionId: newAction.id,
viewMode: false,
},
timeout,
);
yield validateResponse(response);
const payload = {
...executeActionResponse.data,
...executeActionResponse.clientMeta,
};
yield put({
type: ReduxActionTypes.RUN_ACTION_SUCCESS,
payload: { [newAction.id]: payload },
});
yield put({
type: ReduxActionTypes.CREATE_ONBOARDING_DBQUERY_SUCCESS,
});
// Navigate to that datasource page
yield put(changeDatasource(onboardingDatasource));
yield put(showTooltip(OnboardingStep.EXAMPLE_DATABASE));
// Need to hide this tooltip based on some events
yield hideDatabaseTooltip();
} else {
yield put({
type: ReduxActionErrorTypes.CREATE_ONBOARDING_ACTION_ERROR,
payload: actionPayload,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CREATE_ONBOARDING_DBQUERY_ERROR,
payload: { error },
});
}
}
function* listenForWidgetUnselection() {
while (true) {
yield take();
yield take(ReduxActionTypes.HIDE_PROPERTY_PANE);
const currentStep = yield select(getCurrentStep);
const isinOnboarding = yield select(inOnboarding);
if (!isinOnboarding || currentStep !== OnboardingStep.SUCCESSFUL_BINDING)
return;
yield put(setCurrentStep(OnboardingStep.DEPLOY));
yield delay(1000);
yield put(showTooltip(OnboardingStep.DEPLOY));
return;
}
}
function* listenForDeploySaga() {
while (true) {
yield take();
yield take(ReduxActionTypes.PUBLISH_APPLICATION_SUCCESS);
yield put(showTooltip(OnboardingStep.NONE));
yield put({
type: ReduxActionTypes.SHOW_ONBOARDING_COMPLETION_DIALOG,
payload: true,
});
yield put(setOnboardingReduxState(false));
return;
}
}
function* initiateOnboarding() {
const currentOnboardingState = yield getOnboardingState();
AnalyticsUtil.logEvent("ONBOARDING_WELCOME");
if (currentOnboardingState) {
yield put(setOnboardingReduxState(true));
yield put({
type: ReduxActionTypes.NEXT_ONBOARDING_STEP,
});
}
}
function* proceedOnboardingSaga() {
const isInOnboarding = yield select(inOnboarding);
if (isInOnboarding) {
yield put({
type: ReduxActionTypes.INCREMENT_STEP,
});
yield setupOnboardingStep();
}
}
function* setupOnboardingStep() {
const currentStep: OnboardingStep = yield select(getCurrentStep);
const currentConfig = OnboardingConfig[currentStep];
if (currentConfig.eventName) {
AnalyticsUtil.logEvent(currentConfig.eventName);
}
let actions = currentConfig.setup();
if (actions.length) {
actions = actions.map(action => put(action));
yield all(actions);
}
}
function* skipOnboardingSaga() {
AnalyticsUtil.logEvent("END_ONBOARDING");
const set = yield setOnboardingState(false);
if (set) {
yield put(setOnboardingReduxState(false));
}
}
export default function* onboardingSagas() {
yield all([
takeEvery(ReduxActionTypes.CREATE_APPLICATION_SUCCESS, initiateOnboarding),
takeEvery(
ReduxActionTypes.CREATE_ONBOARDING_DBQUERY_INIT,
createOnboardingDatasource,
),
takeEvery(ReduxActionTypes.NEXT_ONBOARDING_STEP, proceedOnboardingSaga),
takeEvery(ReduxActionTypes.END_ONBOARDING, skipOnboardingSaga),
takeEvery(ReduxActionTypes.LISTEN_FOR_ADD_WIDGET, listenForWidgetAdditions),
takeEvery(
ReduxActionTypes.LISTEN_FOR_TABLE_WIDGET_BINDING,
listenForSuccessfullBinding,
),
takeEvery(
ReduxActionTypes.LISTEN_FOR_WIDGET_UNSELECTION,
listenForWidgetUnselection,
),
takeEvery(ReduxActionTypes.SET_CURRENT_STEP, setupOnboardingStep),
takeEvery(ReduxActionTypes.LISTEN_FOR_DEPLOY, listenForDeploySaga),
]);
}

View File

@ -20,6 +20,8 @@ import modalSagas from "./ModalSagas";
import batchSagas from "./BatchSagas";
import themeSagas from "./ThemeSaga";
import evaluationsSaga from "./evaluationsSaga";
import onboardingSaga from "./OnboardingSagas";
import log from "loglevel";
import * as sentry from "@sentry/react";
@ -46,6 +48,7 @@ export function* rootSaga() {
batchSagas,
themeSagas,
evaluationsSaga,
onboardingSaga,
];
yield all(
sagas.map(saga =>

View File

@ -15,6 +15,10 @@ import { CanvasWidgetsReduxState } from "../reducers/entityReducers/canvasWidget
export const getEntities = (state: AppState): AppState["entities"] =>
state.entities;
export const getDatasources = (state: AppState): Datasource[] => {
return state.entities.datasources.list;
};
export const getPluginIdsOfNames = (
state: AppState,
names: Array<string>,

View File

@ -87,7 +87,13 @@ export type EventName =
| "ROUTE_CHANGE"
| "PROPERTY_PANE_CLOSE_CLICK"
| "APPLICATIONS_PAGE_LOAD"
| "EXECUTE_ACTION";
| "EXECUTE_ACTION"
| "ONBOARDING_WELCOME"
| "ONBOARDING_EXAMPLE_DATABASE"
| "ONBOARDING_ADD_WIDGET"
| "ONBOARDING_SUCCESSFUL_BINDING"
| "ONBOARDING_DEPLOY"
| "END_ONBOARDING";
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");
@ -203,6 +209,7 @@ class AnalyticsUtil {
userData: user.userId === ANONYMOUS_USERNAME ? undefined : user,
};
}
if (windowDoc.analytics) {
log.debug("Event fired", eventName, finalEventData);
windowDoc.analytics.track(eventName, finalEventData);

View File

@ -1,4 +1,6 @@
import { GridDefaults } from "constants/WidgetConstants";
import lottie from "lottie-web";
import confetti from "assets/lottie/confetti.json";
import {
DATA_TREE_KEYWORDS,
JAVASCRIPT_KEYWORDS,
@ -191,3 +193,34 @@ export const isNameValid = (
name in invalidNames
);
};
export const playOnboardingAnimation = () => {
const container: Element = document.getElementById("root") as Element;
const el = document.createElement("div");
Object.assign(el.style, {
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
"z-index": 99,
width: "100%",
height: "100%",
});
container.appendChild(el);
const animObj = lottie.loadAnimation({
container: el,
animationData: confetti,
loop: false,
});
const duration = (animObj.totalFrames / animObj.frameRate) * 1000;
animObj.play();
setTimeout(() => {
container.removeChild(el);
}, duration);
};

View File

@ -6,6 +6,7 @@ const STORAGE_KEYS: { [id: string]: string } = {
ROUTE_BEFORE_LOGIN: "RedirectPath",
COPIED_WIDGET: "CopiedWidget",
DELETED_WIDGET_PREFIX: "DeletedWidget-",
ONBOARDING_STATE: "OnboardingState",
};
const store = localforage.createInstance({
@ -91,3 +92,22 @@ export const flushDeletedWidgets = async (widgetId: string) => {
console.log("An error occurred when flushing deleted widgets: ", error);
}
};
export const setOnboardingState = async (onboardingState: boolean) => {
try {
await store.setItem(STORAGE_KEYS.ONBOARDING_STATE, onboardingState);
return true;
} catch (error) {
console.log("An error occurred when setting onboarding state: ", error);
return false;
}
};
export const getOnboardingState = async () => {
try {
const onboardingState = await store.getItem(STORAGE_KEYS.ONBOARDING_STATE);
return onboardingState;
} catch (error) {
console.log("An error occurred when getting onboarding state: ", error);
}
};

View File

@ -12492,6 +12492,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.7.4:
version "5.7.4"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.7.4.tgz#3b252148e904a0aa9833879ffb64924c85a0888c"
integrity sha512-LxqhXlHnHXOPmu+o2ipFKGv42jZLmn/GiEwXP0YC331fFwa+y96OUV22OF9r4i29uWKDciXiJr8tzy6jL8KygA==
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"