diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js index b2342ec463e..673cdfdad4d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js @@ -118,6 +118,8 @@ describe("Table Widget Functionality", function() { .contains("is exactly") .click(); cy.get(publish.inputValue).type(tabValue); + cy.wait(500); + cy.get(publish.applyFiltersBtn).click(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(publish.canvas) @@ -127,8 +129,8 @@ describe("Table Widget Functionality", function() { const tabValue = tabData; expect(tabValue).to.be.equal("Lindsay Ferguson"); }); - cy.get(publish.filterBtn).click(); cy.get(publish.removeFilter).click(); + cy.get("body").type("{esc}"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.readTabledataPublish("0", "3").then((tabData) => { @@ -157,6 +159,8 @@ describe("Table Widget Functionality", function() { .contains("contains") .click(); cy.get(publish.inputValue).type("Lindsay"); + cy.wait(500); + cy.get(publish.applyFiltersBtn).click(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(publish.canvas) @@ -166,8 +170,8 @@ describe("Table Widget Functionality", function() { const tabValue = tabData; expect(tabValue).to.be.equal("Lindsay Ferguson"); }); - cy.get(publish.filterBtn).click(); cy.get(publish.removeFilter).click(); + cy.get("body").type("{esc}"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.readTabledataPublish("0", "3").then((tabData) => { @@ -196,6 +200,8 @@ describe("Table Widget Functionality", function() { .contains("starts with") .click(); cy.get(publish.inputValue).type("Lindsay"); + cy.wait(500); + cy.get(publish.applyFiltersBtn).click(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(publish.canvas) @@ -205,8 +211,8 @@ describe("Table Widget Functionality", function() { const tabValue = tabData; expect(tabValue).to.be.equal("Lindsay Ferguson"); }); - cy.get(publish.filterBtn).click(); cy.get(publish.removeFilter).click(); + cy.get("body").type("{esc}"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.readTabledataPublish("0", "3").then((tabData) => { @@ -235,6 +241,8 @@ describe("Table Widget Functionality", function() { .contains("ends with") .click(); cy.get(publish.inputValue).type("Ferguson"); + cy.wait(500); + cy.get(publish.applyFiltersBtn).click(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(publish.canvas) @@ -244,8 +252,8 @@ describe("Table Widget Functionality", function() { const tabValue = tabData; expect(tabValue).to.be.equal("Lindsay Ferguson"); }); - cy.get(publish.filterBtn).click(); cy.get(publish.removeFilter).click(); + cy.get("body").type("{esc}"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.readTabledataPublish("0", "3").then((tabData) => { diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json index 8e91ef0bfe9..ef6e1c45ea6 100644 --- a/app/client/cypress/locators/publishWidgetspage.json +++ b/app/client/cypress/locators/publishWidgetspage.json @@ -25,6 +25,7 @@ "searchInput": ".t--search-input", "downloadBtn": ".t--table-download-btn", "filterBtn": ".t--table-filter-toggle-btn", + "applyFiltersBtn": ".t--apply-filter-btn", "attributeDropdown": ".t--table-filter-columns-dropdown", "attributeValue": ".t--dropdown-option", "conditionDropdown": ".t--table-filter-conditions-dropdown", diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index e45f15e366a..d67748f1c23 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -101,6 +101,15 @@ export const closePropertyPane = () => { }; }; +export const closeTableFilterPane = () => { + return { + type: ReduxActionTypes.HIDE_TABLE_FILTER_PANE, + payload: { + force: false, + }, + }; +}; + export const copyWidget = (isShortcut: boolean) => { return { type: ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, diff --git a/app/client/src/assets/icons/control/add-circle.svg b/app/client/src/assets/icons/control/add-circle.svg new file mode 100644 index 00000000000..e834e2039b2 --- /dev/null +++ b/app/client/src/assets/icons/control/add-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/assets/icons/control/close-circle.svg b/app/client/src/assets/icons/control/close-circle.svg new file mode 100644 index 00000000000..987f2d41a65 --- /dev/null +++ b/app/client/src/assets/icons/control/close-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx index bbf90c2653a..ab5a34ed791 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect, useCallback } from "react"; import styled from "styled-components"; import { Icon, InputGroup } from "@blueprintjs/core"; +import { debounce } from "lodash"; +import { AnyStyledComponent } from "styled-components"; + import CustomizedDropdown from "pages/common/CustomizedDropdown"; import { Directions } from "utils/helpers"; import { Colors } from "constants/Colors"; import { ControlIcons } from "icons/ControlIcons"; -import { AnyStyledComponent } from "styled-components"; import { Skin } from "constants/DefaultTheme"; import AutoToolTipComponent from "components/designSystems/appsmith/TableComponent/AutoToolTipComponent"; import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent2"; @@ -14,16 +16,13 @@ import { Condition, ColumnTypes, Operator, -} from "components/designSystems/appsmith/TableComponent/Constants"; -import { - DropdownOption, ReactTableFilter, -} from "components/designSystems/appsmith/TableComponent/TableFilters"; +} from "components/designSystems/appsmith/TableComponent/Constants"; +import { DropdownOption } from "components/designSystems/appsmith/TableComponent/TableFilters"; import { RenderOptionWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers"; -import { debounce } from "lodash"; const StyledRemoveIcon = styled( - ControlIcons.REMOVE_CONTROL as AnyStyledComponent, + ControlIcons.CLOSE_CIRCLE_CONTROL as AnyStyledComponent, )` padding: 0; position: relative; @@ -34,8 +33,8 @@ const StyledRemoveIcon = styled( `; const LabelWrapper = styled.div` - width: 105px; - text-align: center; + width: 95px; + margin-left: 10px; color: ${Colors.BLUE_BAYOUX}; font-size: 14px; font-weight: 500; @@ -57,7 +56,7 @@ const StyledInputGroup = styled(InputGroup)` background: ${Colors.WHITE}; border: 1px solid #d3dee3; box-sizing: border-box; - border-radius: 4px; + border-radius: 2px; color: ${Colors.OXFORD_BLUE}; height: 32px; width: 150px; @@ -81,7 +80,7 @@ const DropdownTrigger = styled.div` background: ${Colors.WHITE}; border: 1px solid #d3dee3; box-sizing: border-box; - border-radius: 4px; + border-radius: 2px; font-size: 14px; padding: 5px 12px 7px; color: ${Colors.OXFORD_BLUE}; @@ -219,7 +218,7 @@ function RenderOptions(props: { {selectedValue} - + ), }, @@ -512,7 +511,7 @@ function Fields(props: CascadeFieldProps & { state: CascadeFieldState }) { className={`t--table-filter-remove-btn ${ hasAnyFilters ? "" : "hide-icon" }`} - color={Colors.RIVER_BED} + color={Colors.GRAY} height={16} onClick={handleRemoveFilter} width={16} diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.test.ts b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.test.ts index 356919af056..6798d2751cf 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.test.ts +++ b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.test.ts @@ -49,6 +49,7 @@ describe("ConditionFunctions Constants", () => { it("works as expected for endsWith", () => { const conditionFunction = ConditionFunctions["endsWith"]; expect(conditionFunction("subtest", "test")).toStrictEqual(true); + expect(conditionFunction("subtest", "t")).toStrictEqual(true); }); it("works as expected for is", () => { const conditionFunction = ConditionFunctions["is"]; diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts index 88c74bac352..7cde2342082 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts +++ b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts @@ -206,7 +206,7 @@ export const ConditionFunctions: { }, endsWith: (a: any, b: any) => { if (isString(a) && isString(b)) { - return a.length === a.indexOf(b) + b.length; + return a.length === a.lastIndexOf(b) + b.length; } return false; }, diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx index 45e16fcd1ef..0380053c00f 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx @@ -13,7 +13,6 @@ import { TableHeaderWrapper, TableHeaderInnerWrapper, } from "./TableStyledWrappers"; -import { ReactTableFilter } from "components/designSystems/appsmith/TableComponent/TableFilters"; import { TableHeaderCell, renderEmptyRows, @@ -24,6 +23,7 @@ import TableHeader from "./TableHeader"; import { Classes } from "@blueprintjs/core"; import { ReactTableColumnProps, + ReactTableFilter, TABLE_SIZES, CompactMode, CompactModeTypes, @@ -242,7 +242,6 @@ export function Table(props: TableProps) { columns={tableHeadercolumns} compactMode={props.compactMode} currentPageIndex={currentPageIndex} - editMode={props.editMode} filters={props.filters} isVisibleCompactMode={props.isVisibleCompactMode} isVisibleDownload={props.isVisibleDownload} @@ -262,6 +261,7 @@ export function Table(props: TableProps) { tableSizes={tableSizes} updateCompactMode={props.updateCompactMode} updatePageNo={props.updatePageNo} + widgetId={props.widgetId} widgetName={props.widgetName} /> diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPane.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPane.tsx new file mode 100644 index 00000000000..7402afa99d8 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPane.tsx @@ -0,0 +1,140 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { get } from "lodash"; +import * as log from "loglevel"; +import { AppState } from "reducers"; +import styled from "styled-components"; + +import { Colors } from "constants/Colors"; +import { + ReactTableColumnProps, + ReactTableFilter, +} from "components/designSystems/appsmith/TableComponent/Constants"; +import TableFilterPaneContent from "components/designSystems/appsmith/TableComponent/TableFilterPaneContent"; +import { ThemeMode, getCurrentThemeMode } from "selectors/themeSelectors"; +import { Layers } from "constants/Layers"; +import Popper from "pages/Editor/Popper"; +import { generateClassName } from "utils/generators"; +import { getTableFilterState } from "selectors/tableFilterSelectors"; +import { getWidgetMetaProps } from "sagas/selectors"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { selectWidgetAction } from "actions/widgetSelectionActions"; +import { ReactComponent as DragHandleIcon } from "assets/icons/ads/app-icons/draghandler.svg"; + +const DragBlock = styled.div` + height: 41px; + width: 83px; + background: ${Colors.ATHENS_GRAY_DARKER}; + box-sizing: border-box; + font-size: 12px; + color: ${Colors.SLATE_GRAY}; + letter-spacing: 0.04em; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + span { + padding-left: 8px; + color: ${Colors.GRAY}; + } +`; + +export interface TableFilterPaneProps { + widgetId: string; + columns: ReactTableColumnProps[]; + filters?: ReactTableFilter[]; + applyFilter: (filters: ReactTableFilter[]) => void; +} + +interface PositionPropsInt { + top: number; + left: number; +} + +type Props = ReturnType & + ReturnType & + TableFilterPaneProps; + +class TableFilterPane extends Component { + getPopperTheme() { + return ThemeMode.LIGHT; + } + + handlePositionUpdate = (position: any) => { + this.props.setPanePoistion( + this.props.tableFilterPane.widgetId as string, + position, + ); + }; + + render() { + if ( + this.props.tableFilterPane.isVisible && + this.props.tableFilterPane.widgetId === this.props.widgetId + ) { + log.debug("tablefilter pane rendered"); + const className = + "t--table-filter-toggle-btn " + + generateClassName(this.props.tableFilterPane.widgetId); + const el = document.getElementsByClassName(className)[0]; + return ( + + + Move + + } + renderDragBlockPositions={{ + left: "0px", + }} + targetNode={el} + themeMode={this.getPopperTheme()} + zIndex={Layers.tableFilterPane} + > + + + ); + } else { + return null; + } + } +} + +const mapStateToProps = (state: AppState, ownProps: TableFilterPaneProps) => { + return { + tableFilterPane: getTableFilterState(state), + themeMode: getCurrentThemeMode(state), + metaProps: getWidgetMetaProps(state, ownProps.widgetId), + }; +}; + +const mapDispatchToProps = (dispatch: any) => { + return { + setPanePoistion: (widgetId: string, position: any) => { + dispatch({ + type: ReduxActionTypes.TABLE_PANE_MOVED, + payload: { + widgetId, + position, + }, + }); + dispatch(selectWidgetAction(widgetId)); + }, + hideFilterPane: (widgetId: string) => { + dispatch({ + type: ReduxActionTypes.HIDE_TABLE_FILTER_PANE, + payload: { widgetId }, + }); + dispatch(selectWidgetAction(widgetId)); + }, + }; +}; +export default connect(mapStateToProps, mapDispatchToProps)(TableFilterPane); diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPaneContent.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPaneContent.tsx new file mode 100644 index 00000000000..1ae60209780 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilterPaneContent.tsx @@ -0,0 +1,227 @@ +import React, { useEffect } from "react"; +import styled, { AnyStyledComponent } from "styled-components"; +import { Colors } from "constants/Colors"; +import { + ReactTableColumnProps, + ReactTableFilter, + Operator, + OperatorTypes, +} from "components/designSystems/appsmith/TableComponent/Constants"; +import { DropdownOption } from "components/designSystems/appsmith/TableComponent/TableFilters"; +import Button from "components/editorComponents/Button"; +import CascadeFields from "components/designSystems/appsmith/TableComponent/CascadeFields"; +import { + createMessage, + TABLE_FILTER_COLUMN_TYPE_CALLOUT, +} from "constants/messages"; +import { ControlIcons } from "icons/ControlIcons"; + +const StyledPlusCircleIcon = styled( + ControlIcons.ADD_CIRCLE_CONTROL as AnyStyledComponent, +)` + padding: 0; + position: relative; + cursor: pointer; + svg { + circle { + fill: none !important; + stroke: ${Colors.GREEN}; + } + } +`; + +const TableFilterOuterWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + background: ${Colors.WHITE}; + box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2), 0px 2px 10px rgba(0, 0, 0, 0.1); +`; + +const TableFilerWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + padding: 2px 16px 14px; +`; + +const ButtonWrapper = styled.div` + display: flex; + width: 100%; + justify-content: space-between; + align-items; center; + background: ${Colors.WHITE}; + margin-top: 14px; + &&& button:hover { + background: transparent; + } +`; + +const ButtonActionsWrapper = styled.div` + display: flex; + align-items; center; + &&& button { + margin-left: 14px; + } +`; + +// margin-left is same as move block width in TableFilterPane.tsx +const ColumnTypeBindingMessage = styled.div` + height: 41px; + line-height: 41px; + background: ${Colors.ATHENS_GRAY_DARKER}; + box-sizing: border-box; + font-size: 12px; + color: ${Colors.SLATE_GRAY}; + letter-spacing: 0.04em; + font-weight: 500; + padding: 0 16px; + margin-left: 83px; + min-width: 350px; + text-align: right; +`; + +interface TableFilterProps { + columns: ReactTableColumnProps[]; + filters?: ReactTableFilter[]; + applyFilter: (filters: ReactTableFilter[]) => void; + hideFilterPane: (widgetId: string) => void; + widgetId: string; +} + +const DEFAULT_FILTER = { + column: "", + operator: OperatorTypes.OR, + value: "", + condition: "", +}; + +function TableFilterPaneContent(props: TableFilterProps) { + const [filters, updateFilters] = React.useState( + new Array(), + ); + + useEffect(() => { + const filters: ReactTableFilter[] = props.filters ? [...props.filters] : []; + if (filters.length === 0) { + filters.push({ ...DEFAULT_FILTER }); + } + updateFilters(filters); + }, [props.filters]); + + const addFilter = () => { + const updatedFilters = filters ? [...filters] : []; + let operator: Operator = OperatorTypes.OR; + if (updatedFilters.length >= 2) { + operator = updatedFilters[1].operator; + } + updatedFilters.push({ ...DEFAULT_FILTER, operator }); + updateFilters(updatedFilters); + }; + + const applyFilter = () => { + props.applyFilter(filters); + }; + + const hideFilter = () => { + props.hideFilterPane(props.widgetId); + }; + + const columns: DropdownOption[] = props.columns + .map((column: ReactTableColumnProps) => { + const type = column.metaProperties?.type || "text"; + return { + label: column.Header, + value: column.accessor, + type: type, + }; + }) + .filter((column: { label: string; value: string; type: string }) => { + return !["video", "button", "image"].includes(column.type as string); + }); + const hasAnyFilters = !!( + filters.length >= 1 && + filters[0].column && + filters[0].condition + ); + return ( + { + e.stopPropagation(); + }} + > + + {createMessage(TABLE_FILTER_COLUMN_TYPE_CALLOUT)} + + e.stopPropagation()}> + {filters.map((filter: ReactTableFilter, index: number) => { + return ( + { + // here updated filters store in state, not in redux + const updatedFilters = filters ? [...filters] : []; + updatedFilters[index] = filter; + updateFilters(updatedFilters); + }} + column={filter.column} + columns={columns} + condition={filter.condition} + hasAnyFilters={hasAnyFilters} + index={index} + key={index} + operator={ + filters.length >= 2 ? filters[1].operator : filter.operator + } + removeFilter={(index: number) => { + if (index === 1 && filters.length > 2) { + filters[2].operator = filters[1].operator; + } + const newFilters = [ + ...filters.slice(0, index), + ...filters.slice(index + 1), + ]; + if (newFilters.length === 0) { + newFilters.push({ ...DEFAULT_FILTER }); + } + // removed filter directly update redux + // with redux update, useEffect will update local state too + props.applyFilter(newFilters); + }} + value={filter.value} + /> + ); + })} + {hasAnyFilters ? ( + +