diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js index 3f397e4b07..2dd1ba83e5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js @@ -1,11 +1,6 @@ -const commonlocators = require("../../../locators/commonlocators.json"); -const dsl = require("../../../fixtures/commondsl.json"); -const widgetsPage = require("../../../locators/Widgets.json"); -const testdata = require("../../../fixtures/testdata.json"); -const pages = require("../../../locators/Pages.json"); +const dsl = require("../../../fixtures/displayWidgetDsl.json"); const apiwidget = require("../../../locators/apiWidgetslocator.json"); const explorer = require("../../../locators/explorerlocators.json"); -const pageid = "MyPage"; describe("Entity explorer tests related to widgets and validation", function() { beforeEach(() => { @@ -13,31 +8,26 @@ describe("Entity explorer tests related to widgets and validation", function() { }); it("Widget edit/delete/copy to clipboard validation", function() { - cy.openPropertyPane("textwidget"); - cy.widgetText("Api", widgetsPage.textWidget, widgetsPage.textInputval); - cy.testCodeMirror("/api/users/2"); cy.NavigateToEntityExplorer(); - cy.wait(5000); - cy.SearchEntityandOpen("Api"); + cy.SearchEntityandOpen("Text1"); cy.get(explorer.collapse) .last() .click({ force: true }); cy.get(explorer.property) .last() .click({ force: true }); - cy.wait(2000); cy.get(apiwidget.propertyList).then(function($lis) { expect($lis).to.have.length(2); - expect($lis.eq(0)).to.contain("{{Api.isVisible}}"); - expect($lis.eq(1)).to.contain("{{Api.text}}"); + expect($lis.eq(0)).to.contain("{{Text1.isVisible}}"); + expect($lis.eq(1)).to.contain("{{Text1.text}}"); }); - cy.GlobalSearchEntity("Api"); - cy.EditApiNameFromExplorer("ApiUpdated"); - cy.GlobalSearchEntity("ApiUpdated"); + cy.GlobalSearchEntity("Text1"); + cy.EditApiNameFromExplorer("TextUpdated"); + cy.GlobalSearchEntity("TextUpdated"); cy.get(apiwidget.propertyList).then(function($lis) { expect($lis).to.have.length(2); - expect($lis.eq(0)).to.contain("{{ApiUpdated.isVisible}}"); - expect($lis.eq(1)).to.contain("{{ApiUpdated.text}}"); + expect($lis.eq(0)).to.contain("{{TextUpdated.isVisible}}"); + expect($lis.eq(1)).to.contain("{{TextUpdated.text}}"); }); cy.DeleteWidgetFromSideBar(); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Pages/Pages_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Pages/Pages_spec.js new file mode 100644 index 0000000000..420a709de5 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Pages/Pages_spec.js @@ -0,0 +1,18 @@ +const pages = require("../../../locators/Pages.json"); + +describe("Pages", function() { + it("Clone page", function() { + cy.xpath(pages.popover) + .last() + .click({ force: true }); + cy.get(pages.clonePage).click({ force: true }); + + cy.wait("@clonePage").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); + + cy.get(".t--entity-name:contains(Page1 Copy)"); + }); +}); diff --git a/app/client/cypress/locators/Pages.json b/app/client/cypress/locators/Pages.json index a718431a0b..8ef9748aa0 100644 --- a/app/client/cypress/locators/Pages.json +++ b/app/client/cypress/locators/Pages.json @@ -16,6 +16,7 @@ "entityExplorer": ".t--nav-link-entity-explorer", "popover": "//div[contains(@class,'t--entity page')]//*[local-name()='g' and @id='Icon/Outline/more-vertical']", "editName": ".single-select >div:contains('Edit Name')", + "clonePage": ".single-select >div:contains('Clone')", "deletePage": ".single-select >div:contains('Delete')", "entityQuery": ".t--entity-name:contains('Queries')" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 87da3f2c72..9b4a491d2c 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1478,6 +1478,7 @@ Cypress.Commands.add("startServerAndRoutes", () => { ); cy.route("GET", "/api/v1/users/me").as("getUser"); cy.route("POST", "/api/v1/pages").as("createPage"); + cy.route("POST", "/api/v1/pages/clone/*").as("clonePage"); }); Cypress.Commands.add("alertValidate", text => { diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index 8d0cbc7324..ff0122138e 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -105,6 +105,30 @@ export const createPage = (applicationId: string, pageName: string) => { }; }; +export const clonePageInit = (pageId: string) => { + return { + type: ReduxActionTypes.CLONE_PAGE_INIT, + payload: { + id: pageId, + }, + }; +}; + +export const clonePageSuccess = ( + pageId: string, + pageName: string, + layoutId: string, +) => { + return { + type: ReduxActionTypes.CLONE_PAGE_SUCCESS, + payload: { + pageId, + pageName, + layoutId, + }, + }; +}; + export const updatePage = (id: string, name: string) => { return { type: ReduxActionTypes.UPDATE_PAGE_INIT, diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index dfd0d150fc..4344fb22d6 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -78,6 +78,10 @@ export interface DeletePageRequest { id: string; } +export interface ClonePageRequest { + id: string; +} + export interface UpdateWidgetNameRequest { pageId: string; layoutId: string; @@ -150,6 +154,10 @@ class PageApi extends Api { return Api.delete(PageApi.url + "/" + request.id); } + static clonePage(request: ClonePageRequest): AxiosPromise { + return Api.post(PageApi.url + "/clone/" + request.id); + } + static updateWidgetName( request: UpdateWidgetNameRequest, ): AxiosPromise { diff --git a/app/client/src/components/ads/TableDropdown.tsx b/app/client/src/components/ads/TableDropdown.tsx new file mode 100644 index 0000000000..ae55039dd9 --- /dev/null +++ b/app/client/src/components/ads/TableDropdown.tsx @@ -0,0 +1,109 @@ +import React, { useCallback, useState } from "react"; +import { CommonComponentProps, hexToRgba } from "./common"; +import { ReactComponent as DownArrow } from "../../assets/icons/ads/down_arrow.svg"; +import Text, { TextType } from "./Text"; +import styled from "styled-components"; + +type DropdownOption = { + label: string; + value: string; +}; + +type DropdownProps = CommonComponentProps & { + options: DropdownOption[]; + onSelect: (selectedValue: string) => void; + selectedOption: DropdownOption; +}; + +const DropdownWrapper = styled.div` + width: 100%; + position: relative; +`; + +const SelectedItem = styled.div` + display: flex; + align-items: center; + cursor: pointer; + user-select: none; + + span { + margin-right: ${props => props.theme.spaces[1] + 1}px; + } +`; + +const OptionsWrapper = styled.div` + position: absolute; + margin-top: ${props => props.theme.spaces[8]}px; + left: -60px; + width: 200px; + display: flex; + flex-direction: column; + background-color: ${props => props.theme.colors.blackShades[3]}; + box-shadow: ${props => props.theme.spaces[0]}px + ${props => props.theme.spaces[5]}px ${props => props.theme.spaces[13] - 2}px + ${props => hexToRgba(props.theme.colors.blackShades[0], 0.75)}; +`; + +const DropdownOption = styled.div<{ + selected: DropdownOption; + option: DropdownOption; +}>` + display: flex; + flex-direction: column; + padding: 10px 12px; + cursor: pointer; + background-color: ${props => + props.option.label === props.selected.label + ? props.theme.colors.blackShades[4] + : "transparent"}; + + span:last-child { + margin-top: ${props => props.theme.spaces[1] + 1}px; + } + + &:hover { + span { + color: ${props => props.theme.colors.blackShades[9]}; + } + } +`; + +const TableDropdown = (props: DropdownProps) => { + const [selected, setSelected] = useState(props.selectedOption); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const dropdownHandler = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const optionSelector = (option: DropdownOption) => { + setSelected(option); + setIsDropdownOpen(false); + }; + + return ( + + dropdownHandler()}> + {selected.label} + + + {isDropdownOpen ? ( + + {props.options.map((el: DropdownOption, index: number) => ( + optionSelector(el)} + > + {el.label} + {el.value} + + ))} + + ) : null} + + ); +}; + +export default TableDropdown; diff --git a/app/client/src/components/stories/TableDropdown.stories.tsx b/app/client/src/components/stories/TableDropdown.stories.tsx new file mode 100644 index 0000000000..6756d92b73 --- /dev/null +++ b/app/client/src/components/stories/TableDropdown.stories.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import TableDropdown from "../ads/TableDropdown"; + +export default { + title: "Dropdown", + component: TableDropdown, + decorators: [withKnobs, withDesign], +}; + +const options = [ + { + label: "Admin", + value: "Can edit, view and invite other user to an app", + }, + { + label: "Developer", + value: "Can view and invite other user to an app", + }, + { + label: "User", + value: "Can view and invite other user to an app and...", + }, +]; + +export const TableDropdownStory = () => ( +
+ console.log(selectedValue)} + selectedOption={options[0]} + > +
+); diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 7a50182ef2..51911c992a 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -161,6 +161,8 @@ export const ReduxActionTypes: { [key: string]: string } = { DELETE_APPLICATION_SUCCESS: "DELETE_APPLICATION_SUCCESS", DELETE_PAGE_INIT: "DELETE_PAGE_INIT", DELETE_PAGE_SUCCESS: "DELETE_PAGE_SUCCESS", + CLONE_PAGE_INIT: "CLONE_PAGE_INIT", + CLONE_PAGE_SUCCESS: "CLONE_PAGE_SUCCESS", SET_DEFAULT_APPLICATION_PAGE_INIT: "SET_DEFAULT_APPLICATION_PAGE_INIT", SET_DEFAULT_APPLICATION_PAGE_SUCCESS: "SET_DEFAULT_APPLICATION_PAGE_SUCCESS", CREATE_ORGANIZATION_INIT: "CREATE_ORGANIZATION_INIT", @@ -312,6 +314,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { MOVE_ACTION_ERROR: "MOVE_ACTION_ERROR", COPY_ACTION_ERROR: "COPY_ACTION_ERROR", DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR", + CLONE_PAGE_ERROR: "CLONE_PAGE_ERROR", DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR", SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR", CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR", @@ -394,6 +397,13 @@ export interface Page { latest?: boolean; } +export interface ClonePageSuccessPayload { + pageName: string; + pageId: string; + layoutId: string; + isDefault: boolean; +} + export type PageListPayload = Array; export type ApplicationPayload = { diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx index 41e31c193c..e4c85b4f71 100644 --- a/app/client/src/icons/WidgetIcons.tsx +++ b/app/client/src/icons/WidgetIcons.tsx @@ -125,6 +125,11 @@ export const WidgetIcons: { ), + FORM_BUTTON_WIDGET: (props: IconProps) => ( + + + + ), }; export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons]; diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts index f21f1b4e48..e9b587e3ce 100644 --- a/app/client/src/jsExecution/RealmExecutor.ts +++ b/app/client/src/jsExecution/RealmExecutor.ts @@ -4,6 +4,7 @@ import { JSExecutorResult, } from "./JSExecutionManagerSingleton"; import JSONFn from "json-fn"; +import log from "loglevel"; declare let Realm: any; export default class RealmExecutor implements JSExecutor { @@ -104,7 +105,8 @@ export default class RealmExecutor implements JSExecutor { triggers, }; } catch (e) { - // console.error(`Error: "${e.message}" when evaluating {{${sourceText}}}`); + log.debug(`Error: "${e.message}" when evaluating {{${sourceText}}}`); + log.debug(e); return { result: undefined, triggers: [] }; } } diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index 06b1c0d821..776ab813f3 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -151,6 +151,7 @@ const Control = styled.div<{ fixed?: boolean }>` .${Classes.BUTTON_TEXT} { font-size: 12px; + color: white; } .more { @@ -269,7 +270,7 @@ export const ApplicationCard = (props: ApplicationCardProps) => { {hasEditPermission && (