diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js index 4f11d6e725..4c0f39bf6f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -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 }); diff --git a/app/client/cypress/locators/ApiEditor.json b/app/client/cypress/locators/ApiEditor.json index 0ba7c8ca51..b5e9c1dd11 100644 --- a/app/client/cypress/locators/ApiEditor.json +++ b/app/client/cypress/locators/ApiEditor.json @@ -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", diff --git a/app/client/cypress/locators/Pages.json b/app/client/cypress/locators/Pages.json index 08f1d84051..206e7b874c 100644 --- a/app/client/cypress/locators/Pages.json +++ b/app/client/cypress/locators/Pages.json @@ -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", diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index e782a531d4..e44bb52a71 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -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", diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 16e52a4c38..7487b1837e 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -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"); -}); diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 4377f0b0fb..75aa557ffd 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -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.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) => { {savingState === SavingState.STARTED ? ( - ) : ( + ) : value ? ( - )} + ) : null} {isEditing && !!isInvalid ? ( - {isInvalid} + + {isInvalid} + ) : null} ); diff --git a/app/client/src/components/ads/EditableTextWrapper.tsx b/app/client/src/components/ads/EditableTextWrapper.tsx new file mode 100644 index 0000000000..1567963d89 --- /dev/null +++ b/app/client/src/components/ads/EditableTextWrapper.tsx @@ -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 ( + + { + 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; + } + }} + /> + + ); +} diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index 085b9190dc..263d8fa028 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -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; diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index 87cfb3e1ca..660c9374be 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -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 = ( - - - - Create New - - -); 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 && ( - + { + if ( + Object.entries(creatingApplicationMap).length === 0 + ) { + createNewApplication( + getNextEntityName( + "Untitled application ", + applications.map((el: any) => el.name), + ), + organization.id, + ); + } + }} + > + {creatingApplicationMap && + creatingApplicationMap[organization.id] ? ( + + ) : ( + + + + Create New + + + )} + )} {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< }} /> diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index b04db0fcd7..1976b5b0c9 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -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 ( @@ -192,8 +207,26 @@ export const EditorHeader = (props: EditorHeaderProps) => { - {currentApplication?.name}  - {pageName}  + {currentApplication ? ( + el.id === applicationId).length > 0 + } + onBlur={(value: string) => + updateApplicationDispatch(applicationId || "", { name: value }) + } + /> + ) : null} + {/* {pageName}  */} diff --git a/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx b/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx index c6a7e3ba4a..8f0f06a1ac 100644 --- a/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx +++ b/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx @@ -100,6 +100,7 @@ const PageListItem = withTheme((props: PageListItemProps) => { editInteractionKind={EditInteractionKind.DOUBLE} onTextChanged={onEditPageName} hideEditIcon + className="t--page-list-item" /> diff --git a/app/client/src/pages/Editor/PropertyPaneTitle.tsx b/app/client/src/pages/Editor/PropertyPaneTitle.tsx index f4e25fd825..f1393f7aee 100644 --- a/app/client/src/pages/Editor/PropertyPaneTitle.tsx +++ b/app/client/src/pages/Editor/PropertyPaneTitle.tsx @@ -116,6 +116,7 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => { onBlur={exitEditMode} hideEditIcon minimal + className="t--propery-page-title" /> {updating && } diff --git a/app/client/src/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/reducers/uiReducers/applicationsReducer.tsx index e8ebe8f7fb..56228c84b3 100644 --- a/app/client/src/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/reducers/uiReducers/applicationsReducer.tsx @@ -222,7 +222,7 @@ const applicationsReducer = createReducer(initialState, { return { ...state, userOrgs: _organizations, - isSavingAppName: isSavingAppName, + isSavingAppName: true, }; }, [ReduxActionTypes.UPDATE_APPLICATION_SUCCESS]: ( diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx index ca17fd97f8..4179f117fa 100644 --- a/app/client/src/sagas/ApplicationSagas.tsx +++ b/app/client/src/sagas/ApplicationSagas.tsx @@ -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", + }); } }