diff --git a/app/client/cypress/fixtures/formResetDsl.json b/app/client/cypress/fixtures/formResetDsl.json new file mode 100644 index 00000000000..c40bb6d5c20 --- /dev/null +++ b/app/client/cypress/fixtures/formResetDsl.json @@ -0,0 +1,297 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": { + }, + "version": 5, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "widgetName": "Form1", + "backgroundColor": "white", + "children": [ + { + "isVisible": true, + "widgetName": "Canvas1", + "containerStyle": "none", + "canExtend": false, + "detachFromLayout": true, + "children": [ + { + "isVisible": true, + "text": "Form", + "textStyle": "HEADING", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 0, + "rightColumn": 12, + "topRow": 0, + "bottomRow": 1, + "parentId": "qrqizehc5b", + "widgetId": "c481ah2q0i" + }, + { + "isVisible": true, + "widgetName": "FormButton1", + "text": "Submit", + "isDefaultClickDisabled": true, + "buttonStyle": "PRIMARY_BUTTON", + "disabledWhenInvalid": true, + "resetFormOnClick": true, + "type": "FORM_BUTTON_WIDGET", + "isLoading": false, + "leftColumn": 12, + "rightColumn": 16, + "topRow": 12, + "bottomRow": 13, + "parentId": "qrqizehc5b", + "widgetId": "zsu1y41p1e" + }, + { + "isVisible": true, + "widgetName": "FormButton2", + "text": "Reset", + "isDefaultClickDisabled": true, + "buttonStyle": "SECONDARY_BUTTON", + "disabledWhenInvalid": false, + "resetFormOnClick": true, + "type": "FORM_BUTTON_WIDGET", + "isLoading": false, + "leftColumn": 8, + "rightColumn": 12, + "topRow": 12, + "bottomRow": 13, + "parentId": "qrqizehc5b", + "widgetId": "7o0r2rp3s1" + }, + { + "isVisible": true, + "label": "Data", + "widgetName": "Table1", + "searchKey": "", + "tableData": "[\n {\n \"id\": 2381224,\n \"email\": \"michael.lawson@reqres.in\",\n \"userName\": \"Michael Lawson\",\n \"productName\": \"Chicken Sandwich\",\n \"orderAmount\": 4.99\n },\n {\n \"id\": 2736212,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"userName\": \"Lindsay Ferguson\",\n \"productName\": \"Tuna Salad\",\n \"orderAmount\": 9.99\n },\n {\n \"id\": 6788734,\n \"email\": \"tobias.funke@reqres.in\",\n \"userName\": \"Tobias Funke\",\n \"productName\": \"Beef steak\",\n \"orderAmount\": 19.99\n }\n]", + "type": "TABLE_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.5, + "parentRowSpace": 40, + "leftColumn": 0, + "rightColumn": 16, + "topRow": 1, + "bottomRow": 8, + "parentId": "qrqizehc5b", + "widgetId": "xptqefixji", + "dynamicBindings": { + } + }, + { + "isVisible": true, + "inputType": "TEXT", + "label": "", + "widgetName": "Input1", + "type": "INPUT_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.5, + "parentRowSpace": 40, + "leftColumn": 0, + "rightColumn": 5, + "topRow": 10, + "bottomRow": 11, + "parentId": "qrqizehc5b", + "widgetId": "r3xvjtuhad", + "dynamicBindings": { + "defaultText": true, + "isValid": true, + "value": true + }, + "defaultText": "{{Table1.selectedRow.email}}" + }, + { + "isVisible": true, + "text": "Email", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text2", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.5, + "parentRowSpace": 40, + "leftColumn": 0, + "rightColumn": 4, + "topRow": 9, + "bottomRow": 10, + "parentId": "qrqizehc5b", + "widgetId": "672gf8vm2q", + "dynamicBindings": { + "value": true + } + } + ], + "blueprint": { + "view": [ + { + "type": "TEXT_WIDGET", + "size": { + "rows": 1, + "cols": 12 + }, + "position": { + "top": 0, + "left": 0 + }, + "props": { + "text": "Form", + "textStyle": "HEADING" + } + }, + { + "type": "FORM_BUTTON_WIDGET", + "size": { + "rows": 1, + "cols": 4 + }, + "position": { + "top": 11, + "left": 12 + }, + "props": { + "text": "Submit", + "buttonStyle": "PRIMARY_BUTTON", + "disabledWhenInvalid": true, + "resetFormOnClick": true + } + }, + { + "type": "FORM_BUTTON_WIDGET", + "size": { + "rows": 1, + "cols": 4 + }, + "position": { + "top": 11, + "left": 8 + }, + "props": { + "text": "Reset", + "buttonStyle": "SECONDARY_BUTTON", + "disabledWhenInvalid": false, + "resetFormOnClick": true + } + } + ] + }, + "minHeight": 520, + "type": "CANVAS_WIDGET", + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 518, + "topRow": 0, + "bottomRow": 520, + "parentId": "ozm6zwjk4b", + "widgetId": "qrqizehc5b" + } + ], + "blueprint": { + "view": [ + { + "type": "CANVAS_WIDGET", + "position": { + "top": 0, + "left": 0 + }, + "props": { + "containerStyle": "none", + "canExtend": false, + "detachFromLayout": true, + "children": [ + ], + "blueprint": { + "view": [ + { + "type": "TEXT_WIDGET", + "size": { + "rows": 1, + "cols": 12 + }, + "position": { + "top": 0, + "left": 0 + }, + "props": { + "text": "Form", + "textStyle": "HEADING" + } + }, + { + "type": "FORM_BUTTON_WIDGET", + "size": { + "rows": 1, + "cols": 4 + }, + "position": { + "top": 11, + "left": 12 + }, + "props": { + "text": "Submit", + "buttonStyle": "PRIMARY_BUTTON", + "disabledWhenInvalid": true, + "resetFormOnClick": true + } + }, + { + "type": "FORM_BUTTON_WIDGET", + "size": { + "rows": 1, + "cols": 4 + }, + "position": { + "top": 11, + "left": 8 + }, + "props": { + "text": "Reset", + "buttonStyle": "SECONDARY_BUTTON", + "disabledWhenInvalid": false, + "resetFormOnClick": true + } + } + ] + } + } + } + ] + }, + "type": "FORM_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 0, + "rightColumn": 16, + "topRow": 0, + "bottomRow": 14, + "parentId": "0", + "widgetId": "ozm6zwjk4b" + } + ] + } +} + diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js new file mode 100644 index 00000000000..eea5f26e118 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js @@ -0,0 +1,32 @@ +const dsl = require("../../../fixtures/formResetDsl.json"); +const widgetsPage = require("../../../locators/Widgets.json"); + +describe("Form reset functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Resets the form ", () => { + cy.get(".tr") + .eq(2) + .click() + .should("have.class", "selected-row"); + + cy.get(widgetsPage.inputWidget + " " + "input") + .invoke("attr", "value") + .should("contain", "lindsay.ferguson@reqres.in"); + + cy.get(widgetsPage.formButtonWidget) + .contains("Reset") + .click(); + + cy.get(".tr") + .eq(2) + .click() + .should("not.have.class", "selected-row"); + + cy.get(widgetsPage.inputWidget + " " + "input") + .invoke("attr", "value") + .should("not.contain", "lindsay.ferguson@reqres.in"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js index 80ea044b58a..83f696674d0 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js @@ -1,8 +1,10 @@ const queryLocators = require("../../../locators/QueryEditor.json"); const datasource = require("../../../locators/DatasourcesEditor.json"); +let datasourceName; + describe("Create a query with a postgres datasource, run, save and then delete the query", function() { - it("Create a query with a postgres datasource, run, save and then delete the query", function() { + it("Create a postgres datasource", function() { cy.NavigateToDatasourceEditor(); cy.get(datasource.PostgreSQL).click(); @@ -12,38 +14,43 @@ describe("Create a query with a postgres datasource, run, save and then delete t cy.testSaveDatasource(); - cy.NavigateToQueryEditor(); - cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; - - cy.get(".t--datasource-name") - .contains(datasourceName) - .click(); + datasourceName = httpResponse.response.body.data.name; }); - - cy.get("@getPluginForm").should( - "have.nested.property", - "response.body.responseMeta.status", - 200, - ); + }); + it("Create, runs and delete a query", () => { + cy.NavigateToQueryEditor(); + cy.get(".t--datasource-name") + .contains(datasourceName) + .click(); cy.get(queryLocators.templateMenu).click(); cy.get(".CodeMirror textarea") .first() .focus() - .type("select * from users"); + .type("select * from users limit 10"); - cy.EvaluateCurrentValue("select * from users"); + cy.EvaluateCurrentValue("select * from users limit 10"); cy.runAndDeleteQuery(); + }); + it("Create, runs and delete another query", () => { + 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.EvaluateCurrentValue("select * from configs"); + cy.runAndDeleteQuery(); + }); + it("Deletes a datasource", () => { cy.NavigateToDatasourceEditor(); cy.get(".t--entity-name:contains(PostgreSQL)").click(); - cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; - - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); - }); + cy.get(`.t--entity-name:contains(${datasourceName})`).click(); cy.get(".t--delete-datasource").click(); cy.wait("@deleteDatasource").should( diff --git a/app/client/cypress/locators/Widgets.json b/app/client/cypress/locators/Widgets.json index 30d39512e6e..0888b882903 100644 --- a/app/client/cypress/locators/Widgets.json +++ b/app/client/cypress/locators/Widgets.json @@ -5,6 +5,7 @@ "inputPropsDataType": ".t--property-control-datatype", "inputdatatypeplaceholder": ".t--property-control-placeholder", "buttonWidget": ".t--draggable-buttonwidget", + "formButtonWidget": ".t--widget-formbuttonwidget", "textWidget": ".t--draggable-textwidget", "tableWidget": ".t--draggable-tablewidget", "tableOnRowSelected": ".t--property-control-onrowselected", @@ -37,4 +38,4 @@ "textInputval": ".t--draggable-textwidget span.t--widget-name", "textAlign": ".t--property-control-textalign", "ColumnAction": ".t--property-control-rowbutton button" -} \ No newline at end of file +} diff --git a/app/client/public/index.html b/app/client/public/index.html index 5702864a809..7a51cdaa5ec 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -33,6 +33,18 @@ gtag('config', "%REACT_APP_GOOGLE_ANALYTICS_ID%"); + diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index 292ed39011c..0ea4e647932 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -25,6 +25,11 @@ export const updateDatasource = ( }; }; +export const saveDatasourceName = (payload: { id: string; name: string }) => ({ + type: ReduxActionTypes.SAVE_DATASOURCE_NAME, + payload: payload, +}); + export const changeDatasource = (payload: Datasource) => { return { type: ReduxActionTypes.CHANGE_DATASOURCE, diff --git a/app/client/src/assets/icons/ads/bag.svg b/app/client/src/assets/icons/ads/bag.svg new file mode 100644 index 00000000000..04096a467db --- /dev/null +++ b/app/client/src/assets/icons/ads/bag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/book.svg b/app/client/src/assets/icons/ads/book.svg new file mode 100644 index 00000000000..f016135fcd5 --- /dev/null +++ b/app/client/src/assets/icons/ads/book.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/assets/icons/ads/calender.svg b/app/client/src/assets/icons/ads/calender.svg new file mode 100644 index 00000000000..8be12ddf068 --- /dev/null +++ b/app/client/src/assets/icons/ads/calender.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/camera.svg b/app/client/src/assets/icons/ads/camera.svg new file mode 100644 index 00000000000..d1b23590c07 --- /dev/null +++ b/app/client/src/assets/icons/ads/camera.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/assets/icons/ads/chat.svg b/app/client/src/assets/icons/ads/chat.svg new file mode 100644 index 00000000000..6fbbcfc7546 --- /dev/null +++ b/app/client/src/assets/icons/ads/chat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/close.svg b/app/client/src/assets/icons/ads/close.svg new file mode 100644 index 00000000000..afaca4c528d --- /dev/null +++ b/app/client/src/assets/icons/ads/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/edit.svg b/app/client/src/assets/icons/ads/edit.svg new file mode 100644 index 00000000000..93eaaa69469 --- /dev/null +++ b/app/client/src/assets/icons/ads/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/error.svg b/app/client/src/assets/icons/ads/error.svg new file mode 100644 index 00000000000..43f866b350e --- /dev/null +++ b/app/client/src/assets/icons/ads/error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/file.svg b/app/client/src/assets/icons/ads/file.svg new file mode 100644 index 00000000000..4e83e5a8aef --- /dev/null +++ b/app/client/src/assets/icons/ads/file.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/flight.svg b/app/client/src/assets/icons/ads/flight.svg new file mode 100644 index 00000000000..0a6daa90ab3 --- /dev/null +++ b/app/client/src/assets/icons/ads/flight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/frame.svg b/app/client/src/assets/icons/ads/frame.svg new file mode 100644 index 00000000000..3bd9d4a5029 --- /dev/null +++ b/app/client/src/assets/icons/ads/frame.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/assets/icons/ads/globe.svg b/app/client/src/assets/icons/ads/globe.svg new file mode 100644 index 00000000000..228290a5d65 --- /dev/null +++ b/app/client/src/assets/icons/ads/globe.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/heart.svg b/app/client/src/assets/icons/ads/heart.svg new file mode 100644 index 00000000000..372367768d6 --- /dev/null +++ b/app/client/src/assets/icons/ads/heart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/product.svg b/app/client/src/assets/icons/ads/product.svg new file mode 100644 index 00000000000..9237160df7c --- /dev/null +++ b/app/client/src/assets/icons/ads/product.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/search.svg b/app/client/src/assets/icons/ads/search.svg new file mode 100644 index 00000000000..e3e23f0e537 --- /dev/null +++ b/app/client/src/assets/icons/ads/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/shopper.svg b/app/client/src/assets/icons/ads/shopper.svg new file mode 100644 index 00000000000..e8deb2e9305 --- /dev/null +++ b/app/client/src/assets/icons/ads/shopper.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/success.svg b/app/client/src/assets/icons/ads/success.svg new file mode 100644 index 00000000000..c5c3d40e418 --- /dev/null +++ b/app/client/src/assets/icons/ads/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/components/ads/AppIcon.tsx b/app/client/src/components/ads/AppIcon.tsx new file mode 100644 index 00000000000..fe8fd23ce56 --- /dev/null +++ b/app/client/src/components/ads/AppIcon.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { ReactComponent as BagIcon } from "assets/icons/ads/bag.svg"; +import { ReactComponent as ProductIcon } from "assets/icons/ads/product.svg"; +import { ReactComponent as BookIcon } from "assets/icons/ads/book.svg"; +import { ReactComponent as CameraIcon } from "assets/icons/ads/camera.svg"; +import { ReactComponent as FileIcon } from "assets/icons/ads/file.svg"; +import { ReactComponent as ChatIcon } from "assets/icons/ads/chat.svg"; +import { ReactComponent as CalenderIcon } from "assets/icons/ads/calender.svg"; +import { ReactComponent as FrameIcon } from "assets/icons/ads/frame.svg"; +import { ReactComponent as GlobeIcon } from "assets/icons/ads/globe.svg"; +import { ReactComponent as ShopperIcon } from "assets/icons/ads/shopper.svg"; +import { ReactComponent as HeartIcon } from "assets/icons/ads/heart.svg"; +import { ReactComponent as FlightIcon } from "assets/icons/ads/flight.svg"; + +import styled from "styled-components"; +import { Size } from "./Button"; + +export enum AppIconName { + BAG = "bag", + PRODUCT = "product", + BOOK = "book", + CAMERA = "camera", + FILE = "file", + CHAT = "chat", + CALENDER = "calender", + FLIGHT = "flight", + FRAME = "frame", + GLOBE = "globe", + SHOPPER = "shopper", + HEART = "heart", +} + +export const sizeHandler = (size: Size) => { + let iconSize = 0; + switch (size) { + case Size.small: + iconSize = 20; + break; + case Size.medium: + iconSize = 30; + break; + case Size.large: + iconSize = 54; + break; + } + return iconSize; +}; + +const IconWrapper = styled.div` + cursor: pointer; + &:focus { + outline: none; + } + display: flex; + svg { + width: ${props => sizeHandler(props.size)}px; + height: ${props => sizeHandler(props.size)}px; + path { + fill: ${props => props.theme.colors.blackShades[9]}; + } + } +`; + +export type AppIconProps = { + size: Size; + name: AppIconName; +}; + +const AppIcon = (props: AppIconProps) => { + let returnIcon; + switch (props.name) { + case AppIconName.BAG: + returnIcon = ; + break; + case AppIconName.PRODUCT: + returnIcon = ; + break; + case AppIconName.BOOK: + returnIcon = ; + break; + case AppIconName.CAMERA: + returnIcon = ; + break; + case AppIconName.FILE: + returnIcon = ; + break; + case AppIconName.CHAT: + returnIcon = ; + break; + case AppIconName.CALENDER: + returnIcon = ; + break; + case AppIconName.FRAME: + returnIcon = ; + break; + case AppIconName.GLOBE: + returnIcon = ; + break; + case AppIconName.SHOPPER: + returnIcon = ; + break; + case AppIconName.HEART: + returnIcon = ; + break; + case AppIconName.FLIGHT: + returnIcon = ; + break; + default: + returnIcon = null; + break; + } + return returnIcon ? {returnIcon} : null; +}; + +export default AppIcon; diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 21a969a2ccb..38329daf44a 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -1,7 +1,7 @@ import React from "react"; import { CommonComponentProps, hexToRgba, ThemeProp } from "./common"; import styled from "styled-components"; -import { IconName, Icon } from "./Icon"; +import Icon, { IconName } from "./Icon"; import Spinner from "./Spinner"; import { mediumButton, @@ -233,7 +233,7 @@ const btnFontStyles = (props: ThemeProp & ButtonProps): BtnFontType => { buttonFont = largeButton; padding = !props.text && props.icon - ? `${props.theme.spaces[5] - 1}px ${props.theme.spaces[5] - 1}px` + ? `${props.theme.spaces[3]}px` : `${props.theme.spaces[5] - 1}px ${props.theme.spaces[12] - 4}px`; break; } diff --git a/app/client/src/components/ads/ColorSelector.tsx b/app/client/src/components/ads/ColorSelector.tsx index 57d0063757c..4784e7d1dfc 100644 --- a/app/client/src/components/ads/ColorSelector.tsx +++ b/app/client/src/components/ads/ColorSelector.tsx @@ -1,9 +1,95 @@ +import React, { useState } from "react"; +import styled from "styled-components"; import { CommonComponentProps } from "./common"; +export const appColorPalette = [ + "#4F70FD", + "#54A9FB", + "#5ED3DA", + "#F56AF4", + "#F36380", + "#FE9F44", + "#E9C951", + "#A8D76C", + "#6C4CF1", +]; + type ColorSelectorProps = CommonComponentProps & { - onSelect: (hex: string) => void; + onSelect?: (hex: string) => void; + colorPalette?: string[]; + fill?: boolean; +}; + +const Palette = styled.div<{ fill?: boolean }>` + display: flex; + align-items: center; + flex-wrap: wrap; + width: ${props => (props.fill ? "100%" : "234px")}; +`; + +const ColorBox = styled.div<{ selected: string; color: string }>` + width: ${props => props.theme.spaces[8]}px; + height: ${props => props.theme.spaces[8]}px; + margin: 0 ${props => props.theme.spaces[2]}px + ${props => props.theme.spaces[2]}px 0; + background-color: ${props => props.color}; + cursor: pointer; + position: relative; + + &:hover { + box-shadow: 0px 0px 0px ${props => props.theme.spaces[1] - 1}px #353535; + } + + &:last-child { + margin-right: ${props => props.theme.spaces[1] - 1}px; + } + + ${props => + props.selected === props.color + ? `&::before { + content: ""; + position: absolute; + left: ${props.theme.spaces[3] - 1}px; + top: ${props.theme.spaces[1] - 1}px + width: ${props.theme.spaces[2] - 1}px + height: ${props.theme.spaces[4] - 1}px + border: 1.5px solid ${props.theme.colors.blackShades[9]}; + border-width: 0 1.5px 1.5px 0; + transform: rotate(45deg); + }` + : ` + &::before { + display: none; + } + `} +`; + +const ColorSelector = (props: ColorSelectorProps) => { + const [selected, setSelected] = useState(""); + console.log("colors", props.colorPalette); + + return ( + + {props.colorPalette && + props.colorPalette.map((hex: string, index: number) => { + return ( + { + setSelected(hex); + props.onSelect && props.onSelect(hex); + }} + /> + ); + })} + + ); +}; + +ColorSelector.defaultProps = { + fill: false, }; -export default function ColorSelector(props: ColorSelectorProps) { - return null; -} +export default ColorSelector; diff --git a/app/client/src/components/ads/EditableInput.tsx b/app/client/src/components/ads/EditableInput.tsx deleted file mode 100644 index 816ad2d1b37..00000000000 --- a/app/client/src/components/ads/EditableInput.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonComponentProps } from "./common"; - -export enum EditInteractionKind { - SINGLE, - DOUBLE, -} - -type EditableTextProps = CommonComponentProps & { - type: "text" | "password" | "email" | "phone" | "date"; - defaultValue: string; - onTextChanged: (value: string) => void; - placeholder: string; - cypressSelector?: string; - valueTransform?: (value: string) => string; - isEditingDefault?: boolean; - forceDefault?: boolean; - updating?: boolean; - isInvalid?: (value: string) => string | boolean; - editInteractionKind: EditInteractionKind; - hideEditIcon?: boolean; -}; - -// Check EditableText Component -export default function(props: EditableTextProps) { - return null; -} diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx new file mode 100644 index 00000000000..1a4aa7f9a61 --- /dev/null +++ b/app/client/src/components/ads/EditableText.tsx @@ -0,0 +1,292 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { EditableText as BlueprintEditableText } from "@blueprintjs/core"; +import styled from "styled-components"; +import { Size } from "./Button"; +import Text, { TextType } from "./Text"; +import Spinner from "./Spinner"; +import { hexToRgba } from "./common"; +import { theme } from "constants/DefaultTheme"; +import { noop } from "lodash"; +import Icon from "./Icon"; + +export enum EditInteractionKind { + SINGLE = "SINGLE", + DOUBLE = "DOUBLE", +} + +export type SavingStateHandler = ( + isSaving: boolean, + state?: SavingState, +) => void; + +export enum SavingState { + NOT_STARTED = "NOT_STARTED", + SUCCESS = "SUCCESS", + ERROR = "ERROR", +} + +type EditableTextProps = { + defaultValue: string; + onTextChanged: (value: string) => void; + placeholder: string; + className?: string; + valueTransform?: (value: string) => string; + isEditingDefault?: boolean; + forceDefault?: boolean; + updating?: boolean; + isInvalid?: (value: string) => string | boolean; + editInteractionKind: EditInteractionKind; + hideEditIcon?: boolean; + fill?: boolean; + onSubmit: ( + value: string, + callback: SavingStateHandler, + ) => { saving: SavingState }; +}; + +const EditableTextWrapper = styled.div<{ + fill?: boolean; +}>` + width: ${props => (!props.fill ? "234px" : "100%")}; + .error-message { + color: ${props => props.theme.colors.danger.main}; + } +`; + +const editModeBgcolor = ( + isInvalid: boolean, + isEditing: boolean, + savingState: { isSaving: boolean; name?: SavingState }, +): string => { + if ( + (isInvalid && isEditing) || + (!savingState.isSaving && savingState.name === SavingState.ERROR) + ) { + return hexToRgba(theme.colors.danger.main, 0.08); + } else if (!isInvalid && isEditing) { + return theme.colors.blackShades[2]; + } else { + return "transparent"; + } +}; + +const TextContainer = styled.div<{ + isInvalid: boolean; + isEditing: boolean; + bgColor: string; +}>` + 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; + } + + &&& .bp3-editable-text-content, + &&& .bp3-editable-text-input { + 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; + font-weight: ${props => props.theme.typography.p1.fontWeight}px; + } + + & .bp3-editable-text-content { + cursor: pointer; + color: ${props => props.theme.colors.blackShades[9]}; + overflow: hidden; + text-overflow: ellipsis; + ${props => (props.isEditing ? "display: none" : "display: block")}; + } + + & .bp3-editable-text-input { + border: none; + outline: none; + height: ${props => props.theme.spaces[13] + 3}px; + padding: ${props => props.theme.spaces[0]}px; + color: ${props => props.theme.colors.blackShades[9]}; + min-width: 100%; + border-radius: ${props => props.theme.spaces[0]}px; + } + + & .bp3-editable-text { + overflow: hidden; + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[5]}px; + width: calc(100% - 40px); + background-color: ${props => props.bgColor}; + } + + .icon-wrapper { + background-color: ${props => props.bgColor}; + } +`; + +const IconWrapper = styled.div` + width: ${props => props.theme.spaces[13] + 4}px; + padding-right: ${props => props.theme.spaces[5]}px; + height: ${props => props.theme.spaces[13] + 3}px; + display: flex; + align-items: center; + justify-content: flex-end; +`; + +export const AdsEditableText = (props: EditableTextProps) => { + const [isEditing, setIsEditing] = useState(!!props.isEditingDefault); + const [value, setValue] = useState(props.defaultValue); + const [lastValidValue, setLastValidValue] = useState(props.defaultValue); + const [isInvalid, setIsInvalid] = useState(false); + const [changeStarted, setChangeStarted] = useState(false); + const [savingState, setSavingState] = useState<{ + isSaving: boolean; + name?: SavingState; + }>({ isSaving: false, name: SavingState.NOT_STARTED }); + + useEffect(() => { + setValue(props.defaultValue); + setIsEditing(!!props.isEditingDefault); + }, [props.defaultValue, props.isEditingDefault]); + + useEffect(() => { + if (props.forceDefault === true) setValue(props.defaultValue); + }, [props.forceDefault, props.defaultValue]); + + const bgColor = useMemo( + () => editModeBgcolor(!!isInvalid, isEditing, savingState), + [isInvalid, isEditing, savingState], + ); + + /* should I write ? */ + const editMode = useCallback((e: React.MouseEvent) => { + setIsEditing(true); + const errorMessage = props.isInvalid && props.isInvalid(props.defaultValue); + setIsInvalid(errorMessage ? errorMessage : false); + e.preventDefault(); + e.stopPropagation(); + }, []); + + const onConfirm = (_value: string) => { + if ( + (!savingState.isSaving && savingState.name === SavingState.ERROR) || + isInvalid + ) { + setValue(lastValidValue); + setSavingState({ isSaving: false, name: SavingState.NOT_STARTED }); + } else if (changeStarted) { + props.onTextChanged(_value); + props.onSubmit(_value, SavingStateHandler); + } + setIsEditing(false); + setChangeStarted(false); + }; + + const onInputchange = useCallback((_value: string) => { + let finalVal: string = _value; + if (props.valueTransform) { + finalVal = props.valueTransform(_value); + } + setValue(finalVal); + + const errorMessage = props.isInvalid && props.isInvalid(finalVal); + const error = errorMessage ? errorMessage : false; + if (!error) { + setLastValidValue(finalVal); + } + setIsInvalid(error); + setChangeStarted(true); + }, []); + + const SavingStateHandler = (isSaving: boolean, state?: SavingState) => { + setIsEditing(false); + if (isSaving) { + setSavingState({ isSaving: true }); + } else { + switch (state) { + case SavingState.SUCCESS: + setSavingState({ isSaving: false, name: SavingState.SUCCESS }); + break; + default: + setValue(props.defaultValue); + setSavingState({ isSaving: false, name: SavingState.NOT_STARTED }); + break; + } + } + }; + + const iconName = + !isEditing && savingState.name === SavingState.NOT_STARTED + ? "edit" + : !isEditing && savingState.name === SavingState.SUCCESS + ? "success" + : (isEditing && savingState.name === SavingState.ERROR) || + (isEditing && !!isInvalid) + ? "error" + : undefined; + + const nonEditMode = () => { + if ( + !isEditing && + !savingState.isSaving && + savingState.name === SavingState.SUCCESS + ) { + setSavingState({ isSaving: false, name: SavingState.NOT_STARTED }); + } + }; + + return ( + + + + + + {savingState.isSaving ? ( + + ) : ( + + )} + + + {isEditing && !!isInvalid ? ( + + {isInvalid} + + ) : null} + + ); +}; + +AdsEditableText.defaultProps = { + fill: false, +}; + +export default AdsEditableText; diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index 8cb876cb1a4..3f547a05d9c 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -3,6 +3,11 @@ import { ReactComponent as DeleteIcon } from "assets/icons/ads/delete.svg"; import { ReactComponent as UserIcon } from "assets/icons/ads/user.svg"; import { ReactComponent as GeneralIcon } from "assets/icons/ads/general.svg"; import { ReactComponent as BillingIcon } from "assets/icons/ads/billing.svg"; +import { ReactComponent as EditIcon } from "assets/icons/ads/edit.svg"; +import { ReactComponent as ErrorIcon } from "assets/icons/ads/error.svg"; +import { ReactComponent as SuccessIcon } from "assets/icons/ads/success.svg"; +import { ReactComponent as SearchIcon } from "assets/icons/ads/search.svg"; +import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg"; import styled from "styled-components"; import { Size } from "./Button"; import { sizeHandler } from "./Spinner"; @@ -13,6 +18,11 @@ export type IconName = | "user" | "general" | "billing" + | "edit" + | "error" + | "success" + | "search" + | "close" | undefined; const IconWrapper = styled.div` @@ -21,8 +31,10 @@ const IconWrapper = styled.div` } display: flex; svg { - width: ${props => sizeHandler(props)}px; - height: ${props => sizeHandler(props)}px; + width: ${props => + props.size ? sizeHandler(props) : props.theme.spaces[9]}px; + height: ${props => + props.size ? sizeHandler(props) : props.theme.spaces[9]}px; path { fill: ${props => props.theme.colors.blackShades[4]}; } @@ -48,42 +60,53 @@ export type IconProps = { size?: Size; name?: IconName; invisible?: boolean; + className?: string; + click?: () => void; }; -export const Icon = (props: IconProps) => { +const Icon = (props: IconProps) => { let returnIcon; switch (props.name) { case "delete": - returnIcon = ( - - - - ); + returnIcon = ; break; case "user": - returnIcon = ( - - - - ); + returnIcon = ; break; case "general": - returnIcon = ( - - - - ); + returnIcon = ; break; case "billing": - returnIcon = ( - - - - ); + returnIcon = ; + break; + case "edit": + returnIcon = ; + break; + case "error": + returnIcon = ; + break; + case "success": + returnIcon = ; + break; + case "search": + returnIcon = ; + break; + case "close": + returnIcon = ; break; default: returnIcon = null; break; } - return returnIcon; + return returnIcon ? ( + props.click && props.click()} + > + {returnIcon} + + ) : null; }; + +export default Icon; diff --git a/app/client/src/components/ads/IconSelector.tsx b/app/client/src/components/ads/IconSelector.tsx index 49d7b8304c6..c0f354960f8 100644 --- a/app/client/src/components/ads/IconSelector.tsx +++ b/app/client/src/components/ads/IconSelector.tsx @@ -1,10 +1,95 @@ +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; +import AppIcon, { AppIconName } from "./AppIcon"; +import { Size } from "./Button"; import { CommonComponentProps } from "./common"; -import { IconName } from "./Icon"; + +export const appIconPalette = [ + AppIconName.BAG, + AppIconName.PRODUCT, + AppIconName.BOOK, + AppIconName.CAMERA, + AppIconName.FILE, + AppIconName.CHAT, + AppIconName.CALENDER, + AppIconName.FLIGHT, + AppIconName.FRAME, + AppIconName.GLOBE, + AppIconName.SHOPPER, + AppIconName.HEART, +]; type IconSelectorProps = CommonComponentProps & { - onSelect: (icon: IconName) => void; + onSelect?: (icon: AppIconName) => void; + selectedColor: string; + selectedIcon?: AppIconName; + iconPalette?: AppIconName[]; + fill?: boolean; +}; + +const IconPalette = styled.div<{ fill?: boolean }>` + display: flex; + align-items: center; + flex-wrap: wrap; + width: ${props => (props.fill ? "100%" : "234px")}; +`; + +const IconBox = styled.div<{ + iconName: AppIconName; + selected: AppIconName; + bgColor: string; +}>` + padding: ${props => props.theme.spaces[2]}px + ${props => props.theme.spaces[2] - 1}px; + margin: 0 ${props => props.theme.spaces[2]}px + ${props => props.theme.spaces[2]}px 0; + background-color: ${props => + props.selected === props.iconName + ? props.bgColor + : props.theme.colors.blackShades[2]}; + cursor: pointer; + position: relative; + + &:last-child { + margin-right: ${props => props.theme.spaces[0]}px; + } +`; + +const IconSelector = (props: IconSelectorProps) => { + const [selected, setSelected] = useState(appIconPalette[0]); + + useEffect(() => { + if (props.selectedIcon) { + setSelected(props.selectedIcon); + } + }, [props.selectedIcon]); + + return ( + + {props.iconPalette && + props.iconPalette.map((iconName: AppIconName, index: number) => { + return ( + { + setSelected(iconName); + props.onSelect && props.onSelect(iconName); + }} + > + + + ); + })} + + ); +}; + +IconSelector.defaultProps = { + fill: false, + iconPalette: appIconPalette, }; -export default function IconSelector(props: IconSelectorProps) { - return null; -} +export default IconSelector; diff --git a/app/client/src/components/ads/SearchInput.tsx b/app/client/src/components/ads/SearchInput.tsx new file mode 100644 index 00000000000..dbe8fb4d365 --- /dev/null +++ b/app/client/src/components/ads/SearchInput.tsx @@ -0,0 +1,138 @@ +import React, { forwardRef, Ref, useCallback, useMemo, useState } from "react"; +import { CommonComponentProps } from "./common"; +import styled from "styled-components"; +import { Size } from "./Button"; +import Icon from "./Icon"; + +export enum SearchVariant { + BACKGROUND = "BACKGROUND", + SEAMLESS = "SEAMLESS", +} + +export type TextInputProps = CommonComponentProps & { + placeholder?: string; + fill?: boolean; + defaultValue?: string; + variant?: SearchVariant; + onChange?: (value: string) => void; +}; + +const StyledInput = styled.input< + TextInputProps & { value?: string; isFocused: boolean } +>` + width: ${props => + props.value && props.variant === SearchVariant.BACKGROUND && props.isFocused + ? "calc(100% - 50px)" + : "100%"}; + border-radius: 0; + outline: 0; + box-shadow: none; + border: none; + padding: 0; + background-color: transparent; + 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; + text-overflow: ellipsis; + + color: ${props => props.theme.colors.blackShades[9]}; + + &::placeholder { + color: ${props => props.theme.colors.blackShades[5]}; + } +`; + +const InputWrapper = styled.div<{ + value?: string; + isFocused: boolean; + variant?: SearchVariant; + fill?: boolean; +}>` + display: flex; + align-items: center; + padding: ${props => props.theme.spaces[3]}px + ${props => props.theme.spaces[4]}px ${props => props.theme.spaces[3]}px + ${props => props.theme.spaces[6]}px; + width: ${props => (props.fill ? "100%" : "210px")}; + background-color: ${props => + props.variant === SearchVariant.SEAMLESS ? "transparent" : "#262626"}; + ${props => + props.variant === SearchVariant.BACKGROUND + ? props.isFocused || props.value + ? `box-shadow: 0px 1px 0px ${props.theme.colors.info.main}` + : `box-shadow: 0px 1px 0px ${props.theme.colors.blackShades[4]}` + : null} + + .search-icon { + margin-right: ${props => props.theme.spaces[5]}px; + + svg { + path, + circle { + stroke: ${props => + props.isFocused || props.value + ? props.theme.colors.blackShades[7] + : props.theme.colors.blackShades[5]}; + } + } + } + + .close-icon { + margin-right: ${props => props.theme.spaces[4]}px; + margin-left: ${props => props.theme.spaces[4]}px; + } +`; + +const SearchInput = forwardRef( + (props: TextInputProps, ref: Ref) => { + const [searchValue, setSearchValue] = useState(props.defaultValue); + const [isFocused, setIsFocused] = useState(false); + + const memoizedChangeHandler = useCallback( + el => { + setSearchValue(el.target.value); + return props.onChange && props.onChange(el.target.value); + }, + [props], + ); + + return ( + + + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + onChange={memoizedChangeHandler} + /> + {searchValue && props.variant === SearchVariant.BACKGROUND ? ( + setSearchValue("")} + /> + ) : null} + + ); + }, +); + +SearchInput.defaultProps = { + fill: false, +}; + +SearchInput.displayName = "SearchInput"; + +export default SearchInput; diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index c5ade3092e6..68f5303705c 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import styled from "styled-components"; -import { Icon, IconName } from "./Icon"; +import Icon, { IconName } from "./Icon"; import { Size } from "./Button"; const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` diff --git a/app/client/src/components/stories/ColorSelector.stories.tsx b/app/client/src/components/stories/ColorSelector.stories.tsx new file mode 100644 index 00000000000..9ca90b491cf --- /dev/null +++ b/app/client/src/components/stories/ColorSelector.stories.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { action } from "@storybook/addon-actions"; +import ColorSelector, { appColorPalette } from "components/ads/ColorSelector"; +import { withKnobs, array, boolean } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; + +export default { + title: "ColorSelector", + component: ColorSelector, + decorators: [withKnobs, withDesign], +}; + +const defaultValue = appColorPalette; + +export const ColorPickerStory = () => ( +
+ +
+); diff --git a/app/client/src/components/stories/EditableText.stories.tsx b/app/client/src/components/stories/EditableText.stories.tsx new file mode 100644 index 00000000000..de5a12f3b3a --- /dev/null +++ b/app/client/src/components/stories/EditableText.stories.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { boolean, select, text, withKnobs } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import AdsEditableText, { + EditInteractionKind, + SavingStateHandler, + SavingState, +} from "../ads/EditableText"; +import { action } from "@storybook/addon-actions"; + +export default { + title: "EditableText", + component: AdsEditableText, + decorators: [withKnobs, withDesign], +}; + +const calls = (value: string, callback: any) => { + console.log("value", value); + + // setTimeout(() => { + // return callback(SavingState.ERROR); + // }, 2000); + + setTimeout(() => { + return callback(false, SavingState.SUCCESS); + }, 2000); + + return callback(true); +}; + +const errorFunction = (name: string) => { + if (name === "") { + return "Name cannot be empty"; + } else { + return false; + } +}; + +export const EditableTextStory = () => ( +
+ value.toUpperCase()} + placeholder={text("placeholder", "Edit input")} + hideEditIcon={boolean("hideEditIcon", false)} + isInvalid={name => errorFunction(name)} + isEditingDefault={boolean("isEditingDefault", false)} + fill={boolean("fill", false)} + onSubmit={(value: string, callback: SavingStateHandler) => + calls(value, callback) + } + > +
+); diff --git a/app/client/src/components/stories/Icon.stories.tsx b/app/client/src/components/stories/Icon.stories.tsx index abe85098777..7e267cad2d2 100644 --- a/app/client/src/components/stories/Icon.stories.tsx +++ b/app/client/src/components/stories/Icon.stories.tsx @@ -1,8 +1,9 @@ import React from "react"; -import { Icon } from "../ads/Icon"; import Button, { Size, Category, Variant } from "components/ads/Button"; import { withKnobs, select, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; +import Icon from "../ads/Icon"; +import AppIcon, { AppIconName } from "../ads/AppIcon"; export default { title: "Icon", @@ -11,29 +12,57 @@ export default { }; export const ButtonIcon = () => ( - +
+ +
); export const BordelessIcon = () => ( -
+
); + +export const BorderlessAppIcon = () => ( +
+ +
+); diff --git a/app/client/src/components/stories/IconSelector.stories.tsx b/app/client/src/components/stories/IconSelector.stories.tsx new file mode 100644 index 00000000000..9058aad8489 --- /dev/null +++ b/app/client/src/components/stories/IconSelector.stories.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import IconSelector from "../ads/IconSelector"; +import { action } from "@storybook/addon-actions"; +import { AppIconName } from "../ads/AppIcon"; + +export default { + title: "IconSelector", + component: IconSelector, + decorators: [withKnobs, withDesign], +}; + +export const IconPicker = () => ( +
+ +
+); diff --git a/app/client/src/components/stories/SearchInput.stories.tsx b/app/client/src/components/stories/SearchInput.stories.tsx new file mode 100644 index 00000000000..c245cc932ee --- /dev/null +++ b/app/client/src/components/stories/SearchInput.stories.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { withKnobs, boolean, text, select } from "@storybook/addon-knobs"; +import { action } from "@storybook/addon-actions"; +import SearchInput, { SearchVariant } from "../ads/SearchInput"; + +export default { + title: "Search Input", + component: SearchInput, + decorators: [withKnobs], +}; + +export const SearchInputStory = () => ( +
+ +
+); diff --git a/app/client/src/components/stories/Table.stories.tsx b/app/client/src/components/stories/Table.stories.tsx index 5144f8ae7fd..1e401568358 100644 --- a/app/client/src/components/stories/Table.stories.tsx +++ b/app/client/src/components/stories/Table.stories.tsx @@ -1,7 +1,7 @@ import React from "react"; import Table from "../ads/Table"; import Button, { Category, Variant, Size } from "../ads/Button"; -import { Icon } from "../ads/Icon"; +import Icon from "../ads/Icon"; export default { title: "Table", diff --git a/app/client/src/configs/index.ts b/app/client/src/configs/index.ts index eab451cd81f..56e6d1b02bb 100644 --- a/app/client/src/configs/index.ts +++ b/app/client/src/configs/index.ts @@ -30,6 +30,7 @@ type INJECTED_CONFIGS = { }; intercomAppID: string; mailEnabled: boolean; + smartLookKey: string; }; declare global { interface Window { @@ -94,6 +95,7 @@ const getConfigsFromEnvVars = (): INJECTED_CONFIGS => { mailEnabled: process.env.REACT_APP_MAIL_ENABLED ? process.env.REACT_APP_MAIL_ENABLED.length > 0 : false, + smartLookKey: process.env.REACT_APP_SMART_LOOK_KEY || "", }; }; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 1996a0ab1a7..3dea6e53540 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -62,6 +62,8 @@ export const ReduxActionTypes: { [key: string]: string } = { CREATE_QUERY_INIT: "CREATE_QUERY_INIT", FETCH_DATASOURCES_INIT: "FETCH_DATASOURCES_INIT", FETCH_DATASOURCES_SUCCESS: "FETCH_DATASOURCES_SUCCESS", + SAVE_DATASOURCE_NAME: "SAVE_DATASOURCE_NAME", + SAVE_DATASOURCE_NAME_SUCCESS: "SAVE_DATASOURCE_NAME_SUCCESS", CREATE_DATASOURCE_INIT: "CREATE_DATASOURCE_INIT", CREATE_DATASOURCE_SUCCESS: "CREATE_DATASOURCE_SUCCESS", CREATE_DATASOURCE_FROM_FORM_INIT: "CREATE_DATASOURCE_FROM_FORM_INIT", @@ -281,6 +283,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { FETCH_DATASOURCES_ERROR: "FETCH_DATASOURCES_ERROR", SEARCH_APIORPROVIDERS_ERROR: "SEARCH_APIORPROVIDERS_ERROR", UPDATE_DATASOURCE_ERROR: "UPDATE_DATASOURCE_ERROR", + SAVE_DATASOURCE_NAME_ERROR: "SAVE_DATASOURCE_NAME_ERROR", CREATE_DATASOURCE_ERROR: "CREATE_DATASOURCE_ERROR", DELETE_DATASOURCE_ERROR: "DELETE_DATASOURCE_ERROR", FETCH_PUBLISHED_PAGE_ERROR: "FETCH_PUBLISHED_PAGE_ERROR", diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx index 8d991f3a9ac..0d867162b10 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx @@ -16,7 +16,7 @@ import FormControlFactory from "utils/FormControlFactory"; import { HelpBaseURL, HelpMap } from "constants/HelpConstants"; import Button from "components/editorComponents/Button"; import { Datasource } from "api/DatasourcesApi"; -import { reduxForm, InjectedFormProps, Field } from "redux-form"; +import { reduxForm, InjectedFormProps } from "redux-form"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { APPSMITH_IP_ADDRESS } from "constants/DatasourceEditorConstants"; import { getAppsmithConfigs } from "configs"; @@ -314,11 +314,7 @@ class DatasourceDBEditor extends React.Component<
- + {cloudHosting && ( diff --git a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx index 55a0370e066..1d0adafdf0a 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx @@ -1,6 +1,5 @@ import styled from "styled-components"; -import React from "react"; -import { WrappedFieldInputProps } from "redux-form"; +import React, { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import EditableText, { EditInteractionKind, @@ -8,33 +7,47 @@ import EditableText, { import { AppState } from "reducers"; import { getDatasource } from "selectors/entitiesSelector"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { Datasource } from "api/DatasourcesApi"; import { getDataSources } from "selectors/editorSelectors"; +import { saveDatasourceName } from "actions/datasourceActions"; +import { Spinner } from "@blueprintjs/core"; const Wrapper = styled.div` margin-left: 10px; font-size: 18px; font-weight: 500; line-height: 24px; + display: flex; `; interface ComponentProps { - input: WrappedFieldInputProps; focusOnMount: boolean; } type FormTitleProps = ComponentProps; const FormTitle = (props: FormTitleProps) => { - const { input } = props; const params = useParams<{ datasourceId: string }>(); const currentDatasource: - | Partial + | Datasource | undefined = useSelector((state: AppState) => getDatasource(state, params.datasourceId), ); const datasources: Datasource[] = useSelector(getDataSources); + const [forceUpdate, setForceUpdate] = useState(false); + const dispatch = useDispatch(); + const saveStatus: { + isSaving: boolean; + error: boolean; + } = useSelector((state: AppState) => { + const id = currentDatasource ? currentDatasource.id : ""; + + return { + isSaving: state.ui.datasourceName.isSaving[id], + error: state.ui.datasourceName.errors[id], + }; + }); const hasNameConflict = React.useCallback( (name: string) => @@ -57,17 +70,41 @@ const FormTitle = (props: FormTitleProps) => { [hasNameConflict], ); + const handleDatasourceNameChange = useCallback( + (name: string) => { + if ( + !isInvalidDatasourceName(name) && + currentDatasource && + currentDatasource.name !== name + ) { + dispatch(saveDatasourceName({ id: currentDatasource?.id ?? "", name })); + } + }, + [dispatch, isInvalidDatasourceName, currentDatasource], + ); + + useEffect(() => { + if (saveStatus.isSaving === false && saveStatus.error === true) { + setForceUpdate(true); + } else if (saveStatus.isSaving === true) { + setForceUpdate(false); + } + }, [saveStatus.isSaving, saveStatus.error]); + return ( input.onChange(value)} + onTextChanged={handleDatasourceNameChange} placeholder="Datasource Name" editInteractionKind={EditInteractionKind.SINGLE} isEditingDefault={props.focusOnMount} + updating={saveStatus.isSaving} /> + {saveStatus.isSaving && } ); }; diff --git a/app/client/src/pages/Editor/QueryEditor/Table.tsx b/app/client/src/pages/Editor/QueryEditor/Table.tsx index 36d8e4e6278..9dc40037f59 100644 --- a/app/client/src/pages/Editor/QueryEditor/Table.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Table.tsx @@ -6,6 +6,8 @@ import { import { useTable, useFlexLayout } from "react-table"; import styled from "styled-components"; import { CompactModeTypes, TABLE_SIZES } from "widgets/TableWidget"; +import AutoToolTipComponent from "components/designSystems/appsmith/AutoToolTipComponent"; +import { getType, Types } from "utils/TypeHelpers"; interface TableProps { data: Record[]; @@ -30,6 +32,38 @@ const StyledTableWrapped = styled(TableWrapper)` } `; +const renderCell = (props: any) => { + const value = props.cell.value; + let displayValue; + switch (getType(value)) { + case Types.NUMBER: + case Types.BOOLEAN: + displayValue = value.toString(); + break; + case Types.ARRAY: + case Types.FUNCTION: + case Types.OBJECT: + displayValue = JSON.stringify(value); + break; + case Types.STRING: + displayValue = value; + break; + case Types.NULL: + case Types.UNDEFINED: + case Types.UNKNOWN: + displayValue = ""; + break; + default: + displayValue = ""; + } + + return ( + + {displayValue} + + ); +}; + const Table = (props: TableProps) => { const data = React.useMemo(() => props.data, [props.data]); const columns = React.useMemo(() => { @@ -38,6 +72,7 @@ const Table = (props: TableProps) => { return { Header: key, accessor: key, + Cell: renderCell, }; }); } diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index c53a48329dd..d9ce250ef5a 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -93,6 +93,20 @@ const datasourceReducer = createReducer(initialState, { }), }; }, + [ReduxActionTypes.SAVE_DATASOURCE_NAME_SUCCESS]: ( + state: DatasourceDataState, + action: ReduxAction, + ): DatasourceDataState => { + return { + ...state, + loading: false, + list: state.list.map(datasource => { + if (datasource.id === action.payload.id) return action.payload; + + return datasource; + }), + }; + }, [ReduxActionErrorTypes.CREATE_DATASOURCE_ERROR]: ( state: DatasourceDataState, ) => { diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index 1ca2fb02120..e6e9a946345 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -31,6 +31,7 @@ import { ApiNameReduxState } from "./uiReducers/apiNameReducer"; import { ExplorerReduxState } from "./uiReducers/explorerReducer"; import { PageDSLsReduxState } from "./uiReducers/pageDSLReducer"; import { AppDataState } from "@appsmith/reducers/entityReducers/appReducer"; +import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer"; const appReducer = combineReducers({ entities: entityReducer, @@ -62,6 +63,7 @@ export interface AppState { apiName: ApiNameReduxState; explorer: ExplorerReduxState; pageDSLs: PageDSLsReduxState; + datasourceName: DatasourceNameReduxState; }; entities: { canvasWidgets: CanvasWidgetsReduxState; diff --git a/app/client/src/reducers/uiReducers/datasourceNameReducer.ts b/app/client/src/reducers/uiReducers/datasourceNameReducer.ts new file mode 100644 index 00000000000..4581dc6056a --- /dev/null +++ b/app/client/src/reducers/uiReducers/datasourceNameReducer.ts @@ -0,0 +1,70 @@ +import { createReducer } from "utils/AppsmithUtils"; +import { + ReduxAction, + ReduxActionTypes, + ReduxActionErrorTypes, +} from "constants/ReduxActionConstants"; + +const initialState: DatasourceNameReduxState = { + isSaving: {}, + errors: {}, +}; + +const datasourceNameReducer = createReducer(initialState, { + [ReduxActionErrorTypes.SAVE_DATASOURCE_NAME_ERROR]: ( + state: DatasourceNameReduxState, + action: ReduxAction<{ id: string }>, + ) => { + return { + ...state, + isSaving: { + ...state.isSaving, + [action.payload.id]: false, + }, + errors: { + ...state.errors, + [action.payload.id]: true, + }, + }; + }, + + [ReduxActionTypes.SAVE_DATASOURCE_NAME]: ( + state: DatasourceNameReduxState, + action: ReduxAction<{ id: string }>, + ) => { + return { + ...state, + isSaving: { + ...state.isSaving, + [action.payload.id]: true, + }, + errors: { + ...state.errors, + [action.payload.id]: false, + }, + }; + }, + [ReduxActionTypes.SAVE_DATASOURCE_NAME_SUCCESS]: ( + state: DatasourceNameReduxState, + action: ReduxAction<{ id: string }>, + ) => { + return { + ...state, + isSaving: { + ...state.isSaving, + [action.payload.id]: false, + }, + errors: { + ...state.errors, + [action.payload.id]: false, + }, + }; + }, +}); + +export interface DatasourceNameReduxState { + isSaving: Record; + errors: Record; +} + +export default datasourceNameReducer; diff --git a/app/client/src/reducers/uiReducers/index.tsx b/app/client/src/reducers/uiReducers/index.tsx index c2ed4b9742d..f3f4926a847 100644 --- a/app/client/src/reducers/uiReducers/index.tsx +++ b/app/client/src/reducers/uiReducers/index.tsx @@ -19,6 +19,7 @@ import helpReducer from "./helpReducer"; import apiNameReducer from "./apiNameReducer"; import explorerReducer from "./explorerReducer"; import pageDSLsReducer from "./pageDSLReducer"; +import datasourceNameReducer from "./datasourceNameReducer"; const uiReducer = combineReducers({ widgetSidebar: widgetSidebarReducer, @@ -37,6 +38,7 @@ const uiReducer = combineReducers({ imports: importReducer, queryPane: queryPaneReducer, datasourcePane: datasourcePaneReducer, + datasourceName: datasourceNameReducer, help: helpReducer, apiName: apiNameReducer, explorer: explorerReducer, diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index 4c02af58034..e6ea0cf5f57 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -162,14 +162,16 @@ function* updateDatasourceSaga( }>, ) { try { + const datasourcePayload = _.omit(actionPayload.payload.datasource, "name"); + const response: GenericApiResponse = yield DatasourcesApi.updateDatasource( - actionPayload.payload.datasource, - actionPayload.payload.datasource.id, + datasourcePayload, + datasourcePayload.id, ); const isValidResponse = yield validateResponse(response); if (isValidResponse) { AppToaster.show({ - message: `${actionPayload.payload.datasource.name} Datasource updated`, + message: `${response.data.name} Datasource updated`, type: ToastType.SUCCESS, }); yield put({ @@ -183,9 +185,7 @@ function* updateDatasourceSaga( }, }); if (actionPayload.payload.reinitializeForm) { - yield put( - initialize(DATASOURCE_DB_FORM, actionPayload.payload.datasource), - ); + yield put(initialize(DATASOURCE_DB_FORM, datasourcePayload)); } } } catch (error) { @@ -196,6 +196,38 @@ function* updateDatasourceSaga( } } +function* saveDatasourceNameSaga( + actionPayload: ReduxAction<{ id: string; name: string }>, +) { + try { + const response: GenericApiResponse = yield DatasourcesApi.updateDatasource( + { + name: actionPayload.payload.name, + }, + actionPayload.payload.id, + ); + + const isValidResponse = yield validateResponse(response); + if (isValidResponse) { + yield put({ + type: ReduxActionTypes.SAVE_DATASOURCE_NAME_SUCCESS, + payload: { ...response.data }, + }); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.SAVE_DATASOURCE_NAME_ERROR, + payload: { id: actionPayload.payload.id }, + }); + } +} + +function* handleDatasourceNameChangeFailureSaga( + action: ReduxAction<{ oldName: string }>, +) { + yield put(change(DATASOURCE_DB_FORM, "name", action.payload.oldName)); +} + function* testDatasourceSaga(actionPayload: ReduxAction) { const organizationId = yield select(getCurrentOrgId); const { initialValues, values } = yield select( @@ -405,8 +437,9 @@ function* switchDatasourceSaga(action: ReduxAction<{ datasourceId: string }>) { function* formValueChangeSaga( actionPayload: ReduxActionWithMeta, ) { - const { form } = actionPayload.meta; + const { form, field } = actionPayload.meta; if (form !== DATASOURCE_DB_FORM) return; + if (field === "name") return; yield all([call(updateDraftsSaga)]); } @@ -466,6 +499,11 @@ export function* watchDatasourcesSagas() { createDatasourceFromFormSaga, ), takeEvery(ReduxActionTypes.UPDATE_DATASOURCE_INIT, updateDatasourceSaga), + takeEvery(ReduxActionTypes.SAVE_DATASOURCE_NAME, saveDatasourceNameSaga), + takeEvery( + ReduxActionErrorTypes.SAVE_DATASOURCE_NAME_ERROR, + handleDatasourceNameChangeFailureSaga, + ), takeEvery(ReduxActionTypes.TEST_DATASOURCE_INIT, testDatasourceSaga), takeEvery(ReduxActionTypes.DELETE_DATASOURCE_INIT, deleteDatasourceSaga), takeEvery(ReduxActionTypes.CHANGE_DATASOURCE, changeDatasourceSaga), diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 4c194915958..6e09af77de5 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -91,20 +91,18 @@ export function* fetchPageListSaga( isDefault: page.isDefault, })); yield put({ - type: ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS, + type: ReduxActionTypes.SET_CURRENT_ORG_ID, payload: { - pages, - applicationId, + orgId, }, }); yield put({ - type: ReduxActionTypes.SET_CURRENT_ORG_ID, + type: ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS, payload: { - orgId, + pages, + applicationId, }, }); - - return; } } catch (error) { yield put({ diff --git a/app/client/src/sagas/PluginSagas.ts b/app/client/src/sagas/PluginSagas.ts index 34c78a4c9fa..45bbf5d80cb 100644 --- a/app/client/src/sagas/PluginSagas.ts +++ b/app/client/src/sagas/PluginSagas.ts @@ -11,6 +11,9 @@ import { getCurrentOrgId } from "selectors/organizationSelectors"; function* fetchPluginsSaga() { try { const orgId = yield select(getCurrentOrgId); + if (!orgId) { + throw Error("Org id does not exist"); + } const pluginsResponse = yield call(PluginsApi.fetchPlugins, orgId); const isValid = yield validateResponse(pluginsResponse); if (isValid) { diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index f10c8d7937d..705ba20153a 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -101,7 +101,7 @@ export const getDatasourceRefs = (state: AppState): any => export const getDatasource = ( state: AppState, datasourceId: string, -): Partial | undefined => +): Datasource | undefined => state.entities.datasources.list.find( datasource => datasource.id === datasourceId, ); diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index ec55c2e7ed5..83b9bb58f69 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -200,9 +200,14 @@ class AnalyticsUtil { }); } if (windowDoc.hj) { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - window.hj("identify", userData.email, { email: userData.email }); + windowDoc.hj("identify", userData.email, { email: userData.email }); + } + if (windowDoc.smartLook) { + windowDoc.smartlook("identify", userId, { + email: userData.email, + name: userData.name, + userId: userId, + }); } } diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 78ccdfc9062..0795eeb5478 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -81,7 +81,6 @@ class TableWidget extends BaseWidget { nextPageKey: VALIDATION_TYPES.TEXT, prevPageKey: VALIDATION_TYPES.TEXT, label: VALIDATION_TYPES.TEXT, - selectedRowIndex: VALIDATION_TYPES.NUMBER, searchText: VALIDATION_TYPES.TEXT, defaultSearchText: VALIDATION_TYPES.TEXT, }; @@ -337,7 +336,8 @@ class TableWidget extends BaseWidget { JSON.stringify(prevProps.filters) || this.props.searchText !== prevProps.searchText || JSON.stringify(this.props.sortedColumn) !== - JSON.stringify(prevProps.sortedColumn) + JSON.stringify(prevProps.sortedColumn) || + !this.props.filteredTableData ) { const filteredTableData = this.filterTableData(); super.updateWidgetMetaProperty("filteredTableData", filteredTableData);