Feature: New create app flow (#1375)

* EditableTextWrapper component implemented

* create new app flow implemented

* test cases for new create new app flow fixed

* role update element fixed

* before-all hooks fixed

* createOrg test cases fixed

* loading state test added

* editableText input unique identifier added in test-cases

* updateApplication API alias name corrected

* removed the second className prop on same component

* PR feedback implemented
This commit is contained in:
devrk96 2020-10-31 12:10:51 +05:30 committed by GitHub
parent 5c26ef5d07
commit 7d6a70d3ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 220 additions and 110 deletions

View File

@ -29,7 +29,9 @@ describe("Create new org and share with a user", function() {
cy.wait(2000);
cy.get(homePage.appsContainer).contains(orgid);
cy.xpath(homePage.ShareBtn).should("not.exist");
cy.get(homePage.applicationCard).trigger("mouseover");
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
cy.get(homePage.appEditIcon).should("not.exist");
cy.launchApp(appid);
cy.LogOut();
@ -62,7 +64,9 @@ describe("Create new org and share with a user", function() {
cy.xpath(homePage.ShareBtn)
.first()
.should("be.visible");
cy.get(homePage.applicationCard).trigger("mouseover");
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
cy.get(homePage.appEditIcon)
.first()
.click({ force: true });

View File

@ -4,7 +4,7 @@
"createBlankApiCard": ".t--createBlankApiCard",
"eachProviderCard": ".t--eachProviderCard",
"nameOfApi": ".t--nameOfApi",
"ApiNameField": ".bp3-editable-text",
"ApiNameField": ".t--action-name-edit-field",
"addToPageBtn": ".t--addToPageBtn",
"ApiDeleteBtn": ".t--apiFormDeleteBtn",
"ApiRunBtn": ".t--apiFormRunBtn",

View File

@ -5,7 +5,7 @@
"viewWidgets": ".t--page-sidebar-ViewWidgets",
"widgetsEditor": ".t--nav-link-widgets-editor",
"AddPage": ".pages .t--entity-add-btn",
"editInput": "input.bp3-editable-text-input",
"editInput": ".t--entity-name.editing",
"Menuaction": ".bp3-overlay-open>.bp3-transition-container",
"Delete": ":nth-child(2) > .bp3-menu-item",
"apiEditorIcon": ".t--nav-link-api-editor",

View File

@ -30,7 +30,7 @@
"labelTextStyle": ".bp3-ui-text span",
"bodyTextStyle": ".bp3-running-text span",
"headingTextStyle": ".bp3-heading span",
"editWidgetName": ".bp3-editable-text",
"editWidgetName": ".t--propery-page-title",
"dropDownIcon": ".t--property-control-textstyle span.bp3-icon-chevron-down",
"onDateSelectedField": ".t--property-control-ondateselected",
"TableRow": ".t--draggable-tablewidget .tbody",

View File

@ -214,28 +214,37 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => {
.scrollIntoView()
.should("be.visible")
.click();
cy.get(homePage.inputAppName).type(appname);
cy.get(homePage.CreateApp)
.contains("Submit")
.click({ force: true });
cy.get("#loading").should("not.exist");
cy.wait("@createNewApplication").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
cy.wait(1000);
cy.get(homePage.applicationName).type(appname + "{enter}");
cy.wait("@updateApplication").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
});
Cypress.Commands.add("CreateApp", appname => {
cy.get(homePage.createNew)
.first()
.click({ force: true });
cy.get(homePage.inputAppName).type(appname);
cy.get(homePage.CreateApp)
.contains("Submit")
.click({ force: true });
cy.wait("@createNewApplication").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
cy.get("#loading").should("not.exist");
cy.wait("@getPagesForCreateApp").should(
cy.wait(1000);
cy.get(homePage.applicationName).type(appname + "{enter}");
cy.wait("@updateApplication").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.get("h2").contains("Drag and drop a widget here");
});
Cypress.Commands.add("DeleteApp", appName => {
@ -301,7 +310,9 @@ Cypress.Commands.add("DeleteApp", appName => {
cy.get(commonlocators.homeIcon).click({ force: true });
cy.get(homePage.searchInput).type(appName);
cy.wait(2000);
cy.get(homePage.applicationCard).trigger("mouseover");
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
cy.get(homePage.appMoreIcon)
.first()
.click({ force: true });
@ -1787,15 +1798,3 @@ Cypress.Commands.add("callApi", apiname => {
Cypress.Commands.add("assertPageSave", () => {
cy.get(commonlocators.saveStatusSuccess);
});
Cypress.Commands.add("EditApp", appName => {
cy.get(homePage.searchInput).type(appName);
cy.wait(2000);
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
cy.get(homePage.appEditIcon)
.first()
.click({ force: true });
cy.get("#loading").should("not.exist");
});

View File

@ -21,28 +21,28 @@ export enum SavingState {
ERROR = "ERROR",
}
type EditableTextProps = CommonComponentProps & {
export type EditableTextProps = CommonComponentProps & {
defaultValue: string;
onTextChanged: (value: string) => void;
placeholder: string;
placeholder?: string;
editInteractionKind: EditInteractionKind;
savingState: SavingState;
onBlur: (value: string) => void;
onTextChanged?: (value: string) => void;
className?: string;
valueTransform?: (value: string) => string;
isEditingDefault?: boolean;
forceDefault?: boolean;
updating?: boolean;
isInvalid?: (value: string) => string | boolean;
editInteractionKind: EditInteractionKind;
hideEditIcon?: boolean;
fill?: boolean;
savingState: SavingState;
onBlur: (value: string) => void;
};
const EditableTextWrapper = styled.div<{
fill?: boolean;
}>`
width: ${props => (!props.fill ? "234px" : "100%")};
.${Classes.TEXT} {
.error-message {
margin-left: ${props => props.theme.spaces[5]}px;
color: ${props => props.theme.colors.danger.main};
}
@ -70,10 +70,6 @@ const TextContainer = styled.div<{
}>`
display: flex;
align-items: center;
${props =>
props.isEditing && props.isInvalid
? `margin-bottom: ${props.theme.spaces[2]}px`
: null};
.bp3-editable-text.bp3-editable-text-editing::before,
.bp3-editable-text.bp3-disabled::before {
display: none;
@ -143,7 +139,6 @@ export const EditableText = (props: EditableTextProps) => {
const [savingState, setSavingState] = useState<SavingState>(
SavingState.NOT_STARTED,
);
const valueRef = React.useRef(defaultValue);
useEffect(() => {
setSavingState(props.savingState);
@ -178,14 +173,14 @@ export const EditableText = (props: EditableTextProps) => {
const onConfirm = useCallback(
(_value: string) => {
if (savingState === SavingState.ERROR || isInvalid) {
if (savingState === SavingState.ERROR || isInvalid || _value === "") {
setValue(lastValidValue);
onBlur(lastValidValue);
setSavingState(SavingState.NOT_STARTED);
} else if (changeStarted) {
onTextChanged(_value);
onBlur(_value);
onTextChanged && onTextChanged(_value);
}
onBlur(_value);
setIsEditing(false);
setChangeStarted(false);
},
@ -204,10 +199,9 @@ export const EditableText = (props: EditableTextProps) => {
const finalVal: string = _value;
const errorMessage = inputValidation && inputValidation(finalVal);
const error = errorMessage ? errorMessage : false;
if (!error) {
if (!error && _value !== "") {
setLastValidValue(finalVal);
valueRef.current = finalVal;
onTextChanged(finalVal);
onTextChanged && onTextChanged(finalVal);
}
setValue(finalVal);
setIsInvalid(error);
@ -258,8 +252,8 @@ export const EditableText = (props: EditableTextProps) => {
onChange={onInputchange}
onConfirm={onConfirm}
value={value}
selectAllOnFocus
placeholder={props.placeholder}
selectAllOnFocus={true}
placeholder={props.placeholder || defaultValue}
className={props.className}
onCancel={onConfirm}
/>
@ -267,13 +261,15 @@ export const EditableText = (props: EditableTextProps) => {
<IconWrapper className="icon-wrapper">
{savingState === SavingState.STARTED ? (
<Spinner size={IconSize.XL} />
) : (
) : value ? (
<Icon name={iconName} size={IconSize.XL} />
)}
) : null}
</IconWrapper>
</TextContainer>
{isEditing && !!isInvalid ? (
<Text type={TextType.P2}>{isInvalid}</Text>
<Text className="error-message" type={TextType.P2}>
{isInvalid}
</Text>
) : null}
</EditableTextWrapper>
);

View File

@ -0,0 +1,82 @@
import EditableText, { EditableTextProps, SavingState } from "./EditableText";
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { Classes } from "@blueprintjs/core";
type EditableTextWrapperProps = EditableTextProps & {
variant: "UNDERLINE" | "ICON";
isNewApp: boolean;
};
const Container = styled.div<{
isEditing?: boolean;
savingState: SavingState;
isInvalid: boolean;
}>`
&&& .${Classes.EDITABLE_TEXT}, .icon-wrapper {
padding: 5px 10px;
height: 25px;
text-decoration: ${props => (props.isEditing ? "unset" : "underline")};
text-decoration-style: dotted;
background-color: ${props =>
(props.isInvalid && props.isEditing) ||
props.savingState === SavingState.ERROR
? props.theme.colors.editableText.dangerBg
: "transparent"};
}
&&& .${Classes.EDITABLE_TEXT_CONTENT}, &&& .${Classes.EDITABLE_TEXT_INPUT} {
text-align: center;
color: #d4d4d4;
font-size: ${props => props.theme.typography.h4.fontSize}px;
line-height: ${props => props.theme.typography.h4.lineHeight}px;
letter-spacing: ${props => props.theme.typography.h4.letterSpacing}px;
font-weight: ${props => props.theme.typography.h4.fontWeight}px;
}
.error-message {
margin-top: 2px;
}
`;
export default function EditableTextWrapper(props: EditableTextWrapperProps) {
const [isEditing, setIsEditing] = useState(props.isNewApp);
const [isValid, setIsValid] = useState(false);
useEffect(() => {
setIsEditing(props.isNewApp);
}, [props.isNewApp]);
return (
<Container
isEditing={isEditing}
savingState={props.savingState}
isInvalid={isValid}
>
<EditableText
defaultValue={props.defaultValue}
editInteractionKind={props.editInteractionKind}
placeholder={props.placeholder}
hideEditIcon={props.hideEditIcon}
isEditingDefault={props.isNewApp}
savingState={props.savingState}
fill={props.fill}
onBlur={value => {
setIsEditing(false);
props.onBlur(value);
}}
className={props.className}
onTextChanged={(value: string) => setIsEditing(true)}
isInvalid={(value: string) => {
setIsEditing(true);
if (props.isInvalid) {
setIsValid(Boolean(props.isInvalid(value)));
return props.isInvalid(value);
} else {
return false;
}
}}
/>
</Container>
);
}

View File

@ -204,7 +204,6 @@ const AppNameWrapper = styled.div<{ isFetching: boolean }>`
: null};
`;
type ApplicationCardProps = {
activeAppCard?: boolean;
application: ApplicationPayload;
duplicate?: (applicationId: string) => void;
share?: (applicationId: string) => void;
@ -275,11 +274,6 @@ export const ApplicationCard = (props: ApplicationCardProps) => {
addDeleteOption();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (props.activeAppCard) {
setShowOverlay(true);
}
}, [props.activeAppCard]);
const appIcon = (props.application?.icon ||
getApplicationIcon(props.application.id)) as AppIconName;

View File

@ -1,4 +1,4 @@
import React, { Component, useState } from "react";
import React, { Component, Fragment, useState } from "react";
import styled from "styled-components";
import { connect, useSelector, useDispatch } from "react-redux";
import { AppState } from "reducers";
@ -53,11 +53,11 @@ import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { loadingUserOrgs } from "./ApplicationLoaders";
import CreateApplicationForm from "./CreateApplicationForm";
import { creatingApplicationMap } from "reducers/uiReducers/applicationsReducer";
import CenteredWrapper from "../../components/designSystems/appsmith/CenteredWrapper";
import NoSearchImage from "../../assets/images/NoSearchResult.svg";
import organizationList from "../../mockResponses/OrganisationListResponse";
import { getNextEntityName } from "utils/AppsmithUtils";
import Spinner from "components/ads/Spinner";
const OrgDropDown = styled.div`
display: flex;
@ -348,7 +348,7 @@ function LeftPane() {
isFetchingApplications ? BlueprintClasses.SKELETON : ""
}
icon="workspace"
key={org.organization.name}
key={org.organization.id}
href={`${window.location.pathname}#${org.organization.name}`}
text={org.organization.name}
ellipsize={20}
@ -405,18 +405,6 @@ ${props => {
}
`;
const AddApplicationCard = (
<ApplicationAddCardWrapper>
<Icon
className="t--create-app-popup"
name={"plus"}
size={IconSize.LARGE}
></Icon>
<CreateNewLabel type={TextType.H4} className="createnew">
Create New
</CreateNewLabel>
</ApplicationAddCardWrapper>
);
const NoSearchResultImg = styled.img`
margin: 1em;
`;
@ -513,6 +501,7 @@ const ApplicationsSection = (props: any) => {
};
const createNewApplication = (applicationName: string, orgId: string) => {
console.log(applicationName, orgId);
return dispatch({
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
payload: {
@ -601,14 +590,40 @@ const ApplicationsSection = (props: any) => {
) &&
!isFetchingApplications && (
<PaddingWrapper>
<FormDialogComponent
permissions={organization.userPermissions}
permissionRequired={PERMISSION_TYPE.CREATE_APPLICATION}
trigger={AddApplicationCard}
Form={CreateApplicationForm}
orgId={organization.id}
title={CREATE_APPLICATION_FORM_NAME}
/>
<ApplicationAddCardWrapper
onClick={() => {
if (
Object.entries(creatingApplicationMap).length === 0
) {
createNewApplication(
getNextEntityName(
"Untitled application ",
applications.map((el: any) => el.name),
),
organization.id,
);
}
}}
>
{creatingApplicationMap &&
creatingApplicationMap[organization.id] ? (
<Spinner size={IconSize.XXXL} />
) : (
<Fragment>
<Icon
className="t--create-app-popup"
name={"plus"}
size={IconSize.LARGE}
></Icon>
<CreateNewLabel
type={TextType.H4}
className="createnew"
>
Create New
</CreateNewLabel>
</Fragment>
)}
</ApplicationAddCardWrapper>
</PaddingWrapper>
)}
{applications.map((application: any) => {
@ -619,11 +634,6 @@ const ApplicationsSection = (props: any) => {
key={application.id}
application={application}
orgId={organization.id}
activeAppCard={
props.newApplicationList[
props.newApplicationList.length - 1
] === application.id
}
delete={deleteApplication}
update={updateApplicationDispatch}
duplicate={duplicateApplicationDispatch}
@ -664,14 +674,13 @@ type ApplicationProps = {
};
class Applications extends Component<
ApplicationProps,
{ selectedOrgId: string; newApplicationList: any }
{ selectedOrgId: string }
> {
constructor(props: ApplicationProps) {
super(props);
this.state = {
selectedOrgId: "",
newApplicationList: [],
};
}
@ -679,22 +688,6 @@ class Applications extends Component<
PerformanceTracker.stopTracking(PerformanceTransactionName.LOGIN_CLICK);
PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP);
this.props.getAllApplication();
if (this.props.applicationList.length > 0) {
this.setState({
newApplicationList: this.props.applicationList.map(el => el.id),
});
}
}
componentDidUpdate() {
if (
this.props.applicationList.length > 0 &&
this.props.applicationList.length !== this.state.newApplicationList.length
) {
this.setState({
newApplicationList: this.props.applicationList.map(el => el.id),
});
}
}
public render() {
@ -708,7 +701,6 @@ class Applications extends Component<
}}
/>
<ApplicationsSection
newApplicationList={this.state.newApplicationList}
searchKeyword={this.props.searchKeyword}
></ApplicationsSection>
</PageWrapper>

View File

@ -25,10 +25,17 @@ import {
getIsPublishingApplication,
} from "selectors/editorSelectors";
import { getCurrentOrgId } from "selectors/organizationSelectors";
import { connect } from "react-redux";
import { connect, useDispatch, useSelector } from "react-redux";
import { HeaderIcons } from "icons/HeaderIcons";
import ThreeDotLoading from "components/designSystems/appsmith/header/ThreeDotsLoading";
import DeployLinkButtonDialog from "components/designSystems/appsmith/header/DeployLinkButton";
import { EditInteractionKind, SavingState } from "components/ads/EditableText";
import { updateApplication } from "actions/applicationActions";
import {
getApplicationList,
getIsSavingAppName,
} from "selectors/applicationSelectors";
import EditableTextWrapper from "components/ads/EditableTextWrapper";
const HeaderWrapper = styled(StyledHeader)`
background: ${Colors.BALTIC_SEA};
@ -143,6 +150,10 @@ export const EditorHeader = (props: EditorHeaderProps) => {
publishApplication,
} = props;
const dispatch = useDispatch();
const isSavingName = useSelector(getIsSavingAppName);
const applicationList = useSelector(getApplicationList);
const handlePublish = () => {
if (applicationId) {
publishApplication(applicationId);
@ -180,6 +191,10 @@ export const EditorHeader = (props: EditorHeaderProps) => {
}
}
const updateApplicationDispatch = (id: string, data: { name: string }) => {
dispatch(updateApplication(id, data));
};
return (
<HeaderWrapper>
<HeaderSection>
@ -192,8 +207,26 @@ export const EditorHeader = (props: EditorHeaderProps) => {
</Link>
</HeaderSection>
<HeaderSection flex-direction={"row"}>
<ApplicationName>{currentApplication?.name}&nbsp;</ApplicationName>
<PageName>{pageName}&nbsp;</PageName>
{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 })
}
/>
) : null}
{/* <PageName>{pageName}&nbsp;</PageName> */}
</HeaderSection>
<HeaderSection>
<SaveStatusContainer className={"t--save-status-container"}>

View File

@ -100,6 +100,7 @@ const PageListItem = withTheme((props: PageListItemProps) => {
editInteractionKind={EditInteractionKind.DOUBLE}
onTextChanged={onEditPageName}
hideEditIcon
className="t--page-list-item"
/>
</Tooltip>
</div>

View File

@ -116,6 +116,7 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => {
onBlur={exitEditMode}
hideEditIcon
minimal
className="t--propery-page-title"
/>
{updating && <Spinner size={16} />}
</NameWrapper>

View File

@ -222,7 +222,7 @@ const applicationsReducer = createReducer(initialState, {
return {
...state,
userOrgs: _organizations,
isSavingAppName: isSavingAppName,
isSavingAppName: true,
};
},
[ReduxActionTypes.UPDATE_APPLICATION_SUCCESS]: (

View File

@ -212,6 +212,10 @@ export function* updateApplicationSaga(
type: ReduxActionTypes.UPDATE_APPLICATION_SUCCESS,
payload: response.data,
});
AppToaster.show({
message: `Application updated`,
type: "success",
});
}
} catch (error) {
yield put({
@ -220,6 +224,10 @@ export function* updateApplicationSaga(
error,
},
});
AppToaster.show({
message: error,
type: "error",
});
}
}