Onboarding flow (#1960)
Co-authored-by: Hetu Nandu <hetunandu@gmail.com>
This commit is contained in:
parent
5a36d17f7a
commit
e84699e7ba
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
4
app/client/cypress/locators/Onboarding.json
Normal file
4
app/client/cypress/locators/Onboarding.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tooltipAction": ".tooltip-action",
|
||||
"tooltipSnippet": ".tooltip-snippet"
|
||||
}
|
||||
|
|
@ -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"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
43
app/client/src/actions/onboardingActions.ts
Normal file
43
app/client/src/actions/onboardingActions.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
1
app/client/src/assets/lottie/confetti.json
Normal file
1
app/client/src/assets/lottie/confetti.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
@ -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>{" "}
|
||||
You’re Awesome!
|
||||
</Title>
|
||||
<ContentWrapper>
|
||||
<DescriptionWrapper>
|
||||
<DescriptionTitle>
|
||||
You’ve completed this tutorial. Here’s 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;
|
||||
|
|
@ -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;
|
||||
129
app/client/src/constants/OnboardingConstants.tsx
Normal file
129
app/client/src/constants/OnboardingConstants.tsx
Normal 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: "You’re almost done! Just Hit Deploy",
|
||||
description:
|
||||
"Deploying your apps is a crucial step to building on appsmith.",
|
||||
isFinalStep: true,
|
||||
},
|
||||
eventName: "ONBOARDING_DEPLOY",
|
||||
},
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} </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} </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) => ({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
112
app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
Normal file
112
app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
21
app/client/src/pages/Editor/Explorer/Onboarding/Loading.tsx
Normal file
21
app/client/src/pages/Editor/Explorer/Onboarding/Loading.tsx
Normal 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;
|
||||
40
app/client/src/pages/Editor/Explorer/Onboarding/index.tsx
Normal file
40
app/client/src/pages/Editor/Explorer/Onboarding/index.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
139
app/client/src/pages/Editor/Welcome.tsx
Normal file
139
app/client/src/pages/Editor/Welcome.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
100
app/client/src/reducers/uiReducers/onBoardingReducer.ts
Normal file
100
app/client/src/reducers/uiReducers/onBoardingReducer.ts
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
343
app/client/src/sagas/OnboardingSagas.ts
Normal file
343
app/client/src/sagas/OnboardingSagas.ts
Normal 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),
|
||||
]);
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user