diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss index 743dd90017..94a533499d 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss @@ -88,3 +88,8 @@ $gallery-screen-md: 768px; .widget-gallery-item-button { width: inherit; } + +.widget-gallery-load-more { + display: flex; + justify-content: center; +} diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts index 136ddf7cac..de7475fd0f 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts @@ -10,10 +10,6 @@ import { import { GalleryPreviewProps } from "../typings/GalleryProps"; export function getProperties(values: GalleryPreviewProps, defaultProperties: Properties): Properties { - if (values.pagination !== "buttons") { - hidePropertyIn(defaultProperties, values, "pagingPosition"); - } - if (values.showEmptyPlaceholder === "none") { hidePropertyIn(defaultProperties, values, "emptyPlaceholder"); } @@ -22,8 +18,21 @@ export function getProperties(values: GalleryPreviewProps, defaultProperties: Pr hidePropertiesIn(defaultProperties, values, ["onSelectionChange", "itemSelectionMode"]); } - // Hide scrolling settings for now. - hidePropertiesIn(defaultProperties, values, ["showPagingButtons", "showTotalCount"]); + /** Pagination */ + + if (values.pagination === "buttons") { + hidePropertyIn(defaultProperties, values, "showTotalCount"); + } else { + hidePropertyIn(defaultProperties, values, "showPagingButtons"); + + if (values.showTotalCount === false) { + hidePropertyIn(defaultProperties, values, "pagingPosition"); + } + } + + if (values.pagination !== "loadMore") { + hidePropertyIn(defaultProperties, values, "loadMoreButtonCaption"); + } return defaultProperties; } diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index dda99fb997..e6471a9e03 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -95,6 +95,8 @@ function Preview(props: GalleryPreviewProps): ReactElement { pageSize={props.pageSize ?? numberOfItems} paging={props.pagination === "buttons"} paginationPosition={props.pagingPosition} + paginationType={props.pagination} + showPagingButtons={props.showPagingButtons} showEmptyStatePreview={props.showEmptyPlaceholder === "custom"} phoneItems={props.phoneItems!} tabletItems={props.tabletItems!} diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index bd0b6cf89f..37e665ed95 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -1,22 +1,23 @@ -import { observer } from "mobx-react-lite"; import { useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; import { getColumnAndRowBasedOnIndex, useSelectionHelper } from "@mendix/widget-plugin-grid/selection"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { observer } from "mobx-react-lite"; import { ReactElement, ReactNode, createElement, useCallback } from "react"; import { GalleryContainerProps } from "../typings/GalleryProps"; import { Gallery as GalleryComponent } from "./components/Gallery"; +import { HeaderWidgetsHost } from "./components/HeaderWidgetsHost"; import { useItemEventsController } from "./features/item-interaction/ItemEventsController"; import { GridPositionsProps, useGridPositions } from "./features/useGridPositions"; import { useItemHelper } from "./helpers/ItemHelper"; -import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; +import { GalleryContext, GalleryRootScope, useGalleryRootScope } from "./helpers/root-context"; import { useGalleryStore } from "./helpers/useGalleryStore"; -import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; -import { GalleryRootScope, GalleryContext, useGalleryRootScope } from "./helpers/root-context"; -import { HeaderWidgetsHost } from "./components/HeaderWidgetsHost"; +import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; const Container = observer(function GalleryContainer(props: GalleryContainerProps): ReactElement { const { rootStore, itemSelectHelper } = useGalleryRootScope(); + const items = props.datasource.items ?? []; const config: GridPositionsProps = { desktopItems: props.desktopItems, @@ -76,10 +77,12 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp numberOfItems={props.datasource.totalCount} page={rootStore.paging.currentPage} pageSize={props.pageSize} - paging={props.pagination === "buttons"} + paging={rootStore.paging.showPagination} paginationPosition={props.pagingPosition} - phoneItems={props.phoneItems} + paginationType={props.pagination} setPage={rootStore.paging.setPage} + showPagingButtons={props.showPagingButtons} + phoneItems={props.phoneItems} style={props.style} tabletItems={props.tabletItems} tabIndex={props.tabIndex} @@ -87,6 +90,7 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp itemEventsController={itemEventsController} focusController={focusController} getPosition={getPositionCallback} + loadMoreButtonCaption={props.loadMoreButtonCaption?.value} /> ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index 62f0715b3c..2ec8e2118a 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -52,7 +52,7 @@ - + Page size @@ -63,15 +63,12 @@ Paging buttons Virtual scrolling + Load more - - Position of paging buttons + + Show total count - - Below grid - Above grid - Show paging buttons @@ -81,10 +78,24 @@ Auto - - Show total count + + Position of pagination + + Below grid + Above grid + Both + + + + Load more caption + + + Load More + + + Empty message diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index 1d07b4a271..be5ab3b220 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -5,14 +5,16 @@ import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/ import { ObjectItem } from "mendix"; import { createElement, ReactElement, ReactNode } from "react"; import { GalleryItemHelper } from "../typings/GalleryItem"; -import { ListBox } from "./ListBox"; -import { ListItem } from "./ListItem"; import { GalleryContent } from "./GalleryContent"; import { GalleryFooter } from "./GalleryFooter"; import { GalleryHeader } from "./GalleryHeader"; import { GalleryRoot } from "./GalleryRoot"; import { GalleryTopBar } from "./GalleryTopBar"; +import { ListBox } from "./ListBox"; +import { ListItem } from "./ListItem"; +import { LoadMore, LoadMoreButton as LoadMorePreview } from "src/components/LoadMore"; +import { PaginationEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; import { ItemEventsController } from "../typings/ItemEventsController"; export interface GalleryProps { @@ -29,7 +31,9 @@ export interface GalleryProps { paging: boolean; page: number; pageSize: number; - paginationPosition?: "below" | "above"; + paginationPosition?: "top" | "bottom" | "both"; + paginationType: PaginationEnum; + showPagingButtons: ShowPagingButtonsEnum; showEmptyStatePreview?: boolean; phoneItems: number; setPage?: (computePage: (prevPage: number) => number) => void; @@ -46,9 +50,11 @@ export interface GalleryProps { itemHelper: GalleryItemHelper; selectHelper: SelectActionHandler; getPosition: (index: number) => PositionInGrid; + loadMoreButtonCaption?: string; } export function Gallery(props: GalleryProps): ReactElement { + const { loadMoreButtonCaption = "Load more" } = props; const pagination = props.paging ? ( (props: GalleryProps): ReactElem page={props.page} pageSize={props.pageSize} previousPage={() => props.setPage && props.setPage(prev => prev - 1)} - pagination={props.paging ? "buttons" : "virtualScrolling"} + pagination={props.paginationType} + showPagingButtons={props.showPagingButtons} /> ) : null; - const showTopBar = props.paging && props.paginationPosition === "above"; - const showFooter = props.paging && props.paginationPosition === "below"; + const showTopPagination = + props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); + const showBottomPagination = + props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); return ( (props: GalleryProps): ReactElem selectable={false} data-focusindex={props.tabIndex || 0} > - {showTopBar && {pagination}} + {showTopPagination && pagination} {props.showHeader && {props.header}} {props.items.length > 0 && ( @@ -111,7 +120,15 @@ export function Gallery(props: GalleryProps): ReactElem {children} ))} - {showFooter && {pagination}} + + {showBottomPagination && pagination} + + {props.preview && props.paginationType === "loadMore" && ( + {loadMoreButtonCaption} + )} + {!props.preview && {loadMoreButtonCaption}} + + ); } diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx new file mode 100644 index 0000000000..150b22cec9 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx @@ -0,0 +1,28 @@ +import cn from "classnames"; +import { observer } from "mobx-react-lite"; +import { createElement } from "react"; +import { useGalleryRootScope } from "src/helpers/root-context"; + +export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): React.ReactNode { + return ( + + {props.children} + + ); +} + +export const LoadMore = observer(function LoadMore(props: { children: React.ReactNode }): React.ReactNode { + const { + rootStore: { paging } + } = useGalleryRootScope(); + + if (paging.pagination !== "loadMore") { + return null; + } + + if (!paging.hasMoreItems) { + return null; + } + + return paging.setPage(n => n + 1)}>{props.children}; +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx index fb235e8d16..b8f54f49ef 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx @@ -1,11 +1,11 @@ -import "@testing-library/jest-dom"; import { listAction, listExp, setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils"; -import { waitFor, render } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { render, waitFor } from "@testing-library/react"; +import { ObjectItem } from "mendix"; import { createElement } from "react"; -import { Gallery } from "../Gallery"; import { ItemHelperBuilder } from "../../utils/builders/ItemHelperBuilder"; -import { mockProps, mockItemHelperWithAction, setup } from "../../utils/test-utils"; -import { ObjectItem } from "mendix"; +import { mockItemHelperWithAction, mockProps, setup } from "../../utils/test-utils"; +import { Gallery } from "../Gallery"; describe("Gallery", () => { beforeAll(() => { @@ -96,7 +96,7 @@ describe("Gallery", () => { describe("with pagination", () => { it("renders correctly", () => { const { asFragment } = render( - + ); expect(asFragment()).toMatchSnapshot(); @@ -108,7 +108,7 @@ describe("Gallery", () => { new GalleryStore({ gate, ...props, showPagingButtons: "auto", showTotalCount: false }) - ); + const store = useSetup(() => new GalleryStore({ gate, ...props })); return store; } diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 4c6690d150..6e4e30e17a 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -1,17 +1,17 @@ -import { createElement } from "react"; import { ClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import { SelectActionHandler, getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection"; +import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController"; +import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout"; +import { getColumnAndRowBasedOnIndex, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; import { listAction, objectItems } from "@mendix/widget-plugin-test-utils"; import { render, RenderResult } from "@testing-library/react"; import userEvent, { UserEvent } from "@testing-library/user-event"; import { ObjectItem } from "mendix"; +import { createElement } from "react"; import { GalleryProps } from "../components/Gallery"; import { ItemEventsController } from "../features/item-interaction/ItemEventsController"; import { ItemHelper } from "../helpers/ItemHelper"; import { ItemHelperBuilder } from "./builders/ItemHelperBuilder"; -import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController"; -import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout"; export function setup(jsx: React.ReactElement): { user: UserEvent } & RenderResult { return { @@ -76,6 +76,8 @@ export function mockProps(params: Helpers & Mocks = {}): GalleryProps; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; itemClass?: ListExpressionValue; @@ -69,9 +70,10 @@ export interface GalleryPreviewProps { phoneItems: number | null; pageSize: number | null; pagination: PaginationEnum; - pagingPosition: PagingPositionEnum; - showPagingButtons: ShowPagingButtonsEnum; showTotalCount: boolean; + showPagingButtons: ShowPagingButtonsEnum; + pagingPosition: PagingPositionEnum; + loadMoreButtonCaption: string; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; itemClass: string; diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index fbee1be307..5f5c5b0b1f 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -90,6 +90,10 @@ export class DatasourceController implements ReactiveController, QueryController return this.datasource.totalCount; } + get hasMoreItems(): boolean { + return this.datasource.hasMoreItems ?? false; + } + /** * Returns computed value that holds controller copy. * Recomputes the copy every time the datasource changes. diff --git a/packages/shared/widget-plugin-grid/src/query/PaginationController.ts b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts index ff68aae944..58fbe1c7e0 100644 --- a/packages/shared/widget-plugin-grid/src/query/PaginationController.ts +++ b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts @@ -67,6 +67,10 @@ export class PaginationController implements ReactiveController { } } + get hasMoreItems(): boolean { + return this._query.hasMoreItems; + } + private _setInitParams(): void { if (this.pagination === "buttons" || this.showTotalCount) { this._query.requestTotalCount(true); diff --git a/packages/shared/widget-plugin-grid/src/query/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts index a7290cebbd..bc20172a63 100644 --- a/packages/shared/widget-plugin-grid/src/query/query-controller.ts +++ b/packages/shared/widget-plugin-grid/src/query/query-controller.ts @@ -8,11 +8,13 @@ type Members = | "requestTotalCount" | "totalCount" | "limit" - | "offset"; + | "offset" + | "hasMoreItems"; export interface QueryController extends Pick { refresh(): void; setPageSize(size: number): void; + hasMoreItems: boolean; isLoading: boolean; isRefreshing: boolean; isFetchingNextBatch: boolean;