diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js new file mode 100644 index 0000000000..b9fc71360c --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js @@ -0,0 +1,59 @@ +const testdata = require("../../../fixtures/testdata.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); +const explorer = require("../../../locators/explorerlocators.json"); +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); + +const pageid = "MyPage"; + +describe("Entity explorer Drag and Drop widgets testcases", function() { + it("Drag and drop form widget and validate", function() { + cy.log("Login Successful"); + cy.get(explorer.addWidget).click(); + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get(commonlocators.entityExplorersearch) + .clear() + .type("form"); + cy.dragAndDropToCanvas("formwidget"); + /** + * @param{Text} Random Text + * @param{FormWidget}Mouseover + * @param{FormPre Css} Assertion + */ + cy.widgetText( + "FormTest", + formWidgetsPage.formWidget, + formWidgetsPage.formInner, + ); + /** + * @param{Text} Random Colour + */ + cy.testCodeMirror(this.data.colour); + cy.get(formWidgetsPage.formD) + .should("have.css", "background-color") + .and("eq", this.data.rgbValue); + /** + * @param{toggleButton Css} Assert to be checked + */ + cy.togglebar(commonlocators.scrollView); + cy.get(formWidgetsPage.formD) + .scrollTo("bottom") + .should("be.visible"); + cy.get(commonlocators.editPropCrossButton).click(); + cy.get(explorer.closeWidgets).click(); + cy.PublishtheApp(); + cy.get(publish.backToEditor) + .first() + .click(); + cy.SearchEntityandOpen("FormTest"); + cy.get(explorer.property) + .last() + .click({ force: true }); + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(2); + expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}"); + expect($lis.eq(1)).to.contain("{{FormTest.data}}"); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/ConfirmRunAction_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/ConfirmRunAction_spec.js new file mode 100644 index 0000000000..b1e016724a --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/ConfirmRunAction_spec.js @@ -0,0 +1,49 @@ +const queryLocators = require("../../../locators/QueryEditor.json"); +const queryEditor = require("../../../locators/QueryEditor.json"); +let datasourceName; + +describe("Confirm run action", function() { + beforeEach(() => { + cy.createPostgresDatasource(); + cy.get("@createDatasource").then(httpResponse => { + datasourceName = httpResponse.response.body.data.name; + }); + }); + + it("Confirm run action", () => { + cy.NavigateToQueryEditor(); + + cy.get(".t--datasource-name") + .contains(datasourceName) + .click(); + cy.get(queryLocators.templateMenu).click(); + cy.get(".CodeMirror textarea") + .first() + .focus() + .type("select * from configs"); + cy.get("li:contains('Settings')").click({ force: true }); + cy.get("[data-cy=confirmBeforeExecute]") + .find(".bp3-switch") + .click(); + + cy.get(queryEditor.runQuery).click(); + cy.get(".bp3-dialog") + .find(".bp3-button") + .contains("Confirm") + .click(); + cy.wait("@postExecute").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + + cy.get(queryEditor.deleteQuery).click(); + cy.wait("@deleteAction").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + + cy.deletePostgresDatasource(datasourceName); + }); +}); diff --git a/app/client/cypress/locators/explorerlocators.json b/app/client/cypress/locators/explorerlocators.json index 3a74403c37..a416dfa58d 100644 --- a/app/client/cypress/locators/explorerlocators.json +++ b/app/client/cypress/locators/explorerlocators.json @@ -16,5 +16,8 @@ "property": ".language-appsmith-binding", "editNameField": ".editing input", "editEntityField": ".bp3-editable-text-input", - "entity":".t--entity-name" + "entity":".t--entity-name", + "addWidget":".widgets .t--entity-add-btn", + "dropHere":".appsmith_widget_0", + "closeWidgets":".t--close-widgets-sidebar" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 272139d441..7125e79cbd 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -34,6 +34,15 @@ Cypress.Commands.add("createOrg", orgName => { ); }); +Cypress.Commands.add( + "dragTo", + { prevSubject: "element" }, + (subject, targetEl) => { + cy.wrap(subject).trigger("dragstart"); + cy.get(targetEl).trigger("drop"); + }, +); + Cypress.Commands.add("navigateToOrgSettings", orgName => { cy.get(homePage.orgList.concat(orgName).concat(")")) .scrollIntoView() @@ -1307,6 +1316,30 @@ Cypress.Commands.add("fillPostgresDatasourceForm", () => { ); }); +Cypress.Commands.add("createPostgresDatasource", () => { + cy.NavigateToDatasourceEditor(); + cy.get(datasourceEditor.PostgreSQL).click(); + + cy.getPluginFormsAndCreateDatasource(); + + cy.fillPostgresDatasourceForm(); + + cy.testSaveDatasource(); +}); + +Cypress.Commands.add("deletePostgresDatasource", datasourceName => { + cy.NavigateToDatasourceEditor(); + cy.get(".t--entity-name:contains(PostgreSQL)").click(); + cy.get(`.t--entity-name:contains(${datasourceName})`).click(); + + cy.get(".t--delete-datasource").click(); + cy.wait("@deleteDatasource").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); +}); + Cypress.Commands.add("runQuery", () => { cy.get(queryEditor.runQuery).click(); cy.wait("@postExecute").should( @@ -1363,6 +1396,16 @@ Cypress.Commands.add("runAndDeleteQuery", () => { ); }); +Cypress.Commands.add("dragAndDropToCanvas", widgetType => { + const selector = `.t--widget-card-draggable-${widgetType}`; + cy.get(selector) + .trigger("mousedown", { button: 0 }, { force: true }) + .trigger("mousemove", 300, -300, { force: true }); + cy.get(explorer.dropHere) + .click() + .trigger("mouseup", { force: true }); +}); + Cypress.Commands.add("openPropertyPane", widgetType => { const selector = `.t--draggable-${widgetType}`; cy.get(selector) diff --git a/app/client/package.json b/app/client/package.json index 54319e0891..6ad9d2f312 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -16,8 +16,8 @@ "@craco/craco": "^5.6.1", "@manaflair/redux-batch": "^1.0.0", "@optimizely/optimizely-sdk": "^4.0.0", - "@sentry/react": "^5.22.2", - "@sentry/tracing": "^5.22.2", + "@sentry/react": "^5.22.3", + "@sentry/tracing": "^5.22.3", "@sentry/webpack-plugin": "^1.12.1", "@types/chance": "^1.0.7", "@types/lodash": "^4.14.120", diff --git a/app/client/src/components/ads/Checkbox.tsx b/app/client/src/components/ads/Checkbox.tsx index 7f7325bdee..8221668fa6 100644 --- a/app/client/src/components/ads/Checkbox.tsx +++ b/app/client/src/components/ads/Checkbox.tsx @@ -1,14 +1,100 @@ import { CommonComponentProps } from "./common"; +import React, { useState } from "react"; +import styled from "styled-components"; type CheckboxProps = CommonComponentProps & { label: string; - isChecked: boolean; - onCheckChange: (isChecked: boolean) => void; - isLoading: boolean; - align: "left" | "right"; - cypressSelector?: string; + onCheckChange?: (isChecked: boolean) => void; }; -export default function Checkbox(props: CheckboxProps) { - return null; -} +const Checkmark = styled.span<{ + disabled?: boolean; + isChecked?: boolean; +}>` + position: absolute; + top: 1px; + left: 0; + width: ${props => props.theme.spaces[8]}px; + height: ${props => props.theme.spaces[8]}px; + background-color: ${props => + props.isChecked + ? props.disabled + ? props.theme.colors.blackShades[3] + : props.theme.colors.info.main + : "transparent"}; + border: 2px solid + ${props => + props.isChecked + ? props.disabled + ? props.theme.colors.blackShades[3] + : props.theme.colors.info.main + : props.theme.colors.blackShades[4]}; + + &::after { + content: ""; + position: absolute; + display: none; + top: 0px; + left: 4px; + width: 6px; + height: 11px; + border: solid + ${props => + props.disabled ? "#565656" : props.theme.colors.blackShades[9]}; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } +`; + +const StyledCheckbox = styled.label<{ + disabled?: boolean; +}>` + position: relative; + display: block; + width: 100%; + cursor: ${props => (props.disabled ? "not-allowed" : "pointer")}; + font-weight: ${props => props.theme.typography.p1.fontWeight}; + font-size: ${props => props.theme.typography.p1.fontSize}px; + line-height: ${props => props.theme.typography.p1.lineHeight}px; + letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px; + color: ${props => props.theme.colors.blackShades[7]}; + padding-left: ${props => props.theme.spaces[12] - 2}px; + + input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + } + + input:checked ~ ${Checkmark}:after { + display: block; + } +`; + +const Checkbox = (props: CheckboxProps) => { + const [checked, setChecked] = useState(false); + + const onChangeHandler = (checked: boolean) => { + setChecked(checked); + props.onCheckChange && props.onCheckChange(checked); + }; + + return ( + + {props.label} + ) => + onChangeHandler(e.target.checked) + } + /> + + + ); +}; + +export default Checkbox; diff --git a/app/client/src/components/ads/Radio.tsx b/app/client/src/components/ads/Radio.tsx index e66b8693f1..7486b67c82 100644 --- a/app/client/src/components/ads/Radio.tsx +++ b/app/client/src/components/ads/Radio.tsx @@ -1,12 +1,154 @@ import { CommonComponentProps } from "./common"; +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; -type RadioProps = CommonComponentProps & { - align?: "horizontal" | "vertical" | "column" | "row"; - columns?: number; - rows?: number; - value?: string; +type OptionProps = { + label: string; + value: string; + disabled?: boolean; + onSelect?: (value: string) => void; }; -export default function Radio(props: RadioProps) { - return null; +type RadioProps = CommonComponentProps & { + columns?: number; + rows?: number; + defaultValue: string; + onSelect?: (value: string) => void; + options: OptionProps[]; +}; + +const RadioGroup = styled.div<{ + rows?: number; +}>` + display: flex; + flex-wrap: wrap; + ${props => + props.rows && props.rows > 0 + ? ` + flex-direction: column; + height: 100%; + ` + : null}; +`; + +const Radio = styled.label<{ + disabled?: boolean; + columns?: number; + rows?: number; +}>` + display: block; + position: relative; + padding-left: ${props => props.theme.spaces[12] - 2}px; + cursor: ${props => (props.disabled ? "not-allowed" : "pointer")}; + font-size: ${props => props.theme.typography.p1.fontSize}px; + font-weight: ${props => props.theme.typography.p1.fontWeight}; + line-height: ${props => props.theme.typography.p1.lineHeight}px; + letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px; + color: ${props => props.theme.colors.blackShades[9]}; + ${props => + props.rows && props.rows > 0 + ? `flex-basis: calc(100% / ${props.rows})` + : null}; + ${props => + props.columns && props.columns > 0 + ? ` + flex-basis: calc(100% / ${props.columns}); + margin-bottom: ${props.theme.spaces[11] + 1}px; + ` + : null}; + + input { + position: absolute; + opacity: 0; + cursor: pointer; + } + + .checkbox { + position: absolute; + top: 0; + left: 0; + width: ${props => props.theme.spaces[8]}px; + height: ${props => props.theme.spaces[8]}px; + background-color: transparent; + border: ${props => props.theme.spaces[1] - 2}px solid + ${props => props.theme.colors.blackShades[4]}; + border-radius: 50%; + margin-top: ${props => props.theme.spaces[0]}px; + } + + .checkbox:after { + content: ""; + position: absolute; + display: none; + } + + input:checked ~ .checkbox:after { + display: block; + } + + input:disabled ~ .checkbox:after { + background-color: ${props => props.theme.colors.radio.disabled}; + } + + .checkbox:after { + content: ""; + position: absolute; + width: ${props => props.theme.spaces[4]}px; + height: ${props => props.theme.spaces[4]}px; + ${props => + props.disabled + ? `background-color: ${props.theme.colors.radio.disabled}` + : `background-color: ${props.theme.colors.info.main};`}; + top: ${props => props.theme.spaces[1] - 2}px; + left: ${props => props.theme.spaces[1] - 2}px; + border-radius: 50%; + } +`; + +export default function RadioComponent(props: RadioProps) { + const [selected, setSelected] = useState(props.defaultValue); + + useEffect(() => { + if (props.rows && props.columns && props.rows > 0 && props.columns > 0) { + console.error( + "Please pass either rows prop or column prop but not both.", + ); + } + }, [props]); + + useEffect(() => { + setSelected(props.defaultValue); + }, [props.defaultValue]); + + const onChangeHandler = (value: string) => { + setSelected(value); + props.onSelect && props.onSelect(value); + }; + + return ( + onChangeHandler(e.target.value)} + > + {props.options.map((option: OptionProps, index: number) => ( + + {option.label} + option.onSelect && option.onSelect(e.target.value)} + checked={selected === option.value} + name="radio" + /> + + + ))} + + ); } diff --git a/app/client/src/components/ads/Spinner.tsx b/app/client/src/components/ads/Spinner.tsx index 9adc4101ad..2f40c640a1 100644 --- a/app/client/src/components/ads/Spinner.tsx +++ b/app/client/src/components/ads/Spinner.tsx @@ -1,6 +1,7 @@ import React from "react"; import styled, { keyframes } from "styled-components"; import { sizeHandler, IconSize } from "./Icon"; +import { Classes } from "./common"; const rotate = keyframes` 100% { @@ -23,14 +24,14 @@ const dash = keyframes` } `; -const SvgContainer = styled("svg")` +const SvgContainer = styled.svg` animation: ${rotate} 2s linear infinite; width: ${props => sizeHandler(props.size)}px; height: ${props => sizeHandler(props.size)}px; `; -const SvgCircle = styled("circle")` - stroke: white; +const SvgCircle = styled.circle` + stroke: ${props => props.theme.colors.blackShades[9]}; stroke-linecap: round; animation: ${dash} 1.5s ease-in-out infinite; stroke-width: ${props => props.theme.spaces[1]}px; @@ -46,7 +47,11 @@ Spinner.defaultProp = { export default function Spinner(props: SpinnerProp) { return ( - + ); diff --git a/app/client/src/components/ads/Toggle.tsx b/app/client/src/components/ads/Toggle.tsx index 2146ba2179..1b1dc1de23 100644 --- a/app/client/src/components/ads/Toggle.tsx +++ b/app/client/src/components/ads/Toggle.tsx @@ -1,10 +1,149 @@ -import { CommonComponentProps } from "./common"; +import { CommonComponentProps, Classes, lighten, darken } from "./common"; +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; +import Spinner from "./Spinner"; type ToggleProps = CommonComponentProps & { onToggle: (value: boolean) => void; value: boolean; }; +const StyledToggle = styled.label<{ + isLoading?: boolean; + disabled?: boolean; + value: boolean; +}>` + position: relative; + display: block; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + background-color: ${props => + props.isLoading + ? props.theme.colors.blackShades[3] + : props.theme.colors.blackShades[4]}; + transition: 0.4s; + width: 46px; + height: 23px; + border-radius: 92px; + } + + ${props => + props.isLoading + ? `.toggle-spinner { + position: absolute; + top: 3px; + left: 17px; + } + .slider:before { + display: none; + }` + : `.slider:before { + position: absolute; + content: ""; + height: 19px; + width: 19px; + top: 2px; + left: 2px; + background-color: ${ + props.disabled && !props.value + ? lighten(props.theme.colors.tertiary.dark, 16) + : props.theme.colors.blackShades[9] + }; + box-shadow: ${ + props.value + ? "1px 0px 3px rgba(0, 0, 0, 0.16)" + : "-1px 0px 3px rgba(0, 0, 0, 0.16)" + }; + opacity: ${props.value ? 1 : 0.9}; + transition: .4s; + border-radius: 50%; + }`} + + && input:hover + .slider:before { + opacity: 1; + } + + input:focus + .slider:before { + ${props => (props.value ? "opacity: 0.6" : "opacity: 0.7")}; + } + + input:disabled + .slider:before { + ${props => (props.value ? "opacity: 0.24" : "opacity: 1")}; + } + + input:checked + .slider:before { + transform: translateX(23px); + } + + input:checked + .slider { + background-color: ${props => props.theme.colors.info.main}; + } + + input:hover + .slider, + input:focus + .slider { + background-color: ${props => + props.value + ? lighten(props.theme.colors.info.main, 12) + : lighten(props.theme.colors.blackShades[3], 16)}; + } + + input:disabled + .slider { + cursor: not-allowed; + background-color: ${props => + props.value && !props.isLoading + ? darken(props.theme.colors.info.darker, 20) + : props.theme.colors.tertiary.dark}; + } + + .${Classes.SPINNER} { + circle { + stroke: ${props => props.theme.colors.blackShades[6]}; + } + } +`; + export default function Toggle(props: ToggleProps) { - return null; + const [value, setValue] = useState(false); + + useEffect(() => { + setValue(props.value); + }, [props.value]); + + const onChangeHandler = (value: boolean) => { + setValue(value); + props.onToggle && props.onToggle(value); + }; + + return ( + + ) => + onChangeHandler(e.target.checked) + } + /> + + {props.isLoading ? ( +
+ +
+ ) : null} +
+ ); } diff --git a/app/client/src/components/ads/Tooltip.tsx b/app/client/src/components/ads/Tooltip.tsx new file mode 100644 index 0000000000..1a021f7b8d --- /dev/null +++ b/app/client/src/components/ads/Tooltip.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { CommonComponentProps } from "./common"; +import styled from "styled-components"; +import { Position, Tooltip, Classes } from "@blueprintjs/core"; +import { Classes as CsClasses } from "./common"; + +type Variant = "dark" | "light"; + +type TooltipProps = CommonComponentProps & { + content: JSX.Element | string; + position?: Position; + children: JSX.Element; + variant?: Variant; +}; + +const TooltipWrapper = styled.div<{ variant?: Variant }>` + .${Classes.TOOLTIP} .${Classes.POPOVER_CONTENT} { + padding: 10px 12px; + border-radius: 0px; + background-color: ${props => + props.variant === "dark" + ? props.theme.colors.blackShades[0] + : props.theme.colors.blackShades[8]}; + } + div.${Classes.POPOVER_ARROW} { + display: block; + } + .${Classes.TOOLTIP} { + box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.35);a + } + .${Classes.TOOLTIP} .${CsClasses.BP3_POPOVER_ARROW_BORDER}, + &&&& .${Classes.TOOLTIP} .${CsClasses.BP3_POPOVER_ARROW_FILL} { + fill: ${props => + props.variant === "dark" + ? props.theme.colors.blackShades[0] + : props.theme.colors.blackShades[8]}; + } +`; + +const TooltipComponent = (props: TooltipProps) => { + return ( + + + {props.children} + + + ); +}; + +TooltipComponent.defaultProps = { + position: Position.TOP, + variant: "dark", +}; + +export default TooltipComponent; diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx index 4f779bebb0..a0e3f44750 100644 --- a/app/client/src/components/ads/common.tsx +++ b/app/client/src/components/ads/common.tsx @@ -1,4 +1,5 @@ import { Theme } from "constants/DefaultTheme"; +import tinycolor from "tinycolor2"; import styled from "styled-components"; export interface CommonComponentProps { @@ -14,6 +15,9 @@ export type ThemeProp = { export enum Classes { ICON = "cs-icon", TEXT = "cs-text", + BP3_POPOVER_ARROW_BORDER = "bp3-popover-arrow-border", + BP3_POPOVER_ARROW_FILL = "bp3-popover-arrow-fill", + SPINNER = "cs-spinner", } export const hexToRgb = ( @@ -42,6 +46,17 @@ export const hexToRgba = (color: string, alpha: number) => { return `rgba(${value.r}, ${value.g}, ${value.b}, ${alpha});`; }; +export const lighten = (color: string, amount: number) => { + return tinycolor(color) + .lighten(amount) + .toString(); +}; + +export const darken = (color: string, amount: number) => { + return tinycolor(color) + .darken(amount) + .toString(); +}; export const StoryWrapper = styled.div` background: #1a191c; height: 700px; diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index 87055ee567..7ec12d1fc7 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -54,7 +54,7 @@ interface ReactTableComponentProps { serverSidePaginationEnabled: boolean; columnActions?: ColumnAction[]; selectedRowIndex: number; - selectedRowIndexes: number[]; + selectedRowIndices: number[]; multiRowSelection?: boolean; hiddenColumns?: string[]; columnNameMap?: { [key: string]: string }; @@ -301,7 +301,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }} serverSidePaginationEnabled={props.serverSidePaginationEnabled} selectedRowIndex={props.selectedRowIndex} - selectedRowIndexes={props.selectedRowIndexes} + selectedRowIndices={props.selectedRowIndices} disableDrag={() => { props.disableDrag(true); }} diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 91ac503b64..824b33f5e3 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -49,7 +49,7 @@ interface TableProps { prevPageClick: () => void; serverSidePaginationEnabled: boolean; selectedRowIndex: number; - selectedRowIndexes: number[]; + selectedRowIndices: number[]; disableDrag: () => void; enableDrag: () => void; searchTableData: (searchKey: any) => void; @@ -111,7 +111,7 @@ export const Table = (props: TableProps) => { } const subPage = page.slice(startIndex, endIndex); const selectedRowIndex = props.selectedRowIndex; - const selectedRowIndexes = props.selectedRowIndexes; + const selectedRowIndices = props.selectedRowIndices; const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; /* Subtracting 9px to handling widget padding */ return ( @@ -205,7 +205,7 @@ export const Table = (props: TableProps) => { "tr" + `${ row.index === selectedRowIndex || - selectedRowIndexes.includes(row.index) + selectedRowIndices.includes(row.index) ? " selected-row" : "" }` @@ -215,7 +215,7 @@ export const Table = (props: TableProps) => { props.selectTableRow( row, row.index === selectedRowIndex || - selectedRowIndexes.includes(row.index), + selectedRowIndices.includes(row.index), ); }} key={rowIndex} diff --git a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx index 710a6bcd01..ccace89270 100644 --- a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx +++ b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx @@ -13,8 +13,11 @@ import { getAppsmithConfigs } from "configs"; import { LayersContext } from "constants/Layers"; import { connect } from "react-redux"; import { AppState } from "reducers"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { User } from "constants/userConstants"; +import AnalyticsUtil from "utils/AnalyticsUtil"; -const { algolia } = getAppsmithConfigs(); +const { algolia, cloudHosting, intercomAppID } = getAppsmithConfigs(); const HelpButton = styled.button<{ highlight: boolean; layer: number; @@ -54,10 +57,29 @@ const HelpIcon = HelpIcons.HELP_ICON; type Props = { isHelpModalOpen: boolean; dispatch: any; + user?: User; + page: string; }; class HelpModal extends React.Component { static contextType = LayersContext; + + componentDidMount() { + const { user } = this.props; + if (cloudHosting && intercomAppID && window.Intercom) { + window.Intercom("boot", { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: intercomAppID, + // eslint-disable-next-line @typescript-eslint/camelcase + user_id: user?.username, + // eslint-disable-next-line @typescript-eslint/camelcase + custom_launcher_selector: "#intercom-trigger", + name: user?.name, + email: user?.email, + }); + } + } + render() { const { dispatch, isHelpModalOpen } = this.props; const layers = this.context; @@ -91,6 +113,7 @@ class HelpModal extends React.Component { highlight={!isHelpModalOpen} layer={layers.help} onClick={() => { + AnalyticsUtil.logEvent("OPEN_HELP", { page: this.props.page }); dispatch(setHelpModalVisibility(!isHelpModalOpen)); }} > @@ -104,6 +127,7 @@ class HelpModal extends React.Component { const mapStateToProps = (state: AppState) => ({ isHelpModalOpen: getHelpModalOpen(state), + user: getCurrentUser(state), }); export default connect(mapStateToProps)(HelpModal); diff --git a/app/client/src/components/formControls/SwitchControl.tsx b/app/client/src/components/formControls/SwitchControl.tsx index e9e2cae7d5..0946f856e2 100644 --- a/app/client/src/components/formControls/SwitchControl.tsx +++ b/app/client/src/components/formControls/SwitchControl.tsx @@ -33,7 +33,7 @@ export class SwitchField extends React.Component { return (
- + {label} {isRequired && "*"} diff --git a/app/client/src/components/stories/Button.stories.tsx b/app/client/src/components/stories/Button.stories.tsx index 22bcb3b19a..4f6a7f7931 100644 --- a/app/client/src/components/stories/Button.stories.tsx +++ b/app/client/src/components/stories/Button.stories.tsx @@ -3,7 +3,7 @@ import Button, { Size, Category, Variant } from "components/ads/Button"; import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; import { StoryWrapper } from "components/ads/common"; -import { IconCollection } from "components/ads/Icon"; +import { IconCollection, IconName } from "components/ads/Icon"; export default { title: "Button", @@ -17,7 +17,11 @@ export const withDynamicProps = () => ( size={select("size", Object.values(Size), Size.large)} category={select("category", Object.values(Category), Category.primary)} variant={select("variant", Object.values(Variant), Variant.info)} - icon={select("Icon name", IconCollection, undefined)} + icon={select( + "Icon name", + ["Select icon" as IconName, ...IconCollection], + "Select icon" as IconName, + )} isLoading={boolean("Loading", false)} disabled={boolean("Disabled", false)} text={text("text", "Get")} diff --git a/app/client/src/components/stories/Checkbox.stories.tsx b/app/client/src/components/stories/Checkbox.stories.tsx new file mode 100644 index 0000000000..befd978777 --- /dev/null +++ b/app/client/src/components/stories/Checkbox.stories.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { withKnobs, boolean, text } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import { action } from "@storybook/addon-actions"; +import Checkbox from "components/ads/Checkbox"; +import { StoryWrapper } from "components/ads/common"; + +export default { + title: "Checkbox", + component: Checkbox, + decorators: [withKnobs, withDesign], +}; + +export const CustomCheckbox = () => ( + + + +); diff --git a/app/client/src/components/stories/Icon.stories.tsx b/app/client/src/components/stories/Icon.stories.tsx index 90b1b5be7e..303738ed15 100644 --- a/app/client/src/components/stories/Icon.stories.tsx +++ b/app/client/src/components/stories/Icon.stories.tsx @@ -28,18 +28,7 @@ export const ButtonIcon = () => ( export const BordelessIcon = () => ( @@ -48,11 +37,7 @@ export const BordelessIcon = () => ( export const AppIconVariant = () => ( { W} disabled={boolean("First option disabled", false)} @@ -112,7 +116,11 @@ export const MenuStory = () => { {boolean("First menu item divider", false) ? : null} W} /> diff --git a/app/client/src/components/stories/Radio.stories.tsx b/app/client/src/components/stories/Radio.stories.tsx new file mode 100644 index 0000000000..ff0d9480b5 --- /dev/null +++ b/app/client/src/components/stories/Radio.stories.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { + withKnobs, + select, + boolean, + text, + number, +} from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import { StoryWrapper } from "components/ads/common"; +import RadioComponent from "components/ads/Radio"; +import { action } from "@storybook/addon-actions"; + +export default { + title: "Radio", + component: RadioComponent, + decorators: [withKnobs, withDesign], +}; + +export const Radio = () => ( + +
+ +
+
+); diff --git a/app/client/src/components/stories/Tabs.stories.tsx b/app/client/src/components/stories/Tabs.stories.tsx index 6b7e97a9c4..17eb511ef8 100644 --- a/app/client/src/components/stories/Tabs.stories.tsx +++ b/app/client/src/components/stories/Tabs.stories.tsx @@ -2,7 +2,7 @@ import React from "react"; import { TabComponent, TabProp } from "components/ads/Tabs"; import { select, text, withKnobs } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { IconCollection } from "components/ads/Icon"; +import { IconCollection, IconName } from "components/ads/Icon"; import { StoryWrapper } from "components/ads/common"; export default { @@ -87,13 +87,29 @@ const TabStory = (props: any) => { export const Tabs = () => ( ); diff --git a/app/client/src/components/stories/Toggle.stories.tsx b/app/client/src/components/stories/Toggle.stories.tsx new file mode 100644 index 0000000000..7a0d8ade16 --- /dev/null +++ b/app/client/src/components/stories/Toggle.stories.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { withKnobs, boolean } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import { action } from "@storybook/addon-actions"; +import Toggle from "components/ads/Toggle"; +import { StoryWrapper } from "components/ads/common"; + +export default { + title: "Toggle", + component: Toggle, + decorators: [withKnobs, withDesign], +}; + +export const CustomToggle = () => ( + + + +); diff --git a/app/client/src/components/stories/Tooltip.stories.tsx b/app/client/src/components/stories/Tooltip.stories.tsx new file mode 100644 index 0000000000..c5cddb6cc3 --- /dev/null +++ b/app/client/src/components/stories/Tooltip.stories.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { select, withKnobs } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import { Position } from "@blueprintjs/core"; +import TooltipComponent from "components/ads/Tooltip"; +import { StoryWrapper } from "components/ads/common"; +import Text, { TextType } from "components/ads/Text"; +import Button from "components/ads/Button"; + +export default { + title: "Tooltip", + component: TooltipComponent, + decorators: [withKnobs, withDesign], +}; + +export const MenuStory = () => ( + +
+ + This is a tooltip + + } + variant={select("variant", ["dark", "light"], "dark")} + > + + Hover to show tooltip + + +
+
+); diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 2d110ce332..32471b2d9f 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -572,6 +572,9 @@ export const theme: Theme = { darker: "#2B1A1D", darkest: "#462F32", }, + radio: { + disabled: "#565656", + }, primaryOld: Colors.GREEN, primaryDarker: Colors.JUNGLE_GREEN, primaryDarkest: Colors.JUNGLE_GREEN_DARKER, diff --git a/app/client/src/constants/defs/moment.json b/app/client/src/constants/defs/moment.json new file mode 100644 index 0000000000..94c32a003b --- /dev/null +++ b/app/client/src/constants/defs/moment.json @@ -0,0 +1,109 @@ +{ + "!name": "moment", + "moment": { + "!type": "fn(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean) -> Moment", + "!url": "https://momentjs.com/docs/#/parsing/", + "!doc": "Returns a wrapper for the Date object", + "now": { + "!type": "fn() -> number", + "!doc": "Returns unix time in milliseconds." + } + }, + "Moment": { + "isValid": { + "!type": "fn() -> bool", + "!url": "https://momentjs.com/docs/#/parsing/is-valid/", + "!doc": "Check whether the Moment considers the date invalid" + }, + "add": { + "!type": "fn(amount?: DurationInputArg1, unit?: DurationInputArg2)", + "!url": "https://momentjs.com/docs/#/manipulating/add/", + "!doc": "Mutates the original moment by adding time." + }, + "format": { + "!type": "fn(format?: string) -> string", + "!url": "https://momentjs.com/docs/#/displaying/format/", + "!doc": "Takes a string of tokens and replaces them with their corresponding values" + }, + "isAfter": { + "!type": "fn(inp?: MomentInput, granularity?: ?) -> bool", + "!url": "https://momentjs.com/docs/#/query/is-after/", + "!doc": "Check if a moment is after another moment." + }, + "isSame": { + "!type": "fn(inp?: MomentInput, granularity?: ?) -> bool", + "!url": "https://momentjs.com/docs/#/query/is-same/", + "!doc": "Check if a moment is the same as another moment." + }, + "isBefore": { + "!type": "fn(inp?: MomentInput, granularity?: ?) -> bool", + "!url": "https://momentjs.com/docs/#/query/is-before/", + "!doc": "Check if a moment is before another moment" + }, + "fromNow": { + "!type": "fn(withoutSuffix?: bool) -> string", + "!url": "https://momentjs.com/docs/#/displaying/fromnow/", + "!doc": "Get relative time" + }, + "clone": { + "!type": "fn() -> Moment", + "!url": "https://momentjs.com/docs/#/parsing/moment-clone/", + "!doc": "Returns the clone of a moment," + }, + "year": { + "!type": "fn(y: number) -> Moment", + "!url": "https://momentjs.com/docs/#/get-set/year/", + "!doc": "Gets or sets the year" + }, + "month": { + "!type": "fn() -> number", + "!url": "https://momentjs.com/docs/#/get-set/month/", + "!doc": "Gets or sets the month" + }, + "day": { + "!type": "fn() -> number", + "!url": "https://momentjs.com/docs/#/get-set/day/", + "!doc": "Gets or sets the day of the week." + }, + "date": { + "!type": "fn() -> number", + "!url": "https://momentjs.com/docs/#/get-set/date/", + "!doc": "Gets or sets the day of the month." + }, + "hour": { + "!type": "fn() -> number", + "!url": "https://momentjs.com/docs/#/get-set/hour/", + "!doc": "Gets or sets the hour." + }, + "minute": { + "!type": "fn() -> number", + "!url": "https://momentjs.com/docs/#/get-set/minute/", + "!doc": "Gets or sets the minutes." + }, + "second": { + "!type": "fn([s])", + "!url": "https://momentjs.com/docs/#/get-set/second/", + "!doc": "Gets or sets the seconds." + }, + "millisecond": { + "!type": "fn([ms])", + "!url": "https://momentjs.com/docs/#/get-set/millisecond/", + "!doc": "Gets or sets the milliseconds." + }, + "get": { + "!type": "fn(unit: ?) -> number", + "!url": "https://momentjs.com/docs/#/get-set/get/", + "!doc": "String getter" + }, + "set": { + "!type": "fn(unit: ?, value: number) -> Moment", + "!url": "https://momentjs.com/docs/#/get-set/set/", + "!doc": "Generic setter, accepting unit as first argument, and value as second" + }, + "toDate": { + "!type": "fn()", + "!url": "https://momentjs.com/docs/#/displaying/as-javascript-date/", + "!doc": "Get a copy of the native Date object that Moment.js wraps" + } + } +} \ No newline at end of file diff --git a/app/client/src/constants/userConstants.ts b/app/client/src/constants/userConstants.ts index ea0e91914d..1eb644e959 100644 --- a/app/client/src/constants/userConstants.ts +++ b/app/client/src/constants/userConstants.ts @@ -1,12 +1,15 @@ export const ANONYMOUS_USERNAME = "anonymousUser"; +type Gender = "MALE" | "FEMALE"; + export type User = { - id: string; email: string; currentOrganizationId: string; organizationIds: string[]; applications: UserApplication[]; username: string; + name: string; + gender: Gender; }; export interface UserApplication { diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts index 73f19713fe..c913d8283b 100644 --- a/app/client/src/entities/Action/index.ts +++ b/app/client/src/entities/Action/index.ts @@ -71,6 +71,7 @@ export interface Action { providerId?: string; provider?: ActionProvider; documentation?: { text: string }; + confirmBeforeExecute?: boolean; } export interface RestAction extends Action { diff --git a/app/client/src/mockResponses/ActionSettings.tsx b/app/client/src/mockResponses/ActionSettings.tsx index 036ec9cdad..2a01006e46 100644 --- a/app/client/src/mockResponses/ActionSettings.tsx +++ b/app/client/src/mockResponses/ActionSettings.tsx @@ -9,12 +9,29 @@ export const queryActionSettingsConfig = [ controlType: "SWITCH", info: "Will refresh data everytime page is reloaded", }, + { + label: "Request confirmation before running query", + configProperty: "confirmBeforeExecute", + controlType: "SWITCH", + info: "Ask confirmation from the user everytime before refreshing data", + }, // { - // label: "Request confirmation before running query", - // configProperty: "requestConfirmation", + // label: "Cache response", + // configProperty: "shouldCacheResponse", // controlType: "SWITCH", - // info: "Ask confirmation from the user everytime before refreshing data", // }, + // { + // label: "Cache timeout (in milliseconds)", + // configProperty: "cacheTimeout", + // controlType: "INPUT_TEXT", + // dataType: "NUMBER", + // }, + { + label: "Query Timeout (in milliseconds)", + configProperty: "actionConfiguration.timeoutInMillisecond", + controlType: "INPUT_TEXT", + dataType: "NUMBER", + }, ], }, ]; @@ -24,18 +41,35 @@ export const apiActionSettingsConfig = [ sectionName: "", id: 1, children: [ + // { + // label: "Run api on Page load", + // configProperty: "executeOnLoad", + // controlType: "SWITCH", + // info: "Will refresh data everytime page is reloaded", + // }, { - label: "Run api on Page load", - configProperty: "executeOnLoad", + label: "Request confirmation before running api", + configProperty: "confirmBeforeExecute", controlType: "SWITCH", - info: "Will refresh data everytime page is reloaded", + info: "Ask confirmation from the user everytime before refreshing data", }, // { - // label: "Request confirmation before running api", - // configProperty: "requestConfirmation", + // label: "Cache response", + // configProperty: "shouldCacheResponse", // controlType: "SWITCH", - // info: "Ask confirmation from the user everytime before refreshing data", // }, + // { + // label: "Cache timeout (in milliseconds)", + // configProperty: "cacheTimeout", + // controlType: "INPUT_TEXT", + // dataType: "NUMBER", + // }, + { + label: "Api Timeout (in milliseconds)", + configProperty: "actionConfiguration.timeoutInMillisecond", + controlType: "INPUT_TEXT", + dataType: "NUMBER", + }, ], }, ]; diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index 9858a0fb03..e0ead7b216 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -39,6 +39,7 @@ import { import { Directions } from "utils/helpers"; import { HeaderIcons } from "icons/HeaderIcons"; import { duplicateApplication } from "actions/applicationActions"; +import HelpModal from "components/designSystems/appsmith/help/HelpModal"; const OrgDropDown = styled.div` display: flex; @@ -337,6 +338,7 @@ class Applications extends Component< ); })} + ); } diff --git a/app/client/src/pages/Editor/ConfirmRunModal.tsx b/app/client/src/pages/Editor/ConfirmRunModal.tsx index ce4aa9facf..5d9f572092 100644 --- a/app/client/src/pages/Editor/ConfirmRunModal.tsx +++ b/app/client/src/pages/Editor/ConfirmRunModal.tsx @@ -19,12 +19,14 @@ class ConfirmRunModal extends React.Component { const { dispatch, isModalOpen } = this.props; const handleClose = () => { dispatch(showRunActionConfirmModal(false)); + + dispatch(cancelRunActionConfirmModal()); }; return ( - +
- Are you sure you want to refresh your current data + Are you sure you want to perform this action?
@@ -39,7 +41,7 @@ class ConfirmRunModal extends React.Component { />