diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js index 5880c1992f..341bbd6cde 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js @@ -29,7 +29,7 @@ describe("Button Widget Functionality", function() { cy.EvaluateDataType("string"); cy.EvaluateCurrentValue(this.data.ButtonLabel); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); //Verify the Button name and label cy.get(widgetsPage.buttonWidget).trigger("mouseover"); diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index d586905e72..d2f07624c5 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -10,7 +10,7 @@ "deleteButton":".bp3-menu-item.bp3-popover-dismiss", "selectAction":"#Base", "deleteApp":".bp3-menu-item", - "homeIcon": ".bp3-icon-home", + "homeIcon": ".t--appsmith-logo", "inputAppName": "input[name=applicationName]", "createNew": ".createnew", "createOrg": "button:contains('Create Organization')", @@ -39,4 +39,4 @@ "shareOrg": ") .bp3-button-text:contains('Share')", "orgSection": ".bp3-button-text:contains(", "createAppFrOrg": ") .t--create-app-popup" -} \ No newline at end of file +} diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 3170dd0efc..96d18d397e 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -22,7 +22,7 @@ "scrollView": ".t--property-control-scrollcontents input", "InputforText": ".t--property-control-text .CodeMirror-code", "TextInside": ".bp3-ui-text span", - "homeIcon": ".bp3-icon.bp3-icon-home", + "homeIcon": ".t--appsmith-logo", "typeWidgetName": ".bp3-editable-text-editing>input", "requiredCheckbox": ".t--property-control-required input[type='checkbox']", "visibleCheckbox": ".t--property-control-visible input[type='checkbox']", @@ -57,5 +57,9 @@ "singleSelectMenuItem": ".bp3-menu-item.single-select div", "selectMenuItem": ".bp3-menu li>a>div", "evaluatedType": ".t--CodeEditor-evaluatedValue>pre", - "evaluatedCurrentValue": ".t--CodeEditor-evaluatedValue div pre" -} \ No newline at end of file + "evaluatedCurrentValue": ".t--CodeEditor-evaluatedValue div pre", + "saveStatusContainer": ".t--save-status-container", + "saveStatusIsSaving": "t--save-status-is-saving", + "saveStatusSuccess": ".t--save-status-success", + "saveStatusError": ".t--save-status-error" +} diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 8a8b8c49f9..27719e7e90 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -629,7 +629,7 @@ Cypress.Commands.add("createModal", (modalType, ModalName) => { .children() .contains(modalType) .click(); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); // changing the model name verify cy.widgetText( @@ -650,21 +650,21 @@ Cypress.Commands.add("createModal", (modalType, ModalName) => { cy.get(widgetsPage.textAlign + " .bp3-menu-item") .contains("Center") .click(); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); }); Cypress.Commands.add("CheckWidgetProperties", checkboxCss => { cy.get(checkboxCss).check({ force: true, }); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); }); Cypress.Commands.add("UncheckWidgetProperties", checkboxCss => { cy.get(checkboxCss).uncheck({ force: true, }); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); }); Cypress.Commands.add( @@ -708,7 +708,7 @@ Cypress.Commands.add("PublishtheApp", () => { cy.route("POST", "/api/v1/applications/publish/*").as("publishApp"); // Wait before publish cy.wait(2000); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); cy.get(homePage.publishButton).click(); cy.wait("@publishApp"); cy.get('a[class="bp3-button"]') @@ -781,14 +781,14 @@ Cypress.Commands.add("SetDateToToday", () => { cy.get(formWidgetsPage.datepickerFooter) .contains("Today") .click(); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); }); Cypress.Commands.add("ClearDate", () => { cy.get(formWidgetsPage.datepickerFooter) .contains("Clear") .click(); - cy.xpath(homePage.homePageID).contains("All changes saved"); + cy.assertPageSave(); }); Cypress.Commands.add("DeleteModal", () => { @@ -1453,3 +1453,7 @@ Cypress.Commands.add("callApi", apiname => { .contains(apiname) .click(); }); + +Cypress.Commands.add("assertPageSave", () => { + cy.get(commonlocators.saveStatusSuccess); +}); diff --git a/app/client/src/assets/icons/header/deploy.svg b/app/client/src/assets/icons/header/deploy.svg new file mode 100644 index 0000000000..faa203e316 --- /dev/null +++ b/app/client/src/assets/icons/header/deploy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/header/feedback.svg b/app/client/src/assets/icons/header/feedback.svg new file mode 100644 index 0000000000..4eba24b0c4 --- /dev/null +++ b/app/client/src/assets/icons/header/feedback.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/header/save-failure.svg b/app/client/src/assets/icons/header/save-failure.svg new file mode 100644 index 0000000000..d1d8ba1915 --- /dev/null +++ b/app/client/src/assets/icons/header/save-failure.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/header/save-loading.gif b/app/client/src/assets/icons/header/save-loading.gif new file mode 100644 index 0000000000..b7cfcf3cab Binary files /dev/null and b/app/client/src/assets/icons/header/save-loading.gif differ diff --git a/app/client/src/assets/icons/header/save-success.svg b/app/client/src/assets/icons/header/save-success.svg new file mode 100644 index 0000000000..a6b536bbe9 --- /dev/null +++ b/app/client/src/assets/icons/header/save-success.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx new file mode 100644 index 0000000000..a08bc3a470 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx @@ -0,0 +1,85 @@ +import React, { ReactNode, useState } from "react"; +import styled from "styled-components"; +import { Icon, Popover, PopoverPosition } from "@blueprintjs/core"; + +const IconContainer = styled.div` + margin: 0 10px; + border: 1px solid #bcccd9; + border-radius: 50%; + min-width: 32px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; +`; + +const DeployLinkDialog = styled.a` + display: flex; + align-items: center; + background-color: #fff; + padding: 10px; + width: 336px; + height: 60px; + cursor: pointer; + text-decoration: none; + color: #2e3d49; + :hover { + text-decoration: none; + color: #2e3d49; + } +`; + +const DeployUrl = styled.div` + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +type Props = { + trigger: ReactNode; + link: string; +}; + +export const DeployLinkButton = (props: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const onClose = () => { + setIsOpen(false); + }; + + return ( + + + + + + + {window.location.origin} + {props.link} + + + + } + canEscapeKeyClose={false} + onClose={onClose} + isOpen={isOpen} + position={PopoverPosition.BOTTOM} + > +
setIsOpen(true)}>{props.trigger}
+
+
+ ); +}; + +export default DeployLinkButton; diff --git a/app/client/src/components/designSystems/appsmith/header/ThreeDotsLoading.tsx b/app/client/src/components/designSystems/appsmith/header/ThreeDotsLoading.tsx new file mode 100644 index 0000000000..6705113b6e --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/header/ThreeDotsLoading.tsx @@ -0,0 +1,69 @@ +/*Huge thanks to @tobiasahlin at http://tobiasahlin.com/spinkit/ */ + +import React from "react"; +import styled from "styled-components"; + +const Spinner = styled.div` + width: 30px; + text-align: center; + && > div { + width: 4px; + height: 4px; + background-color: #fff; + + border-radius: 100%; + display: inline-block; + -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; + animation: sk-bouncedelay 1.4s infinite ease-in-out both; + } + + && .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; + } + + && .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; + } + + @-webkit-keyframes sk-bouncedelay { + 0%, + 80%, + 100% { + -webkit-transform: scale(0); + } + 40% { + -webkit-transform: scale(1); + } + } + + @keyframes sk-bouncedelay { + 0%, + 80%, + 100% { + -webkit-transform: scale(0); + transform: scale(0); + } + 40% { + -webkit-transform: scale(1); + transform: scale(1); + } + } +`; + +type Props = { + className?: string; +}; + +const ThreeDotLoading = (props: Props) => { + return ( + +
+
+
+ + ); +}; + +export default ThreeDotLoading; diff --git a/app/client/src/components/editorComponents/Button.tsx b/app/client/src/components/editorComponents/Button.tsx index 4c5481842f..243abfe7e3 100644 --- a/app/client/src/components/editorComponents/Button.tsx +++ b/app/client/src/components/editorComponents/Button.tsx @@ -96,6 +96,7 @@ export type ButtonProps = { className?: string; fluid?: boolean; skin?: Skin; + target?: string; }; export const Button = (props: ButtonProps) => { @@ -133,6 +134,7 @@ export const Button = (props: ButtonProps) => { rightIcon={rightIcon} {...baseProps} href={props.href} + target={props.target} /> ); } else diff --git a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx index d992f87dec..7a7eb67024 100644 --- a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx +++ b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx @@ -1,6 +1,5 @@ import React, { ReactNode, useState } from "react"; import styled from "styled-components"; -import { connect } from "react-redux"; import { Dialog, Classes } from "@blueprintjs/core"; import { isPermitted } from "pages/Applications/permissionHelpers"; @@ -75,4 +74,4 @@ export const FormDialogComponent = (props: FormDialogComponentProps) => { ); }; -export default connect()(FormDialogComponent); +export default FormDialogComponent; diff --git a/app/client/src/icons/HeaderIcons.tsx b/app/client/src/icons/HeaderIcons.tsx index 435df70e70..e6f36a3d63 100644 --- a/app/client/src/icons/HeaderIcons.tsx +++ b/app/client/src/icons/HeaderIcons.tsx @@ -1,6 +1,10 @@ import React, { JSXElementConstructor } from "react"; import { IconProps, IconWrapper } from "constants/IconConstants"; import { ReactComponent as ShareIcon } from "assets/icons/header/share-white.svg"; +import { ReactComponent as DeployIcon } from "assets/icons/header/deploy.svg"; +import { ReactComponent as FeedbackIcon } from "assets/icons/header/feedback.svg"; +import { ReactComponent as SaveFailureIcon } from "assets/icons/header/save-failure.svg"; +import { ReactComponent as SaveSuccessIcon } from "assets/icons/header/save-success.svg"; /* eslint-disable react/display-name */ export const HeaderIcons: { @@ -11,6 +15,26 @@ export const HeaderIcons: { ), + DEPLOY: (props: IconProps) => ( + + + + ), + FEEDBACK: (props: IconProps) => ( + + + + ), + SAVE_FAILURE: (props: IconProps) => ( + + + + ), + SAVE_SUCCESS: (props: IconProps) => ( + + + + ), }; export type HeaderIconName = keyof typeof HeaderIcons; diff --git a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx index 1b83635b11..963eff4c04 100644 --- a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx +++ b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx @@ -150,7 +150,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => { intent="none" outline size="small" - className="t--application-share-btn share-button" + className="t--application-share-btn" icon={ props.theme.fonts[0]}; - font-size: ${props => props.theme.fontSizes[2]}px; - } + :nth-child(1) { + justify-content: flex-start; + } + :nth-child(2) { + justify-content: center; + flex-direction: column; + } + :nth-child(3) { + justify-content: flex-end; } `; -const ShareButton = styled.div` +const AppsmithLogoImg = styled.img` + max-width: 110px; +`; + +const ApplicationName = styled.span` + font-weight: 500; + font-size: 14px; + line-height: 14px; + color: #fff; + margin-bottom: 6px; +`; + +const PageName = styled.span` display: flex; - flex-grow: 1; - justify-content: flex-end; + flex: 1; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.04em; + color: #ffffff; + opacity: 0.5; +`; + +const SaveStatusContainer = styled.div` + margin: 0 10px; + border: 1px solid rgb(95, 105, 116); + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +`; +const DeploySection = styled.div` + display: flex; +`; + +const DeployButton = styled(Button)` + height: 32px; + margin: 5px 10px; + margin-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +`; + +const DeployLinkButton = styled(Button)` + height: 32px; + margin: 5px 10px; + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + min-width: 20px !important; + width: 20px !important; + background-color: rgb(42, 195, 157) !important; + border: none !important; +`; + +const ShareButton = styled(Button)` + height: 32px; + margin: 5px 10px; + color: white !important; + border-color: rgb(95, 105, 116) !important; `; type EditorHeaderProps = { @@ -64,143 +126,173 @@ type EditorHeaderProps = { isSaving?: boolean; pageSaveError?: boolean; pageName?: string; - onPublish: () => void; - onCreatePage: (name: string) => void; - pages?: PageListPayload; - currentPageId?: string; + pageId?: string; isPublishing: boolean; publishedTime?: string; orgId: string; - currentApplicationId?: string; - createModal: () => void; + applicationId?: string; + publishApplication: (appId: string) => void; }; -const navigation: IBreadcrumbProps[] = [ - { href: APPLICATIONS_URL, icon: "home", text: "Home" }, - { href: APPLICATIONS_URL, icon: "folder-close", text: "Applications" }, - { icon: "page-layout", text: "", current: true }, -]; + export const EditorHeader = (props: EditorHeaderProps) => { const { currentApplication, isSaving, pageSaveError, - onPublish, - pages, - currentPageId, + pageId, isPublishing, orgId, - currentApplicationId, + applicationId, + pageName, + publishApplication, } = props; - const selectedPageName = pages?.find(page => page.pageId === currentPageId) - ?.pageName; + const handlePublish = () => { + if (applicationId) { + publishApplication(applicationId); - const pageSelectorData: CustomizedDropdownProps = { - sections: [ - { - isSticky: true, - options: [ - { - content: ( -