From de20a2a52fc8d133ea9eeaf0e1dc50f0228abfde Mon Sep 17 00:00:00 2001 From: Bhavin K <58818598+techbhavin@users.noreply.github.com> Date: Wed, 22 Sep 2021 14:16:51 +0530 Subject: [PATCH] feat: add server side pagination in list widget (#7128) --- .../ActionConstants.tsx | 1 + .../autocomplete/EntityDefinitions.test.ts | 2 + .../utils/autocomplete/EntityDefinitions.ts | 2 + .../ListWidget/component/ListPagination.tsx | 63 ++++++++-- .../src/widgets/ListWidget/widget/derived.js | 26 ++++ .../widgets/ListWidget/widget/derived.test.js | 26 ++++ .../src/widgets/ListWidget/widget/index.tsx | 114 ++++++++++++++++-- .../ListWidget/widget/propertyConfig.ts | 33 +++++ 8 files changed, 247 insertions(+), 20 deletions(-) diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index 0d3586fbdb..8a22ea924e 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -81,6 +81,7 @@ export enum EventType { ON_SNIPPET_EXECUTE = "ON_SNIPPET_EXECUTE", ON_SORT = "ON_SORT", ON_CHECKBOX_GROUP_SELECTION_CHANGE = "ON_CHECKBOX_GROUP_SELECTION_CHANGE", + ON_LIST_PAGE_CHANGE = "ON_LIST_PAGE_CHANGE", ON_RECORDING_START = "ON_RECORDING_START", ON_RECORDING_COMPLETE = "ON_RECORDING_COMPLETE", } diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.test.ts b/app/client/src/utils/autocomplete/EntityDefinitions.test.ts index dcdbda20d8..67b2ebf802 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.test.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.test.ts @@ -43,6 +43,8 @@ describe("EntityDefinitions", () => { gridGap: "number", items: "?", listData: "?", + pageNo: "?", + pageSize: "?", }; expect(listWidgetEntityDefinitions).toStrictEqual(output); diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index eb5295ba17..22bf0457b3 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -280,6 +280,8 @@ export const entityDefinitions: Record = { selectedItem: generateTypeDef(widget.selectedItem), items: generateTypeDef(widget.items), listData: generateTypeDef(widget.listData), + pageNo: generateTypeDef(widget.pageNo), + pageSize: generateTypeDef(widget.pageSize), }), RATE_WIDGET: { "!doc": "Rating widget is used to display ratings in your app.", diff --git a/app/client/src/widgets/ListWidget/component/ListPagination.tsx b/app/client/src/widgets/ListWidget/component/ListPagination.tsx index 053378cc62..01fc0db15d 100644 --- a/app/client/src/widgets/ListWidget/component/ListPagination.tsx +++ b/app/client/src/widgets/ListWidget/component/ListPagination.tsx @@ -1,6 +1,6 @@ import React from "react"; import Pagination from "rc-pagination"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; const locale = { // Options.jsx @@ -17,9 +17,7 @@ const locale = { next_3: "Next 3 Pages", }; -const StyledPagination = styled(Pagination)<{ - disabled?: boolean; -}>` +const paginatorCss = css` margin: 0 auto; padding: 0; font-size: 14px; @@ -30,9 +28,6 @@ const StyledPagination = styled(Pagination)<{ left: 0; right: 0; z-index: 3; - pointer-events: ${(props) => (props.disabled ? "none" : "all")}; - opacity: ${(props) => (props.disabled ? "0.4" : "1")}; - .rc-pagination::after { display: block; clear: both; @@ -303,6 +298,14 @@ const StyledPagination = styled(Pagination)<{ } `; +const StyledPagination = styled(Pagination)<{ + disabled?: boolean; +}>` + ${paginatorCss} + pointer-events: ${(props) => (props.disabled ? "none" : "all")}; + opacity: ${(props) => (props.disabled ? "0.4" : "1")}; +`; + interface ListPaginationProps { current: number; total: number; @@ -324,4 +327,50 @@ function ListPagination(props: ListPaginationProps) { ); } +const PaginationWrapper = styled.ul` + ${paginatorCss} + pointer-events: "all"; + opacity: "1"; +`; + +export function ServerSideListPagination(props: any) { + return ( + +
  • +
  • +
  • + {props.pageNo} +
  • +
  • +
  • +
    + ); +} + export default ListPagination; diff --git a/app/client/src/widgets/ListWidget/widget/derived.js b/app/client/src/widgets/ListWidget/widget/derived.js index 66816f5f7a..6e62fee123 100644 --- a/app/client/src/widgets/ListWidget/widget/derived.js +++ b/app/client/src/widgets/ListWidget/widget/derived.js @@ -111,6 +111,32 @@ export default { return updatedItems; }, + // + getPageSize: (props, moment, _) => { + const LIST_WIDGET_PAGINATION_HEIGHT = 36; + const DEFAULT_GRID_ROW_HEIGHT = 10; + const WIDGET_PADDING = DEFAULT_GRID_ROW_HEIGHT * 0.4; + + const templateBottomRow = props.templateBottomRow; + + const templateHeight = templateBottomRow * DEFAULT_GRID_ROW_HEIGHT; + + const componentHeight = + (props.bottomRow - props.topRow) * props.parentRowSpace; + + const totalSpaceAvailable = + componentHeight - (LIST_WIDGET_PAGINATION_HEIGHT + WIDGET_PADDING * 2); + const spaceTakenByOneContainer = templateHeight + (props.gridGap * 3) / 4; + + const perPage = totalSpaceAvailable / spaceTakenByOneContainer; + + if (_.isNaN(perPage)) { + return 0; + } else { + return _.floor(perPage); + } + }, + // // this is just a patch for #7520 getChildAutoComplete: (props, moment, _) => { const data = [...props.listData]; diff --git a/app/client/src/widgets/ListWidget/widget/derived.test.js b/app/client/src/widgets/ListWidget/widget/derived.test.js index 09b1b748b0..d25a001f6f 100644 --- a/app/client/src/widgets/ListWidget/widget/derived.test.js +++ b/app/client/src/widgets/ListWidget/widget/derived.test.js @@ -50,4 +50,30 @@ describe("Validates Derived Properties", () => { let result = getItems(input, moment, _); expect(result).toStrictEqual(expected); }); + + it("validates pageSize property", () => { + const { getPageSize } = derivedProperty; + const input1 = { + bottomRow: 86, + children: [{ children: [{ bottomRow: 16 }] }], + templateBottomRow: 16, + gridGap: 0, + parentRowSpace: 10, + topRow: 9, + }; + // resize ListWidget so bottomRow changes + const input2 = { + ...input1, + bottomRow: 56, + }; + + const expected1 = 4; + const expected2 = 2; + + let result1 = getPageSize(input1, moment, _); + let result2 = getPageSize(input2, moment, _); + + expect(result1).toStrictEqual(expected1); + expect(result2).toStrictEqual(expected2); + }); }); diff --git a/app/client/src/widgets/ListWidget/widget/index.tsx b/app/client/src/widgets/ListWidget/widget/index.tsx index f4bb3b2950..64e8c624a7 100644 --- a/app/client/src/widgets/ListWidget/widget/index.tsx +++ b/app/client/src/widgets/ListWidget/widget/index.tsx @@ -27,7 +27,9 @@ import ListComponent, { import propertyPaneConfig from "./propertyConfig"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { getDynamicBindings } from "utils/DynamicBindingUtils"; -import ListPagination from "../component/ListPagination"; +import ListPagination, { + ServerSideListPagination, +} from "../component/ListPagination"; import { GridDefaults, WIDGET_PADDING } from "constants/WidgetConstants"; import { ValidationTypes } from "constants/WidgetValidation"; import derivedProperties from "./parseDerivedProperties"; @@ -49,6 +51,7 @@ class ListWidget extends BaseWidget, WidgetState> { static getDerivedPropertiesMap() { return { + pageSize: `{{(()=>{${derivedProperties.getPageSize}})()}}`, selectedItem: `{{(()=>{${derivedProperties.getSelectedItem}})()}}`, items: `{{(() => {${derivedProperties.getItems}})()}}`, childAutoComplete: `{{(() => {${derivedProperties.getChildAutoComplete}})()}}`, @@ -56,6 +59,14 @@ class ListWidget extends BaseWidget, WidgetState> { } componentDidMount() { + if (this.props.serverSidePaginationEnabled && !this.props.pageNo) { + this.props.updateWidgetMetaProperty("pageNo", 1); + } + this.props.updateWidgetMetaProperty( + "templateBottomRow", + get(this.props.children, "0.children.0.bottomRow"), + ); + // generate childMetaPropertyMap this.generateChildrenDefaultPropertiesMap(this.props); this.generateChildrenMetaPropertiesMap(this.props); @@ -169,16 +180,78 @@ class ListWidget extends BaseWidget, WidgetState> { this.generateChildrenMetaPropertiesMap(this.props); this.generateChildrenEntityDefinitions(this.props); } + + if (this.props.serverSidePaginationEnabled) { + if (!this.props.pageNo) this.props.updateWidgetMetaProperty("pageNo", 1); + // run onPageSizeChange if user resize widgets + if ( + this.props.onPageSizeChange && + this.props.pageSize !== prevProps.pageSize + ) { + super.executeAction({ + triggerPropertyName: "onPageSizeChange", + dynamicString: this.props.onPageSizeChange, + event: { + type: EventType.ON_PAGE_SIZE_CHANGE, + }, + }); + } + } + + if (this.props.serverSidePaginationEnabled) { + if ( + this.props.serverSidePaginationEnabled === true && + prevProps.serverSidePaginationEnabled === false + ) { + super.executeAction({ + triggerPropertyName: "onPageSizeChange", + dynamicString: this.props.onPageSizeChange, + event: { + type: EventType.ON_PAGE_SIZE_CHANGE, + }, + }); + } + } + + if ( + get(this.props.children, "0.children.0.bottomRow") !== + get(prevProps.children, "0.children.0.bottomRow") + ) { + this.props.updateWidgetMetaProperty( + "templateBottomRow", + get(this.props.children, "0.children.0.bottomRow"), + { + triggerPropertyName: "onPageSizeChange", + dynamicString: this.props.onPageSizeChange, + event: { + type: EventType.ON_PAGE_SIZE_CHANGE, + }, + }, + ); + } } static getDefaultPropertiesMap(): Record { return {}; } - static getMetaPropertiesMap(): Record { - return {}; + static getMetaPropertiesMap(): Record { + return { + pageNo: 1, + templateBottomRow: 16, + }; } + onPageChange = (page: number) => { + this.props.updateWidgetMetaProperty("pageNo", page, { + triggerPropertyName: "onPageChange", + dynamicString: this.props.onPageChange, + event: { + type: EventType.ON_LIST_PAGE_CHANGE, + }, + }); + }; + /** * on click item action * @@ -545,6 +618,8 @@ class ListWidget extends BaseWidget, WidgetState> { * @param children */ paginateItems = (children: DSLWidget[]) => { + // return all children if serverside pagination + if (this.props.serverSidePaginationEnabled) return children; const { page } = this.state; const { perPage, shouldPaginate } = this.shouldPaginate(); @@ -596,7 +671,12 @@ class ListWidget extends BaseWidget, WidgetState> { */ shouldPaginate = () => { let { gridGap } = this.props; - const { children, listData } = this.props; + const { children, listData, serverSidePaginationEnabled } = this.props; + + if (serverSidePaginationEnabled) { + return { shouldPaginate: true, perPage: this.props.pageSize }; + } + if (!listData?.length) { return { shouldPaginate: false, perPage: 0 }; } @@ -636,6 +716,7 @@ class ListWidget extends BaseWidget, WidgetState> { getPageView() { const children = this.renderChildren(); const { componentHeight } = this.getComponentDimensions(); + const { pageNo, serverSidePaginationEnabled } = this.props; const { perPage, shouldPaginate } = this.shouldPaginate(); const templateBottomRow = get( this.props.children, @@ -697,15 +778,22 @@ class ListWidget extends BaseWidget, WidgetState> { > {children} - {shouldPaginate && ( - this.setState({ page })} - perPage={perPage} - total={(this.props.listData || []).length} - /> - )} + {shouldPaginate && + (serverSidePaginationEnabled ? ( + this.onPageChange(pageNo + 1)} + pageNo={this.props.pageNo} + prevPageClick={() => this.onPageChange(pageNo - 1)} + /> + ) : ( + this.setState({ page })} + perPage={perPage} + total={(this.props.listData || []).length} + /> + ))} ); } diff --git a/app/client/src/widgets/ListWidget/widget/propertyConfig.ts b/app/client/src/widgets/ListWidget/widget/propertyConfig.ts index 48e82da416..cc6316a864 100644 --- a/app/client/src/widgets/ListWidget/widget/propertyConfig.ts +++ b/app/client/src/widgets/ListWidget/widget/propertyConfig.ts @@ -72,6 +72,15 @@ const PropertyPaneConfig = [ inputType: "INTEGER", validation: { type: ValidationTypes.NUMBER, params: { min: 0 } }, }, + { + helpText: + "Bind the List.pageNo property in your API and call it onPageChange", + propertyName: "serverSidePaginationEnabled", + label: "Server Side Pagination", + controlType: "SWITCH", + isBindProperty: false, + isTriggerProperty: false, + }, { propertyName: "isVisible", label: "Visible", @@ -117,6 +126,30 @@ const PropertyPaneConfig = [ }, dependencies: ["listData"], }, + { + helpText: "Triggers an action when a list page is changed", + propertyName: "onPageChange", + label: "onPageChange", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + hidden: (props: ListWidgetProps) => + !props.serverSidePaginationEnabled, + dependencies: ["serverSidePaginationEnabled"], + }, + { + helpText: "Triggers an action when a list page size is changed", + propertyName: "onPageSizeChange", + label: "onPageSizeChange", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + hidden: (props: ListWidgetProps) => + !props.serverSidePaginationEnabled, + dependencies: ["serverSidePaginationEnabled"], + }, ], }, ];