From 87a8c83048cad6ec4db796a9d0a3c76e14f5d2d4 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:58:36 +0100 Subject: [PATCH 1/6] feat: start container migration --- .../pluggableWidgets/gallery-web/package.json | 2 + .../src/model/configs/Gallery.config.ts | 16 +++ .../src/model/containers/Gallery.container.ts | 123 ++++++++++++++++++ .../src/model/containers/Root.container.ts | 20 +++ .../model/services/GallerySetup.service.ts | 4 + .../src/model/services/QueryParams.service.ts | 44 +++++++ .../gallery-web/src/model/tokens.ts | 41 ++++++ .../src/typings/GalleryGateProps.ts | 4 + pnpm-lock.yaml | 6 + 9 files changed, 260 insertions(+) create mode 100644 packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/containers/Root.container.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/services/GallerySetup.service.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/services/QueryParams.service.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/tokens.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json index 81d9012097..16a68cea99 100644 --- a/packages/pluggableWidgets/gallery-web/package.json +++ b/packages/pluggableWidgets/gallery-web/package.json @@ -46,6 +46,8 @@ "@mendix/widget-plugin-filtering": "workspace:*", "@mendix/widget-plugin-mobx-kit": "workspace:^", "@mendix/widget-plugin-sorting": "workspace:*", + "brandi": "^5.0.0", + "brandi-react": "^5.0.0", "classnames": "^2.5.1", "mobx": "6.12.3", "mobx-react-lite": "4.0.7" diff --git a/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts b/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts new file mode 100644 index 0000000000..af17c6db71 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts @@ -0,0 +1,16 @@ +import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { GalleryContainerProps } from "../../../typings/GalleryProps"; + +export interface GalleryConfig { + id: string; + name: string; +} + +export function galleryConfig(props: GalleryContainerProps): GalleryConfig { + const id = `${props.name}:Gallery@${generateUUID()}`; + + return { + id, + name: props.name + }; +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts new file mode 100644 index 0000000000..5e3b0215eb --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts @@ -0,0 +1,123 @@ +import { WidgetFilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { CombinedFilter } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; +import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; +import { DatasourceService } from "@mendix/widget-plugin-grid/main"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; +import { Container, injected } from "brandi"; +import { GalleryGateProps } from "../../typings/GalleryGateProps"; +import { GalleryConfig } from "../configs/Gallery.config"; +import { QueryParamsService } from "../services/QueryParams.service"; +import { CORE_TOKENS as CORE, GY_TOKENS as GY } from "../tokens"; + +interface InitDependencies { + props: GalleryGateProps; + mainGate: DerivedPropsGate; + config: GalleryConfig; +} + +/** Just little utility object to group related bindings */ +interface BindingGroup { + /** Runs during container constructor. */ + define?(container: Container): void; + /** Runs on container init with deps. */ + init?(container: Container, deps: InitDependencies): void; + /** This method runs after init phase. */ + postInit?(container: Container): void; + /** This method runs only once. Should be used to inject dependencies. */ + inject?(): void; +} + +const coreBindings: BindingGroup = { + init(container, { mainGate, config }) { + container.bind(CORE.mainGate).toConstant(mainGate); + container.bind(CORE.config).toConstant(config); + } +}; + +const queryBindings: BindingGroup = { + define(container: Container) { + container.bind(GY.query).toInstance(DatasourceService).inSingletonScope(); + container.bind(GY.queryParams).toInstance(QueryParamsService).inSingletonScope(); + }, + init(container, { mainGate }) { + container.bind(GY.queryGate).toConstant(mainGate); + }, + postInit(container) { + // Create param service instance. + container.get(GY.queryParams); + }, + inject() { + injected(DatasourceService, CORE.setupService, GY.queryGate, GY.refreshInterval.optional); + injected(QueryParamsService, CORE.setupService, GY.query, GY.combinedFilter, GY.sortHost); + } +}; + +const filterBindings: BindingGroup = { + define(container: Container) { + container.bind(GY.filterAPI).toInstance(WidgetFilterAPI).inSingletonScope(); + container.bind(GY.filterHost).toInstance(CustomFilterHost).inSingletonScope(); + container.bind(GY.combinedFilter).toInstance(CombinedFilter).inSingletonScope(); + }, + init(container, { config }) { + container.bind(GY.combinedFilterConfig).toConstant({ + stableKey: config.name, + inputs: [container.get(GY.filterHost)] + }); + }, + inject() { + injected(CombinedFilter, CORE.setupService, GY.combinedFilterConfig); + injected(WidgetFilterAPI, GY.parentChannelName, GY.filterHost); + } +}; + +const sortBindings: BindingGroup = { + define(container) { + container.bind(GY.sortHost).toInstance(SortStoreHost).inSingletonScope(); + }, + init(container, { props }) { + container.bind(GY.sortHostConfig).toConstant({ initSort: props.datasource.sortOrder }); + }, + inject() { + injected(SortStoreHost, GY.sortHostConfig.optional); + } +}; + +const groups = [coreBindings, queryBindings, filterBindings, sortBindings]; + +// Inject tokens from groups +for (const grp of groups) { + grp.inject?.(); +} + +export class GalleryContainer extends Container { + id = `GalleryContainer@${generateUUID()}`; + + constructor(root: Container) { + super(); + this.extend(root); + + for (const grp of groups) { + grp.define?.(this); + } + } + + init(dependencies: { + props: GalleryGateProps; + mainGate: DerivedPropsGate; + config: GalleryConfig; + }): void { + for (const grp of groups) { + grp.init?.(this, dependencies); + } + + this.postInit(); + } + + private postInit(): void { + for (const grp of groups) { + grp.postInit?.(this); + } + } +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Root.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Root.container.ts new file mode 100644 index 0000000000..4e21762ede --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Root.container.ts @@ -0,0 +1,20 @@ +import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { Container } from "brandi"; +import { GallerySetupService } from "../services/GallerySetup.service"; +import { CORE_TOKENS as CORE } from "../tokens"; + +/** + * Root container for bindings that can be shared down the hierarchy. + * Declare only bindings that needs to be shared across multiple containers. + * @remark Don't bind constants or directly prop-dependent values here. + * Prop-derived atoms/stores via dependency injection are acceptable. + */ +export class RootContainer extends Container { + id = `GalleryRootContainer@${generateUUID()}`; + constructor() { + super(); + + // Setup service + this.bind(CORE.setupService).toInstance(GallerySetupService).inSingletonScope(); + } +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/services/GallerySetup.service.ts b/packages/pluggableWidgets/gallery-web/src/model/services/GallerySetup.service.ts new file mode 100644 index 0000000000..5676a32616 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/services/GallerySetup.service.ts @@ -0,0 +1,4 @@ +import { SetupHost } from "@mendix/widget-plugin-mobx-kit/SetupHost"; + +/** Host for components implemented setup hook */ +export class GallerySetupService extends SetupHost {} diff --git a/packages/pluggableWidgets/gallery-web/src/model/services/QueryParams.service.ts b/packages/pluggableWidgets/gallery-web/src/model/services/QueryParams.service.ts new file mode 100644 index 0000000000..15431e1f21 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/services/QueryParams.service.ts @@ -0,0 +1,44 @@ +import { QueryService } from "@mendix/widget-plugin-grid/main"; +import { disposeBatch, SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; +import { SortInstruction } from "@mendix/widget-plugin-sorting/types/store"; +import { FilterCondition } from "mendix/filters"; +import { reaction } from "mobx"; + +interface ObservableFilterStore { + filter: FilterCondition | undefined; +} + +interface ObservableSortStore { + sortOrder: SortInstruction[] | undefined; +} + +export class QueryParamsService implements SetupComponent { + constructor( + host: SetupComponentHost, + private query: QueryService, + private filters: ObservableFilterStore, + private sort: ObservableSortStore + ) { + host.add(this); + } + + setup(): () => void { + const [add, disposeAll] = disposeBatch(); + add( + reaction( + () => this.sort.sortOrder, + sortOrder => this.query.setSortOrder(sortOrder), + { fireImmediately: true } + ) + ); + add( + reaction( + () => this.filters.filter, + filter => this.query.setFilter(filter), + { fireImmediately: true } + ) + ); + + return disposeAll; + } +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts new file mode 100644 index 0000000000..eeef948a89 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts @@ -0,0 +1,41 @@ +/** Tokens to resolve dependencies from the container. */ + +import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { CombinedFilter, CombinedFilterConfig } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; +import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; +import { QueryService } from "@mendix/widget-plugin-grid/main"; +import { DerivedPropsGate, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; +import { SortInstruction } from "@mendix/widget-plugin-sorting/types/store"; +import { token } from "brandi"; +import { ListValue } from "mendix"; +import { GalleryGateProps } from "../typings/GalleryGateProps"; +import { GalleryConfig } from "./configs/Gallery.config"; +import { QueryParamsService } from "./services/QueryParams.service"; + +const label = (name: string): string => `Gallery/${name}`; + +/** Core tokens shared across containers through root container. */ +export const CORE_TOKENS = { + mainGate: token>(label("@gate:mainGate")), + setupService: token(label("@service:setupService")), + config: token(label("@config:galleryConfig")) +}; + +export const GY_TOKENS = { + // datasource + query: token(label("@service:query")), + queryGate: token>(label("@gate:queryGate")), + refreshInterval: token(label("@const:refreshInterval")), + queryParams: token(label("@service:queryParams")), + + // filtering + combinedFilter: token(label("@service:combinedFilter")), + combinedFilterConfig: token(label("@config:combinedFilterConfig")), + filterAPI: token(label("@service:filterAPI")), + filterHost: token(label("@service:filterHost")), + parentChannelName: token(label("@const:parentChannelName")), + + // sorting + sortHost: token<{ sortOrder: SortInstruction[] | undefined }>(label("@service:sortHost")), + sortHostConfig: token<{ initSort: SortInstruction[] }>(label("@config:sortHostConfig")) +}; diff --git a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts new file mode 100644 index 0000000000..8afe10c688 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts @@ -0,0 +1,4 @@ +import { GalleryContainerProps } from "../../typings/GalleryProps"; + +/** Type to declare props available through main gate. */ +export type GalleryGateProps = Pick; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a058423185..176ae8fca4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1563,6 +1563,12 @@ importers: '@mendix/widget-plugin-sorting': specifier: workspace:* version: link:../../shared/widget-plugin-sorting + brandi: + specifier: ^5.0.0 + version: 5.0.0 + brandi-react: + specifier: ^5.0.0 + version: 5.0.0(brandi@5.0.0)(react@18.3.1) classnames: specifier: ^2.5.1 version: 2.5.1 From d1e8c7fdb55df07f4aef132032a3e15bed3687a9 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:03:53 +0100 Subject: [PATCH 2/6] feat: add gallery v2 --- .../src/components/WidgetRoot.tsx | 1 + .../gallery-web/src/Gallery.tsx | 125 ++---------------- .../src/components/GalleryHeader.tsx | 13 +- .../src/components/GalleryRoot.tsx | 27 ++-- .../gallery-web/src/components/GalleryV2.tsx | 15 +++ .../features/base/GalleryRoot.viewModel.ts | 27 ++++ .../src/model/containers/Gallery.container.ts | 20 ++- .../containers/createGalleryContainer.ts | 21 +++ .../src/model/hooks/injection-hooks.ts | 5 + .../src/model/hooks/useGalleryContainer.ts | 13 ++ .../src/model/services/Texts.service.ts | 13 ++ .../gallery-web/src/model/tokens.ts | 6 +- .../src/typings/GalleryGateProps.ts | 5 +- 13 files changed, 145 insertions(+), 146 deletions(-) create mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/features/base/GalleryRoot.viewModel.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/containers/createGalleryContainer.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/hooks/useGalleryContainer.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/services/Texts.service.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx index d005ac1201..df477acb54 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import { PropsWithChildren, ReactElement } from "react"; import { useDatagridRootVM } from "../model/hooks/injection-hooks"; +/** @remark vm source `WidgetRoot.viewModel.ts` */ export const WidgetRoot = observer(function WidgetRoot({ children }: PropsWithChildren): ReactElement { const vm = useDatagridRootVM(); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index 4e2262c42a..18990717f6 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -1,126 +1,19 @@ -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 { ContainerProvider } from "brandi-react"; import { observer } from "mobx-react-lite"; -import { ReactElement, ReactNode, useCallback } from "react"; +import { ReactElement } 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 { GalleryContext, GalleryRootScope, useGalleryRootScope } from "./helpers/root-context"; -import { useGalleryJSActions } from "./helpers/useGalleryJSActions"; -import { useGalleryStore } from "./helpers/useGalleryStore"; -import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; +import { useGalleryContainer } from "./model/hooks/useGalleryContainer"; -const Container = observer(function GalleryContainer(props: GalleryContainerProps): ReactElement { - const { rootStore, itemSelectHelper } = useGalleryRootScope(); - - const items = props.datasource.items ?? []; - const config: GridPositionsProps = { - desktopItems: props.desktopItems, - phoneItems: props.phoneItems, - tabletItems: props.tabletItems, - totalItems: items.length - }; - const { numberOfColumns, numberOfRows } = useGridPositions(config); - const getPositionCallback = useCallback( - (index: number) => getColumnAndRowBasedOnIndex(numberOfColumns, items.length, index), - [numberOfColumns, items.length] - ); - - const focusController = useFocusTargetController({ - rows: numberOfRows, - columns: numberOfColumns, - pageSize: props.pageSize - }); - - const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: props.onClickTrigger }); - - const itemEventsController = useItemEventsController( - itemSelectHelper, - clickActionHelper, - focusController, - numberOfColumns, - props.itemSelectionMode - ); - - const itemHelper = useItemHelper({ - classValue: props.itemClass, - contentValue: props.content, - clickValue: props.onClick - }); - - useGalleryJSActions(rootStore, itemSelectHelper); - - const header = {props.filtersPlaceholder}; - - return ( - ReactElement) => - props.showEmptyPlaceholder === "custom" ? renderWrapper(props.emptyPlaceholder) :
, - [props.emptyPlaceholder, props.showEmptyPlaceholder] - )} - emptyMessageTitle={props.emptyMessageTitle?.value} - header={header} - headerTitle={props.filterSectionTitle?.value} - ariaLabelListBox={props.ariaLabelListBox?.value} - showHeader={!!props.filtersPlaceholder} - hasMoreItems={props.datasource.hasMoreItems ?? false} - items={items} - itemHelper={itemHelper} - numberOfItems={props.datasource.totalCount} - page={rootStore.paging.currentPage} - pageSize={props.pageSize} - paging={rootStore.paging.showPagination} - paginationPosition={props.pagingPosition} - paginationType={props.pagination} - setPage={rootStore.paging.setPage} - showPagingButtons={props.showPagingButtons} - phoneItems={props.phoneItems} - style={props.style} - tabletItems={props.tabletItems} - tabIndex={props.tabIndex} - selectHelper={itemSelectHelper} - itemEventsController={itemEventsController} - focusController={focusController} - getPosition={getPositionCallback} - loadMoreButtonCaption={props.loadMoreButtonCaption?.value} - showRefreshIndicator={rootStore.loaderCtrl.showRefreshIndicator} - selectionCountPosition={props.selectionCountPosition} - /> - ); +const GalleryWidget = observer(function GalleryWidget(): ReactElement { + return
; }); -function useCreateGalleryScope(props: GalleryContainerProps): GalleryRootScope { - const rootStore = useGalleryStore(props); - const selectionHelper = useSelectionHelper( - props.itemSelection, - props.datasource, - props.onSelectionChange, - props.keepSelection ? "always keep" : "always clear" - ); - const itemSelectHelper = useItemSelectHelper(props.itemSelection, selectionHelper); - - return useConst({ - rootStore, - selectionHelper, - itemSelectHelper, - selectionCountStore: rootStore.selectionCountStore - }); -} - export function Gallery(props: GalleryContainerProps): ReactElement { - const scope = useCreateGalleryScope(props); + const container = useGalleryContainer(props); return ( - - - + + + ); } diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx index 5d0d96a09e..81d7a5f95b 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx @@ -1,13 +1,12 @@ -import { JSX, ReactElement } from "react"; +import { ReactElement } from "react"; +import { useMainGate } from "../model/hooks/injection-hooks"; -type GalleryHeaderProps = Omit; +export function GalleryHeader(): ReactElement | null { + const { filtersPlaceholder } = useMainGate().props; -export function GalleryHeader(props: GalleryHeaderProps): ReactElement | null { - const { children } = props; - - if (!children) { + if (!filtersPlaceholder) { return null; } - return
; + return
{filtersPlaceholder}
; } diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx index bab0eee139..889c9e7050 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx @@ -1,25 +1,16 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; +import { PropsWithChildren, ReactElement } from "react"; +import { useGalleryRootVM } from "../model/hooks/injection-hooks"; -export type GalleryRootProps = Omit & { - selectable?: boolean; -}; - -export function GalleryRoot(props: GalleryRootProps): ReactElement { - const { className, selectable, children, ...rest } = props; +/** @remark vm source `GalleryRoot.viewModel.ts` */ +export const GalleryRoot = observer(function GalleryRoot(props: PropsWithChildren): ReactElement { + const { children } = props; + const vm = useGalleryRootVM(); return ( -
+
{children}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx new file mode 100644 index 0000000000..2128720327 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; +import { GalleryHeader as Header } from "./GalleryHeader"; +import { GalleryRoot as Root } from "./GalleryRoot"; +import { GalleryTopBar as TopBar } from "./GalleryTopBar"; + +export function Gallery(): ReactElement { + return ( + + +
+
+
+ + ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/features/base/GalleryRoot.viewModel.ts b/packages/pluggableWidgets/gallery-web/src/features/base/GalleryRoot.viewModel.ts new file mode 100644 index 0000000000..3564d7b182 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/features/base/GalleryRoot.viewModel.ts @@ -0,0 +1,27 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { makeAutoObservable } from "mobx"; +import { CSSProperties } from "react"; + +export class GalleryRootViewModel { + constructor( + private gate: DerivedPropsGate<{ + style?: CSSProperties; + class?: string; + tabIndex?: number; + }> + ) { + makeAutoObservable(this); + } + + get className(): string | undefined { + return this.gate.props.class; + } + + get style(): CSSProperties | undefined { + return this.gate.props.style; + } + + get tabIndex(): number { + return this.gate.props.tabIndex ?? 0; + } +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts index 5e3b0215eb..41be63b6c4 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts @@ -6,6 +6,7 @@ import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; import { Container, injected } from "brandi"; +import { GalleryRootViewModel } from "../../features/base/GalleryRoot.viewModel"; import { GalleryGateProps } from "../../typings/GalleryGateProps"; import { GalleryConfig } from "../configs/Gallery.config"; import { QueryParamsService } from "../services/QueryParams.service"; @@ -84,7 +85,16 @@ const sortBindings: BindingGroup = { } }; -const groups = [coreBindings, queryBindings, filterBindings, sortBindings]; +const viewBindings: BindingGroup = { + define(container) { + container.bind(GY.galleryRootVM).toInstance(GalleryRootViewModel).inSingletonScope(); + }, + inject() { + injected(GalleryRootViewModel, CORE.mainGate); + } +}; + +const groups = [coreBindings, queryBindings, filterBindings, sortBindings, viewBindings]; // Inject tokens from groups for (const grp of groups) { @@ -107,17 +117,21 @@ export class GalleryContainer extends Container { props: GalleryGateProps; mainGate: DerivedPropsGate; config: GalleryConfig; - }): void { + }): GalleryContainer { for (const grp of groups) { grp.init?.(this, dependencies); } this.postInit(); + + return this; } - private postInit(): void { + private postInit(): GalleryContainer { for (const grp of groups) { grp.postInit?.(this); } + + return this; } } diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/createGalleryContainer.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/createGalleryContainer.ts new file mode 100644 index 0000000000..bec6051ae6 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/createGalleryContainer.ts @@ -0,0 +1,21 @@ +import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; +import { GalleryContainerProps } from "../../../typings/GalleryProps"; +import { GalleryGateProps } from "../../typings/GalleryGateProps"; +import { galleryConfig } from "../configs/Gallery.config"; +import { GalleryContainer } from "./Gallery.container"; +import { RootContainer } from "./Root.container"; + +export function createGalleryContainer( + props: GalleryContainerProps +): [GalleryContainer, GateProvider] { + const root = new RootContainer(); + const config = galleryConfig(props); + const mainProvider = new GateProvider(props); + const container = new GalleryContainer(root).init({ + props, + config, + mainGate: mainProvider.gate + }); + + return [container, mainProvider]; +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts new file mode 100644 index 0000000000..3b9fc55ec3 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts @@ -0,0 +1,5 @@ +import { createInjectionHooks } from "brandi-react"; +import { CORE_TOKENS as CORE, GY_TOKENS as GY } from "../tokens"; + +export const [useGalleryRootVM] = createInjectionHooks(GY.galleryRootVM); +export const [useMainGate] = createInjectionHooks(CORE.mainGate); diff --git a/packages/pluggableWidgets/gallery-web/src/model/hooks/useGalleryContainer.ts b/packages/pluggableWidgets/gallery-web/src/model/hooks/useGalleryContainer.ts new file mode 100644 index 0000000000..cdf4b3cec0 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/hooks/useGalleryContainer.ts @@ -0,0 +1,13 @@ +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { Container } from "brandi"; +import { useEffect } from "react"; +import { GalleryContainerProps } from "../../../typings/GalleryProps"; +import { createGalleryContainer } from "../containers/createGalleryContainer"; + +export function useGalleryContainer(props: GalleryContainerProps): Container { + const [container, mainProvider] = useConst(() => createGalleryContainer(props)); + + useEffect(() => mainProvider.setProps(props)); + + return container; +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/services/Texts.service.ts b/packages/pluggableWidgets/gallery-web/src/model/services/Texts.service.ts new file mode 100644 index 0000000000..82345cafd0 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/services/Texts.service.ts @@ -0,0 +1,13 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { makeAutoObservable } from "mobx"; +import { GalleryGateProps } from "../../typings/GalleryGateProps"; + +export class TextsService { + constructor(private gate: DerivedPropsGate) { + makeAutoObservable(this); + } + + get headerAriaLabel(): string | undefined { + return this.gate.props.filterSectionTitle?.value; + } +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts index eeef948a89..8254f740a6 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts @@ -8,6 +8,7 @@ import { DerivedPropsGate, SetupComponentHost } from "@mendix/widget-plugin-mobx import { SortInstruction } from "@mendix/widget-plugin-sorting/types/store"; import { token } from "brandi"; import { ListValue } from "mendix"; +import { GalleryRootViewModel } from "../features/base/GalleryRoot.viewModel"; import { GalleryGateProps } from "../typings/GalleryGateProps"; import { GalleryConfig } from "./configs/Gallery.config"; import { QueryParamsService } from "./services/QueryParams.service"; @@ -37,5 +38,8 @@ export const GY_TOKENS = { // sorting sortHost: token<{ sortOrder: SortInstruction[] | undefined }>(label("@service:sortHost")), - sortHostConfig: token<{ initSort: SortInstruction[] }>(label("@config:sortHostConfig")) + sortHostConfig: token<{ initSort: SortInstruction[] }>(label("@config:sortHostConfig")), + + // gallery root + galleryRootVM: token(label("@viewModel:galleryRootVM")) }; diff --git a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts index 8afe10c688..b01bf3565d 100644 --- a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts +++ b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts @@ -1,4 +1,7 @@ import { GalleryContainerProps } from "../../typings/GalleryProps"; /** Type to declare props available through main gate. */ -export type GalleryGateProps = Pick; +export type GalleryGateProps = Pick< + GalleryContainerProps, + "name" | "style" | "class" | "datasource" | "tabIndex" | "filterSectionTitle" | "filtersPlaceholder" +>; From 593f8e17867e0eb0cd54ab24971ca604106e3ff5 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:06:49 +0100 Subject: [PATCH 3/6] feat: start pagination --- .../gallery-web/src/components/GalleryV2.tsx | 16 ++++ .../gallery-web/src/components/Pagination.tsx | 6 ++ .../src/model/containers/Gallery.container.ts | 59 ++++++++----- .../services/Loder.service.ts} | 19 ++-- .../gallery-web/src/model/tokens.ts | 7 +- .../src/services/PaginationController.ts | 88 ------------------- .../gallery-web/src/stores/GalleryStore.ts | 6 +- .../src/typings/GalleryGateProps.ts | 11 ++- 8 files changed, 88 insertions(+), 124 deletions(-) create mode 100644 packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx rename packages/pluggableWidgets/gallery-web/src/{controllers/DerivedLoaderController.ts => model/services/Loder.service.ts} (54%) delete mode 100644 packages/pluggableWidgets/gallery-web/src/services/PaginationController.ts diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx index 2128720327..e72dad268f 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx @@ -1,4 +1,5 @@ import { ReactElement } from "react"; +import { GalleryFooter } from "./GalleryFooter"; import { GalleryHeader as Header } from "./GalleryHeader"; import { GalleryRoot as Root } from "./GalleryRoot"; import { GalleryTopBar as TopBar } from "./GalleryTopBar"; @@ -10,6 +11,21 @@ export function Gallery(): ReactElement {
+ +
+ {showBottomSelectionCounter &&
{selectionCounter}
} + +
+ {showBottomPagination && pagination} + {props.paginationType === "loadMore" && + (props.preview ? ( + {loadMoreButtonCaption} + ) : ( + {loadMoreButtonCaption} + ))} +
+
+
); } diff --git a/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx new file mode 100644 index 0000000000..2fff223875 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx @@ -0,0 +1,6 @@ +import { observer } from "mobx-react-lite"; +import { ReactElement } from "react"; + +export const Pagination = observer(function Pagination(): ReactElement { + return
Pagination
; +}); diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts index 41be63b6c4..8417c6e856 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts @@ -9,6 +9,7 @@ import { Container, injected } from "brandi"; import { GalleryRootViewModel } from "../../features/base/GalleryRoot.viewModel"; import { GalleryGateProps } from "../../typings/GalleryGateProps"; import { GalleryConfig } from "../configs/Gallery.config"; +import { LoaderService } from "../services/Loder.service"; import { QueryParamsService } from "../services/QueryParams.service"; import { CORE_TOKENS as CORE, GY_TOKENS as GY } from "../tokens"; @@ -25,7 +26,7 @@ interface BindingGroup { /** Runs on container init with deps. */ init?(container: Container, deps: InitDependencies): void; /** This method runs after init phase. */ - postInit?(container: Container): void; + postInit?(container: Container, deps: InitDependencies): void; /** This method runs only once. Should be used to inject dependencies. */ inject?(): void; } @@ -38,6 +39,10 @@ const coreBindings: BindingGroup = { }; const queryBindings: BindingGroup = { + inject() { + injected(DatasourceService, CORE.setupService, GY.queryGate, GY.refreshInterval.optional); + injected(QueryParamsService, CORE.setupService, GY.query, GY.combinedFilter, GY.sortHost); + }, define(container: Container) { container.bind(GY.query).toInstance(DatasourceService).inSingletonScope(); container.bind(GY.queryParams).toInstance(QueryParamsService).inSingletonScope(); @@ -48,14 +53,14 @@ const queryBindings: BindingGroup = { postInit(container) { // Create param service instance. container.get(GY.queryParams); - }, - inject() { - injected(DatasourceService, CORE.setupService, GY.queryGate, GY.refreshInterval.optional); - injected(QueryParamsService, CORE.setupService, GY.query, GY.combinedFilter, GY.sortHost); } }; const filterBindings: BindingGroup = { + inject() { + injected(CombinedFilter, CORE.setupService, GY.combinedFilterConfig); + injected(WidgetFilterAPI, GY.parentChannelName, GY.filterHost); + }, define(container: Container) { container.bind(GY.filterAPI).toInstance(WidgetFilterAPI).inSingletonScope(); container.bind(GY.filterHost).toInstance(CustomFilterHost).inSingletonScope(); @@ -67,34 +72,52 @@ const filterBindings: BindingGroup = { inputs: [container.get(GY.filterHost)] }); }, - inject() { - injected(CombinedFilter, CORE.setupService, GY.combinedFilterConfig); - injected(WidgetFilterAPI, GY.parentChannelName, GY.filterHost); + postInit(container, { props }) { + // Hydrate filters from props + container.get(GY.combinedFilter).hydrate(props.datasource.filter); } }; const sortBindings: BindingGroup = { + inject() { + injected(SortStoreHost, GY.sortHostConfig.optional); + }, define(container) { container.bind(GY.sortHost).toInstance(SortStoreHost).inSingletonScope(); }, init(container, { props }) { container.bind(GY.sortHostConfig).toConstant({ initSort: props.datasource.sortOrder }); - }, - inject() { - injected(SortStoreHost, GY.sortHostConfig.optional); } }; const viewBindings: BindingGroup = { + inject() { + injected(GalleryRootViewModel, CORE.mainGate); + }, define(container) { container.bind(GY.galleryRootVM).toInstance(GalleryRootViewModel).inSingletonScope(); - }, + } +}; + +const loaderBindings: BindingGroup = { inject() { - injected(GalleryRootViewModel, CORE.mainGate); + injected(LoaderService, GY.query, GY.loaderConfig); + }, + define(container: Container) { + container.bind(GY.loader).toInstance(LoaderService).inSingletonScope(); + }, + init(container, { props }) { + container.bind(GY.loaderConfig).toConstant({ + refreshIndicator: props.refreshIndicator, + showSilentRefresh: props.refreshInterval > 1 + }); + }, + postInit(container) { + container.get(GY.loader); } }; -const groups = [coreBindings, queryBindings, filterBindings, sortBindings, viewBindings]; +const groups = [coreBindings, queryBindings, filterBindings, sortBindings, viewBindings, loaderBindings]; // Inject tokens from groups for (const grp of groups) { @@ -122,14 +145,8 @@ export class GalleryContainer extends Container { grp.init?.(this, dependencies); } - this.postInit(); - - return this; - } - - private postInit(): GalleryContainer { for (const grp of groups) { - grp.postInit?.(this); + grp.postInit?.(this, dependencies); } return this; diff --git a/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts b/packages/pluggableWidgets/gallery-web/src/model/services/Loder.service.ts similarity index 54% rename from packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts rename to packages/pluggableWidgets/gallery-web/src/model/services/Loder.service.ts index 0585e621db..468fa7fe45 100644 --- a/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/services/Loder.service.ts @@ -1,11 +1,10 @@ -import { DatasourceService } from "@mendix/widget-plugin-grid/main"; +import { QueryService } from "@mendix/widget-plugin-grid/main"; import { computed, makeObservable } from "mobx"; -export class DerivedLoaderController { +export class LoaderService { constructor( - private datasourceService: DatasourceService, - private refreshIndicator: boolean, - private showSilentRefresh: boolean + private query: QueryService, + private config: { refreshIndicator: boolean; showSilentRefresh: boolean } ) { makeObservable(this, { isFirstLoad: computed, @@ -15,17 +14,17 @@ export class DerivedLoaderController { } get isFirstLoad(): boolean { - return this.datasourceService.isFirstLoad; + return this.query.isFirstLoad; } get isFetchingNextBatch(): boolean { - return this.datasourceService.isFetchingNextBatch; + return this.query.isFetchingNextBatch; } get isRefreshing(): boolean { - const { isSilentRefresh, isRefreshing } = this.datasourceService; + const { isSilentRefresh, isRefreshing } = this.query; - if (this.showSilentRefresh) { + if (this.config.showSilentRefresh) { return isSilentRefresh || isRefreshing; } @@ -33,7 +32,7 @@ export class DerivedLoaderController { } get showRefreshIndicator(): boolean { - if (!this.refreshIndicator) { + if (!this.config.refreshIndicator) { return false; } diff --git a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts index 8254f740a6..2ad397515b 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts @@ -11,6 +11,7 @@ import { ListValue } from "mendix"; import { GalleryRootViewModel } from "../features/base/GalleryRoot.viewModel"; import { GalleryGateProps } from "../typings/GalleryGateProps"; import { GalleryConfig } from "./configs/Gallery.config"; +import { LoaderService } from "./services/Loder.service"; import { QueryParamsService } from "./services/QueryParams.service"; const label = (name: string): string => `Gallery/${name}`; @@ -41,5 +42,9 @@ export const GY_TOKENS = { sortHostConfig: token<{ initSort: SortInstruction[] }>(label("@config:sortHostConfig")), // gallery root - galleryRootVM: token(label("@viewModel:galleryRootVM")) + galleryRootVM: token(label("@viewModel:galleryRootVM")), + + // loader + loaderConfig: token<{ refreshIndicator: boolean; showSilentRefresh: boolean }>(label("@config:loaderConfig")), + loader: token(label("@service:loader")) }; diff --git a/packages/pluggableWidgets/gallery-web/src/services/PaginationController.ts b/packages/pluggableWidgets/gallery-web/src/services/PaginationController.ts deleted file mode 100644 index 1e8482edc7..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/services/PaginationController.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { QueryService } from "@mendix/widget-plugin-grid/main"; - -type PaginationEnum = "buttons" | "virtualScrolling" | "loadMore"; - -type ShowPagingButtonsEnum = "always" | "auto"; - -type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}`; - -interface StaticProps { - pageSize: number; - pagination: PaginationEnum; - showPagingButtons: ShowPagingButtonsEnum; - showTotalCount: boolean; -} - -/** NOTE: Use gate for dynamic props */ -type PaginationControllerSpec = StaticProps & { - query: QueryService; -}; - -export class PaginationController { - private readonly _pageSize: number; - private readonly _query: QueryService; - readonly pagination: PaginationEnum; - readonly paginationKind: PaginationKind; - readonly showPagingButtons: ShowPagingButtonsEnum; - readonly showTotalCount: boolean; - - constructor(spec: PaginationControllerSpec) { - this._pageSize = spec.pageSize; - this._query = spec.query; - this.pagination = spec.pagination; - this.showPagingButtons = spec.showPagingButtons; - this.showTotalCount = spec.showTotalCount; - this.paginationKind = `${this.pagination}.${this.showPagingButtons}`; - this._setInitParams(); - } - - get isLimitBased(): boolean { - return this.pagination === "virtualScrolling" || this.pagination === "loadMore"; - } - - get pageSize(): number { - return this._pageSize; - } - - get currentPage(): number { - const { - _query: { limit, offset }, - pageSize - } = this; - return this.isLimitBased ? limit / pageSize : offset / pageSize; - } - - get showPagination(): boolean { - switch (this.paginationKind) { - case "buttons.always": - return true; - case "buttons.auto": { - const { totalCount = -1 } = this._query; - return totalCount > this._query.limit; - } - default: - return this.showTotalCount; - } - } - - get hasMoreItems(): boolean { - return this._query.hasMoreItems; - } - - private _setInitParams(): void { - if (this.pagination === "buttons" || this.showTotalCount) { - this._query.requestTotalCount(true); - } - - this._query.setBaseLimit(this.pageSize); - } - - setPage = (computePage: (prevPage: number) => number): void => { - const newPage = computePage(this.currentPage); - if (this.isLimitBased) { - this._query.setLimit(newPage * this.pageSize); - } else { - this._query.setOffset(newPage * this.pageSize); - } - }; -} diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts index 448a280229..d96116969f 100644 --- a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts +++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts @@ -9,8 +9,8 @@ import { SortAPI } from "@mendix/widget-plugin-sorting/react/context"; import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; import { DynamicValue, EditableValue, ListValue, SelectionMultiValue, SelectionSingleValue } from "mendix"; import { PaginationEnum, StateStorageTypeEnum } from "../../typings/GalleryProps"; -import { DerivedLoaderController } from "../controllers/DerivedLoaderController"; import { QueryParamsController } from "../controllers/QueryParamsController"; +import { LoaderService } from "../model/services/Loder.service"; import { PaginationController } from "../services/PaginationController"; import { ObservableStorage } from "../typings/storage"; import { AttributeStorage } from "./AttributeStorage"; @@ -56,7 +56,7 @@ export class GalleryStore extends SetupHost { readonly paging: PaginationController; readonly filterAPI: FilterAPI; readonly sortAPI: SortAPI; - loaderCtrl: DerivedLoaderController; + loaderCtrl: LoaderService; selectionCountStore: SelectionCounterViewModel; constructor(spec: GalleryStoreSpec) { @@ -96,7 +96,7 @@ export class GalleryStore extends SetupHost { host: this._sortHost }; - this.loaderCtrl = new DerivedLoaderController(this._query, spec.refreshIndicator, spec.refreshInterval >= 1); + this.loaderCtrl = new LoaderService(this._query, spec.refreshIndicator, spec.refreshInterval >= 1); const useStorage = spec.storeFilters || spec.storeSort; if (useStorage) { diff --git a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts index b01bf3565d..436cece14e 100644 --- a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts +++ b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts @@ -3,5 +3,14 @@ import { GalleryContainerProps } from "../../typings/GalleryProps"; /** Type to declare props available through main gate. */ export type GalleryGateProps = Pick< GalleryContainerProps, - "name" | "style" | "class" | "datasource" | "tabIndex" | "filterSectionTitle" | "filtersPlaceholder" + | "name" + | "style" + | "class" + | "datasource" + | "tabIndex" + | "filterSectionTitle" + | "filtersPlaceholder" + | "refreshIndicator" + | "refreshInterval" + | "selectionCountPosition" >; From 8742c1044088190b35a581b5455d36218e75bf04 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:43:04 +0100 Subject: [PATCH 4/6] feat: move to brandi --- .../features/pagination/pagination.model.ts | 32 -- .../model/containers/Datagrid.container.ts | 24 +- .../datagrid-web/src/model/tokens.ts | 12 +- .../gallery-web/src/Gallery.editorPreview.tsx | 109 +--- .../gallery-web/src/Gallery.tsx | 6 +- .../gallery-web/src/Gallery.xml | 87 +++- .../src/components/EmptyPlaceholder.tsx | 15 + .../gallery-web/src/components/Gallery.tsx | 167 ------ .../src/components/GalleryContent.tsx | 31 +- .../src/components/GalleryFooter.tsx | 12 +- .../src/components/GalleryFooterControls.tsx | 35 ++ .../src/components/GalleryHeader.tsx | 25 +- .../src/components/GalleryItems.tsx | 32 ++ .../src/components/GalleryRoot.tsx | 1 - .../src/components/GalleryTobBarControls.tsx | 26 + .../gallery-web/src/components/GalleryV2.tsx | 31 -- .../src/components/GalleryWidget.tsx | 28 + .../src/components/HeaderWidgetsHost.tsx | 22 - .../gallery-web/src/components/ListBox.tsx | 6 +- .../gallery-web/src/components/ListItem.tsx | 48 +- .../gallery-web/src/components/LoadMore.tsx | 14 +- .../gallery-web/src/components/Pagination.tsx | 25 +- .../src/components/SelectionCounter.tsx | 35 -- .../src/components/__tests__/Gallery.spec.tsx | 209 -------- .../components/__tests__/GalleryRoot.spec.tsx | 30 ++ .../__snapshots__/Gallery.spec.tsx.snap | 492 ------------------ .../__snapshots__/GalleryRoot.spec.tsx.snap | 11 + ...sController.ts => ItemEvents.viewModel.ts} | 63 +-- .../item-interaction/get-item-aria-props.ts | 4 +- .../selection-counter/SelectionCounter.tsx | 20 + .../selection-counter/injection-hooks.ts | 4 + .../src/features/useGridPositions.ts | 55 -- .../src/features/useGridPositionsPreview.ts | 54 -- .../gallery-web/src/helpers/ItemHelper.tsx | 47 -- .../src/helpers/ItemPreviewHelper.tsx | 50 -- .../gallery-web/src/helpers/root-context.ts | 21 - .../src/helpers/useGalleryJSActions.ts | 11 - .../src/helpers/useGalleryStore.ts | 18 - .../src/helpers/useItemSelectHelper.ts | 10 - .../src/model/configs/Gallery.config.ts | 37 +- .../model/configs/GalleryPagination.config.ts | 60 +++ .../GalleryPagination.config.spec.ts | 12 + .../src/model/containers/Gallery.container.ts | 203 +++++++- .../src/model/containers/Root.container.ts | 82 ++- .../__tests__/createGalleryContainer.spec.ts | 65 +++ .../src/model/hooks/injection-hooks.ts | 21 + .../src/model/models/items.model.ts | 11 + .../src/model/models/layout.model.ts | 19 + .../src/model/services/Layout.service.ts | 87 ++++ .../{Loder.service.ts => Loader.service.ts} | 0 .../model/services/SelectionGate.service.ts | 17 + .../src/model/services/Texts.service.ts | 4 + .../gallery-web/src/model/tokens.ts | 104 +++- .../gallery-web/src/stores/GalleryStore.ts | 131 ----- .../src/typings/GalleryGateProps.ts | 26 + .../src/utils/builders/ItemHelperBuilder.ts | 40 -- .../src/utils/mock-container-props.ts | 33 ++ .../gallery-web/src/utils/test-utils.tsx | 161 ------ .../view-models/EmptyPlaceholder.viewModel.ts | 27 + .../src/view-models/GalleryItem.viewModel.ts | 49 ++ .../GalleryRoot.viewModel.ts | 0 .../__tests__/GalleryItem.viewModel.spec.tsx | 157 ++++++ .../__tests__/GalleryRoot.viewModel.spec.tsx | 39 ++ .../gallery-web/typings/GalleryProps.d.ts | 25 +- .../src/interfaces}/GridPageControl.ts | 0 .../shared/widget-plugin-grid/src/main.ts | 5 +- .../pagination/DynamicPagination.feature.ts | 2 +- .../src}/pagination/PageControl.service.ts | 4 +- .../src}/pagination/Pagination.viewModel.ts | 33 +- .../widget-plugin-grid/src/pagination/main.ts | 7 + .../src/pagination/pagination.model.ts | 38 +- 71 files changed, 1543 insertions(+), 1878 deletions(-) delete mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap create mode 100644 packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/GalleryRoot.spec.tsx.snap rename packages/pluggableWidgets/gallery-web/src/features/item-interaction/{ItemEventsController.ts => ItemEvents.viewModel.ts} (71%) create mode 100644 packages/pluggableWidgets/gallery-web/src/features/selection-counter/SelectionCounter.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/features/selection-counter/injection-hooks.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/features/useGridPositions.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/features/useGridPositionsPreview.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/ItemHelper.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/ItemPreviewHelper.tsx delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/root-context.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/useGalleryJSActions.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/useGalleryStore.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/configs/GalleryPagination.config.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/configs/__tests__/GalleryPagination.config.spec.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/containers/__tests__/createGalleryContainer.spec.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/models/items.model.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/models/layout.model.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/model/services/Layout.service.ts rename packages/pluggableWidgets/gallery-web/src/model/services/{Loder.service.ts => Loader.service.ts} (100%) create mode 100644 packages/pluggableWidgets/gallery-web/src/model/services/SelectionGate.service.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/utils/builders/ItemHelperBuilder.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/utils/mock-container-props.ts delete mode 100644 packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/view-models/EmptyPlaceholder.viewModel.ts create mode 100644 packages/pluggableWidgets/gallery-web/src/view-models/GalleryItem.viewModel.ts rename packages/pluggableWidgets/gallery-web/src/{features/base => view-models}/GalleryRoot.viewModel.ts (100%) create mode 100644 packages/pluggableWidgets/gallery-web/src/view-models/__tests__/GalleryItem.viewModel.spec.tsx create mode 100644 packages/pluggableWidgets/gallery-web/src/view-models/__tests__/GalleryRoot.viewModel.spec.tsx rename packages/{pluggableWidgets/datagrid-web/src/features/pagination => shared/widget-plugin-grid/src/interfaces}/GridPageControl.ts (100%) rename packages/{pluggableWidgets/datagrid-web/src/features => shared/widget-plugin-grid/src}/pagination/DynamicPagination.feature.ts (96%) rename packages/{pluggableWidgets/datagrid-web/src/features => shared/widget-plugin-grid/src}/pagination/PageControl.service.ts (85%) rename packages/{pluggableWidgets/datagrid-web/src/features => shared/widget-plugin-grid/src}/pagination/Pagination.viewModel.ts (69%) create mode 100644 packages/shared/widget-plugin-grid/src/pagination/main.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts deleted file mode 100644 index 7d19cf3143..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { boundPageSize } from "@mendix/widget-plugin-grid/main"; -import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; -import { computed } from "mobx"; -import { ReactNode } from "react"; - -/** Atom for the dynamic page index provided by the widget's props. */ -export function dynamicPageAtom( - gate: DerivedPropsGate<{ dynamicPage?: { value?: Big } }>, - config: { isLimitBased: boolean } -): ComputedAtom { - return computed(() => { - const page = gate.props.dynamicPage?.value?.toNumber() ?? -1; - if (config.isLimitBased) { - return Math.max(page, -1); - } - // Switch to zero-based index for offset-based pagination - return Math.max(page - 1, -1); - }); -} - -/** Atom for the dynamic page size. */ -export function dynamicPageSizeAtom( - gate: DerivedPropsGate<{ dynamicPageSize?: { value?: Big } }> -): ComputedAtom { - return boundPageSize(() => gate.props.dynamicPageSize?.value?.toNumber() ?? -1); -} - -export function customPaginationAtom( - gate: DerivedPropsGate<{ customPagination?: ReactNode }> -): ComputedAtom { - return computed(() => gate.props.customPagination); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index 2493dfe82c..733fd39f9e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -6,15 +6,23 @@ import { createClickActionHelper, createFocusController, createSelectionHelper, - createSetPageAction, - createSetPageSizeAction, - currentPageAtom, DatasourceService, layoutAtom, - pageSizeAtom, SelectActionsProvider, TaskProgressService } from "@mendix/widget-plugin-grid/main"; +import { + createSetPageAction, + createSetPageSizeAction, + currentPageAtom, + customPaginationAtom, + dynamicPageAtom, + dynamicPageSizeAtom, + DynamicPaginationFeature, + PageControlService, + pageSizeAtom, + PaginationViewModel +} from "@mendix/widget-plugin-grid/pagination/main"; import { SelectionCounterViewModel } from "@mendix/widget-plugin-grid/selection-counter/SelectionCounter.viewModel-atoms"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; @@ -22,11 +30,7 @@ import { Container, injected } from "brandi"; import { MainGateProps } from "../../../typings/MainGateProps"; import { WidgetRootViewModel } from "../../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../../features/empty-message/EmptyPlaceholder.viewModel"; -import { DynamicPaginationFeature } from "../../features/pagination/DynamicPagination.feature"; -import { PageControlService } from "../../features/pagination/PageControl.service"; import { paginationConfig } from "../../features/pagination/pagination.config"; -import { customPaginationAtom, dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; -import { PaginationViewModel } from "../../features/pagination/Pagination.viewModel"; import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; import { SelectAllModule } from "../../features/select-all/SelectAllModule.container"; @@ -39,8 +43,8 @@ import { rowClassProvider } from "../models/rows.model"; import { DatasourceParamsController } from "../services/DatasourceParamsController"; import { DerivedLoaderController } from "../services/DerivedLoaderController"; import { SelectionGate } from "../services/SelectionGate.service"; -import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; import { GridSizeStore } from "../stores/GridSize.store"; +import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; // base injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.filterHost); @@ -48,7 +52,7 @@ injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFil injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); injected(GridBasicData, CORE.mainGate); injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); -injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction); +injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction.optional); /** Pagination **/ injected(createSetPageAction, DG.query, DG.paginationConfig, DG.currentPage, DG.pageSize); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index cdcf6d5911..cfe55f1015 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -10,9 +10,15 @@ import { SelectAllService, SelectionDynamicProps, SelectionHelperService, - SetPageAction, TaskProgressService } from "@mendix/widget-plugin-grid/main"; +import { + DynamicPaginationFeature, + GridPageControl, + PageSizeStore, + PaginationViewModel, + SetPageAction +} from "@mendix/widget-plugin-grid/pagination/main"; import { SelectAllFeature } from "@mendix/widget-plugin-grid/select-all/select-all.feature"; import { BarStore, @@ -27,9 +33,6 @@ import { CSSProperties, ReactNode } from "react"; import { MainGateProps } from "../../typings/MainGateProps"; import { WidgetRootViewModel } from "../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../features/empty-message/EmptyPlaceholder.viewModel"; -import { DynamicPaginationFeature } from "../features/pagination/DynamicPagination.feature"; -import { GridPageControl } from "../features/pagination/GridPageControl"; -import { PaginationViewModel } from "../features/pagination/Pagination.viewModel"; import { PaginationConfig } from "../features/pagination/pagination.config"; import { CellEventsController } from "../features/row-interaction/CellEventsController"; import { CheckboxEventsController } from "../features/row-interaction/CheckboxEventsController"; @@ -45,7 +48,6 @@ import { RowClassProvider } from "./models/rows.model"; import { DatagridSetupService } from "./services/DatagridSetup.service"; import { DerivedLoaderController, DerivedLoaderControllerConfig } from "./services/DerivedLoaderController"; import { TextsService } from "./services/Texts.service"; -import { PageSizeStore } from "./stores/PageSize.store"; import { GridSizeStore } from "./stores/GridSize.store"; /** Tokens to resolve dependencies from the container. */ diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index e1994c0b37..ce1859a112 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -1,112 +1,19 @@ import { enableStaticRendering } from "mobx-react-lite"; enableStaticRendering(true); -import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; -import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; -import { getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection"; -import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; -import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; -import { GUID, ObjectItem } from "mendix"; -import { createElement, ReactElement, ReactNode, RefObject, useCallback, useMemo } from "react"; +import { createElement, ReactElement } from "react"; import { GalleryPreviewProps } from "../typings/GalleryProps"; -import { Gallery as GalleryComponent } from "./components/Gallery"; -import { useItemEventsController } from "./features/item-interaction/ItemEventsController"; -import { useGridPositionsPreview } from "./features/useGridPositionsPreview"; -import { useItemPreviewHelper } from "./helpers/ItemPreviewHelper"; -import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; import "./ui/GalleryPreview.scss"; -const SortAPI = getGlobalSortContext(); - function Preview(props: GalleryPreviewProps): ReactElement { - const { emptyPlaceholder } = props; - const { numberOfColumns, numberOfRows, containerRef, numberOfItems } = useGridPositionsPreview({ - phoneItems: props.phoneItems ?? 1, - tabletItems: props.tabletItems ?? 1, - desktopItems: props.desktopItems ?? 1, - totalItems: props.pageSize ?? 3 - }); - - const items: ObjectItem[] = Array.from({ length: numberOfItems }).map((_, index) => ({ - id: String(index) as GUID - })); - - const selectHelper = useItemSelectHelper(props.itemSelection, undefined); - - const getPositionCallback = useCallback( - (index: number) => getColumnAndRowBasedOnIndex(numberOfColumns, items.length, index), - [numberOfColumns, items.length] - ); - - const focusController = useFocusTargetController({ - rows: numberOfRows, - columns: numberOfColumns, - pageSize: props.pageSize ?? 0 - }); - - const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: "none" }); - - const itemEventsController = useItemEventsController( - selectHelper, - clickActionHelper, - focusController, - numberOfColumns, - props.itemSelectionMode - ); - - const sortAPI = useMemo( - () => - ({ - version: 1, - host: new SortStoreHost() - }) as const, - [] - ); - return ( -
}> - ReactElement) => ( - - {renderWrapper(null)} - - ), - [emptyPlaceholder] - )} - header={ - - -
- - - } - showHeader - hasMoreItems={false} - items={items} - itemHelper={useItemPreviewHelper({ - contentValue: props.content, - hasOnClick: props.onClick !== null - })} - numberOfItems={props.pageSize!} - page={0} - pageSize={props.pageSize!} - paging={props.pagination === "buttons"} - paginationPosition={props.pagingPosition} - paginationType={props.pagination} - showPagingButtons={props.showPagingButtons} - showEmptyStatePreview={props.showEmptyPlaceholder === "custom"} - phoneItems={props.phoneItems!} - tabletItems={props.tabletItems!} - selectHelper={selectHelper} - itemEventsController={itemEventsController} - focusController={focusController} - getPosition={getPositionCallback} - showRefreshIndicator={false} - preview - /> +
+ FIX ME: Preview for gallery +
+ +
+ +
); } diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index 18990717f6..1e98553069 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -1,13 +1,9 @@ import { ContainerProvider } from "brandi-react"; -import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; import { GalleryContainerProps } from "../typings/GalleryProps"; +import { GalleryWidget } from "./components/GalleryWidget"; import { useGalleryContainer } from "./model/hooks/useGalleryContainer"; -const GalleryWidget = observer(function GalleryWidget(): ReactElement { - return
; -}); - export function Gallery(props: GalleryContainerProps): ReactElement { const container = useGalleryContainer(props); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index f786af638b..96501e5432 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -95,6 +95,14 @@ Load more + + Custom pagination + + + + Custom pagination + + Show total count @@ -124,6 +132,27 @@ Laad meer + + Page size attribute + Attribute to set the page size dynamically. + + + + + + Page attribute + Attribute to set the page dynamically. + + + + + + Total count + Attribute to store current total count + + + + @@ -162,6 +191,48 @@ + + + Row count singular + Must include '%d' to denote number position + + %d row selected + %d rij geselecteerd + + + + Row count plural + Must include '%d' to denote number position + + %d rows selected + %d rijen geselecteerd + + + + Select all text + + + Select all rows in the data source + Selecteer alle rijen in de gegevensbron + + + + Select all template + This caption used when total count is available. + + Select all %d rows in the data source + Selecteer alle %d rijen in de gegevensbron + + + + Select status template + + + All %d rows selected. + Alle %d rijen geselecteerd. + + + @@ -212,22 +283,6 @@ Item description Assistive technology will read this upon reaching each gallery item. - - Item count singular - Must include '%d' to denote number position - - %d item selected - %d item geselecteerd - - - - Item count plural - Must include '%d' to denote number position - - %d items selected - %d items geselecteerd - - diff --git a/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx b/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx new file mode 100644 index 0000000000..e01fdcc597 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx @@ -0,0 +1,15 @@ +import { observer } from "mobx-react-lite"; +import { ReactNode } from "react"; +import { useEmptyPlaceholderVM } from "../model/hooks/injection-hooks"; + +export const EmptyPlaceholder = observer(function EmptyPlaceholder(): ReactNode { + const vm = useEmptyPlaceholderVM(); + + if (!vm.content) return null; + + return ( +
+
{vm.content}
+
+ ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx deleted file mode 100644 index 56d2b458b1..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator"; -import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination"; -import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; -import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; -import { ObjectItem } from "mendix"; -import { CSSProperties, ReactElement, ReactNode } from "react"; -import { GalleryItemHelper } from "../typings/GalleryItem"; -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 { PaginationEnum, SelectionCountPositionEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; -import { LoadMore, LoadMoreButton as LoadMorePreview } from "../components/LoadMore"; -import { ItemEventsController } from "../typings/ItemEventsController"; -import { SelectionCounter } from "./SelectionCounter"; - -export interface GalleryProps { - className?: string; - desktopItems: number; - emptyPlaceholderRenderer?: (renderWrapper: (children: ReactNode) => ReactElement) => ReactElement; - emptyMessageTitle?: string; - header?: ReactNode; - headerTitle?: string; - showHeader: boolean; - hasMoreItems: boolean; - items: T[]; - numberOfItems?: number; - paging: boolean; - page: number; - pageSize: number; - paginationPosition?: "top" | "bottom" | "both"; - paginationType: PaginationEnum; - showPagingButtons: ShowPagingButtonsEnum; - showEmptyStatePreview?: boolean; - phoneItems: number; - setPage?: (computePage: (prevPage: number) => number) => void; - style?: CSSProperties; - tabletItems: number; - tabIndex?: number; - ariaLabelListBox?: string; - ariaLabelItem?: (item: T) => string | undefined; - preview?: boolean; - selectionCountPosition?: SelectionCountPositionEnum; - - // Helpers - focusController: FocusTargetController; - itemEventsController: ItemEventsController; - itemHelper: GalleryItemHelper; - selectHelper: SelectActionHandler; - getPosition: (index: number) => PositionInGrid; - loadMoreButtonCaption?: string; - showRefreshIndicator: boolean; -} - -export function Gallery(props: GalleryProps): ReactElement { - const { loadMoreButtonCaption = "Load more" } = props; - const pagination = props.paging ? ( - props.setPage && props.setPage(() => page)} - nextPage={() => props.setPage && props.setPage(prev => prev + 1)} - numberOfItems={props.numberOfItems} - page={props.page} - pageSize={props.pageSize} - previousPage={() => props.setPage && props.setPage(prev => prev - 1)} - pagination={props.paginationType} - showPagingButtons={props.showPagingButtons} - /> - ) : null; - - const showTopPagination = - props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); - const showBottomPagination = - props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); - - const selectionCounter = - !props.preview && props.selectionCountPosition !== "off" && props.selectHelper.selectionType === "Multi" ? ( - - ) : null; - - const showTopSelectionCounter = selectionCounter && props.selectionCountPosition === "top"; - const showBottomSelectionCounter = selectionCounter && props.selectionCountPosition === "bottom"; - - const showLoadMore = props.paginationType === "loadMore"; - const showFooter = showBottomSelectionCounter || showBottomPagination || showLoadMore; - - return ( - - -
- {showTopSelectionCounter && selectionCounter} - {showTopPagination &&
{pagination}
} -
-
- {props.showHeader && {props.header}} - {props.showRefreshIndicator ? : null} - - {props.items.length > 0 && ( - - - {props.items.map((item, index) => ( - - ))} - - - )} - - {(props.items.length === 0 || props.showEmptyStatePreview) && - props.emptyPlaceholderRenderer && - props.emptyPlaceholderRenderer(children => ( -
-
{children}
-
- ))} - {showFooter && ( - -
- {showBottomSelectionCounter && ( -
{selectionCounter}
- )} - -
- {showBottomPagination && pagination} - {showLoadMore && - (props.preview ? ( - {loadMoreButtonCaption} - ) : ( - {loadMoreButtonCaption} - ))} -
-
-
- )} -
- ); -} diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx index 45b0da9952..fe7c5c838a 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx @@ -1,30 +1,21 @@ -import { InfiniteBodyProps, useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; +import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; import classNames from "classnames"; -import { ReactElement, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; +import { PropsWithChildren, ReactElement } from "react"; +import { usePaginationConfig, usePaginationVM } from "../model/hooks/injection-hooks"; -type PickProps = "hasMoreItems" | "setPage" | "isInfinite"; - -export type GalleryContentProps = { - className?: string; - children?: ReactNode; -} & Pick; - -export function GalleryContent({ - children, - className, - hasMoreItems, - isInfinite, - setPage -}: GalleryContentProps): ReactElement { +export const GalleryContent = observer(function GalleryContent({ children }: PropsWithChildren): ReactElement { + const paginationVM = usePaginationVM(); + const isInfinite = usePaginationConfig().isLimitBased; const [trackScrolling, bodySize, containerRef] = useInfiniteControl({ - hasMoreItems, + hasMoreItems: paginationVM.hasMoreItems, isInfinite, - setPage + setPage: paginationVM.setPage.bind(paginationVM) }); return (
0 ? { maxHeight: bodySize } : undefined} @@ -32,4 +23,4 @@ export function GalleryContent({ {children}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx index e9d9b88bfa..033b31f45e 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx @@ -1,12 +1,6 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { PropsWithChildren, ReactElement } from "react"; -type GalleryFooterProps = Omit; - -export function GalleryFooter({ children, className, ...rest }: GalleryFooterProps): ReactElement { - return ( -
- {children} -
- ); +export function GalleryFooter({ children, className }: PropsWithChildren<{ className?: string }>): ReactElement { + return
{children}
; } diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx new file mode 100644 index 0000000000..508a17780b --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx @@ -0,0 +1,35 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { observer } from "mobx-react-lite"; +import { ReactElement } from "react"; +import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; +import { usePaginationConfig } from "../model/hooks/injection-hooks"; +import { LoadMore } from "./LoadMore"; +import { Pagination } from "./Pagination"; + +export const GalleryFooterControls = observer(function GalleryFooterControls(): ReactElement { + const counterVM = useSelectionCounterViewModel(); + const pgConfig = usePaginationConfig(); + const loadMoreButtonCaption = "Load more"; + + return ( +
+
+ + + +
+
+ + {loadMoreButtonCaption} + +
+
+ + + +
+
+ ); +} +) \ No newline at end of file diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx index 81d7a5f95b..4e01b2fd99 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx @@ -1,12 +1,29 @@ +import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; +import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; +import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; +import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; -import { useMainGate } from "../model/hooks/injection-hooks"; +import { useFilterAPI, useMainGate, useSelectionHelper, useSortAPI } from "../model/hooks/injection-hooks"; -export function GalleryHeader(): ReactElement | null { +const SelectionContext = getGlobalSelectionContext(); +const SortAPI = getGlobalSortContext(); +const FilterAPI = getGlobalFilterContextObject(); + +export const GalleryHeader = observer(function GalleryHeader(): ReactElement | null { const { filtersPlaceholder } = useMainGate().props; + const filterAPI = useFilterAPI(); + const sortAPI = useSortAPI(); + const selectionContext = useCreateSelectionContextValue(useSelectionHelper()); if (!filtersPlaceholder) { return null; } - return
{filtersPlaceholder}
; -} + return + + +
{filtersPlaceholder}
+
+
+
+}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx new file mode 100644 index 0000000000..0e6aa5ed7c --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx @@ -0,0 +1,32 @@ +import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; +import { observer } from "mobx-react-lite"; +import { useGalleryConfig, useItems, useKeyNavFocus, useTextsService } from "../model/hooks/injection-hooks"; +import { ListBox } from "./ListBox"; +import { ListItem } from "./ListItem"; + +export const GalleryItems = observer(function GalleryItems() { + const items = useItems().get(); + const config = useGalleryConfig(); + const texts = useTextsService(); + const focusController = useKeyNavFocus(); + + if (items.length === 0) { + return
Empty
; + } + + return ( + + + {items.map((item, index) => ( + + ))} + + + ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx index 889c9e7050..f4946b70af 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx @@ -3,7 +3,6 @@ import { observer } from "mobx-react-lite"; import { PropsWithChildren, ReactElement } from "react"; import { useGalleryRootVM } from "../model/hooks/injection-hooks"; -/** @remark vm source `GalleryRoot.viewModel.ts` */ export const GalleryRoot = observer(function GalleryRoot(props: PropsWithChildren): ReactElement { const { children } = props; const vm = useGalleryRootVM(); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx new file mode 100644 index 0000000000..eb8bfbd85f --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx @@ -0,0 +1,26 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { ReactElement } from "react"; +import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; +import { usePaginationConfig } from "../model/hooks/injection-hooks"; +import { Pagination } from "./Pagination"; + +export function GalleryTobBarControls(): ReactElement { + const counterVM = useSelectionCounterViewModel(); + const pgConfig = usePaginationConfig(); + + return ( +
+
+ + + + +
+ +
+
+
+
+ ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx deleted file mode 100644 index e72dad268f..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryV2.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ReactElement } from "react"; -import { GalleryFooter } from "./GalleryFooter"; -import { GalleryHeader as Header } from "./GalleryHeader"; -import { GalleryRoot as Root } from "./GalleryRoot"; -import { GalleryTopBar as TopBar } from "./GalleryTopBar"; - -export function Gallery(): ReactElement { - return ( - - -
-
-
- -
- {showBottomSelectionCounter &&
{selectionCounter}
} - -
- {showBottomPagination && pagination} - {props.paginationType === "loadMore" && - (props.preview ? ( - {loadMoreButtonCaption} - ) : ( - {loadMoreButtonCaption} - ))} -
-
-
- - ); -} diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx new file mode 100644 index 0000000000..cf016313d5 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx @@ -0,0 +1,28 @@ +import { ReactElement } from "react"; +import { EmptyPlaceholder } from "./EmptyPlaceholder"; +import { GalleryContent as Content } from "./GalleryContent"; +import { GalleryFooter as Footer } from "./GalleryFooter"; +import { GalleryFooterControls as FooterControls } from "./GalleryFooterControls"; +import { GalleryHeader as Header } from "./GalleryHeader"; +import { GalleryItems as Items } from "./GalleryItems"; +import { GalleryRoot as Root } from "./GalleryRoot"; +import { GalleryTobBarControls as TopBarControls } from "./GalleryTobBarControls"; +import { GalleryTopBar as TopBar } from "./GalleryTopBar"; + +export function GalleryWidget(): ReactElement { + return ( + + + + +
+ + + + +
+ +
+ + ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx b/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx deleted file mode 100644 index bb6e871fa2..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; -import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; -import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; -import { ReactElement, ReactNode } from "react"; -import { useGalleryRootScope } from "../helpers/root-context"; - -const SelectionContext = getGlobalSelectionContext(); -const SortAPI = getGlobalSortContext(); -const FilterAPI = getGlobalFilterContextObject(); - -export function HeaderWidgetsHost(props: { children?: ReactNode }): ReactElement { - const { selectionHelper, rootStore } = useGalleryRootScope(); - const selectionContext = useCreateSelectionContextValue(selectionHelper); - - return ( - - - {props.children} - - - ); -} diff --git a/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx b/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx index 3335e65d8f..866a55d3db 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx @@ -1,12 +1,14 @@ import { SelectionType } from "@mendix/widget-plugin-grid/selection"; import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { ReactElement, ReactNode } from "react"; -type ListBoxProps = Omit & { +type ListBoxProps = { lg: number; md: number; sm: number; selectionType: SelectionType; + children?: ReactNode; + className?: string; }; export function ListBox({ children, className, selectionType, lg, md, sm, ...rest }: ListBoxProps): ReactElement { diff --git a/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx b/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx index d0b1644472..ddedc94ad2 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx @@ -1,31 +1,30 @@ +import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; import classNames from "classnames"; import { ObjectItem } from "mendix"; -import { JSX, ReactElement, RefObject, useMemo } from "react"; -import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; -import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; +import { observer } from "mobx-react-lite"; +import { ReactElement, RefObject, useMemo } from "react"; import { getAriaProps } from "../features/item-interaction/get-item-aria-props"; +import { useGalleryItemVM, useItemEventsVM, useLayoutService, useSelectActions } from "../model/hooks/injection-hooks"; +import { ListItemButton } from "./ListItemButton"; -import { GalleryItemHelper } from "../typings/GalleryItem"; -import { ItemEventsController } from "../typings/ItemEventsController"; - -type ListItemProps = Omit & { - eventsController: ItemEventsController; - getPosition: (index: number) => PositionInGrid; - helper: GalleryItemHelper; +type ListItemProps = { item: ObjectItem; itemIndex: number; - selectHelper: SelectActionHandler; - preview?: boolean; - label?: string; }; -export function ListItem(props: ListItemProps): ReactElement { - const { eventsController, getPosition, helper, item, itemIndex, selectHelper, label, ...rest } = props; - const clickable = helper.hasOnClick(item) || selectHelper.selectionType !== "None"; - const ariaProps = getAriaProps(item, selectHelper, label); +export const ListItem = observer(function ListItem(props: ListItemProps): ReactElement { + const { item, itemIndex, ...rest } = props; + + const eventsVM = useItemEventsVM().get(); + const selectActions = useSelectActions(); + const itemVM = useGalleryItemVM(); + const getPosition = useLayoutService().getPositionFn; + + const clickable = itemVM.hasOnClick(item) || selectActions.selectionType !== "None"; + const ariaProps = getAriaProps(item, selectActions, itemVM.label(item)); const { columnIndex, rowIndex } = getPosition(itemIndex); const keyNavProps = useFocusTargetProps({ columnIndex: columnIndex ?? -1, rowIndex }); - const handlers = useMemo(() => eventsController.getProps(item), [eventsController, item]); + const handlers = useMemo(() => eventsVM.getProps(item), [eventsVM, item]); return (
} tabIndex={keyNavProps.tabIndex} > - {helper.render(item)} + {itemVM.hasOnClick(item) === true ? ( + {itemVM.content(item)} + ) : ( + itemVM.content(item) + )}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx index fd3a2ca232..df9c4e9118 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx @@ -1,7 +1,7 @@ import cn from "classnames"; import { observer } from "mobx-react-lite"; import { JSX, ReactNode } from "react"; -import { useGalleryRootScope } from "../helpers/root-context"; +import { usePaginationVM } from "../model/hooks/injection-hooks"; export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): ReactNode { return ( @@ -11,10 +11,8 @@ export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): ReactNod ); } -export const LoadMore = observer(function LoadMore(props: { children: ReactNode }): ReactNode { - const { - rootStore: { paging } - } = useGalleryRootScope(); +export const LoadMore = observer(function LoadMore(): ReactNode { + const paging = usePaginationVM(); if (paging.pagination !== "loadMore") { return null; @@ -24,5 +22,9 @@ export const LoadMore = observer(function LoadMore(props: { children: ReactNode return null; } - return paging.setPage(n => n + 1)}>{props.children}; + return ( + paging.setPage(n => n + 1)}> + Fix me: Add load more caption from props + + ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx index 2fff223875..bdbac0fe0b 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx @@ -1,6 +1,25 @@ +import { Pagination as PaginationComponent } from "@mendix/widget-plugin-grid/components/Pagination"; import { observer } from "mobx-react-lite"; -import { ReactElement } from "react"; +import { ReactNode } from "react"; +import { usePaginationVM } from "../model/hooks/injection-hooks"; -export const Pagination = observer(function Pagination(): ReactElement { - return
Pagination
; +export const Pagination = observer(function Pagination(): ReactNode { + const paginationVM = usePaginationVM(); + + if (!paginationVM.paginationVisible) return null; + + return ( + 0} + gotoPage={page => paginationVM.setPage(page)} + nextPage={() => paginationVM.setPage(n => n + 1)} + numberOfItems={paginationVM.totalCount} + page={paginationVM.currentPage} + pageSize={paginationVM.pageSize} + showPagingButtons={paginationVM.showPagingButtons} + previousPage={() => paginationVM.setPage(n => n - 1)} + pagination={paginationVM.pagination} + /> + ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx b/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx deleted file mode 100644 index 12d09ef397..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { If } from "@mendix/widget-plugin-component-kit/If"; -import { observer } from "mobx-react-lite"; -import { useGalleryRootScope } from "../helpers/root-context"; - -type SelectionCounterLocation = "top" | "bottom" | undefined; - -export const SelectionCounter = observer(function SelectionCounter({ - location -}: { - location?: SelectionCounterLocation; -}) { - const { selectionCountStore, itemSelectHelper } = useGalleryRootScope(); - - const containerClass = location === "top" ? "widget-gallery-tb-start" : "widget-gallery-pb-start"; - - const clearButtonAriaLabel = `${selectionCountStore.clearButtonLabel} (${selectionCountStore.selectedCount} selected)`; - - return ( - -
- - {selectionCountStore.selectedCountText} - -  |  - -
-
- ); -}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx deleted file mode 100644 index 3551501fef..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { listAction, listExpression, setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils"; -import "@testing-library/jest-dom"; -import { render, waitFor } from "@testing-library/react"; -import { ObjectItem } from "mendix"; -import { createElement } from "react"; -import { ItemHelperBuilder } from "../../utils/builders/ItemHelperBuilder"; -import { mockItemHelperWithAction, mockProps, setup, withGalleryContext } from "../../utils/test-utils"; -import { Gallery } from "../Gallery"; - -jest.mock("@mendix/widget-plugin-component-kit/RefreshIndicator", () => ({ - RefreshIndicator: (_props: any) => createElement("div", { "data-testid": "refresh-indicator" }) -})); - -describe("Gallery", () => { - beforeAll(() => { - setupIntersectionObserverStub(); - }); - describe("DOM Structure", () => { - it("renders correctly", () => { - const { asFragment } = render(withGalleryContext()); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders correctly with onclick event", () => { - const { asFragment } = render( - withGalleryContext() - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders RefreshIndicator when `showRefreshIndicator` is true", () => { - const base = mockProps(); - const props = { ...base, showRefreshIndicator: true }; - const { getByTestId } = render(withGalleryContext()); - expect(getByTestId("refresh-indicator")).toBeInTheDocument(); - }); - - it("does not render RefreshIndicator when `showRefreshIndicator` is false", () => { - const base = mockProps(); - const props = { ...base, showRefreshIndicator: false }; - const { queryByTestId } = render(withGalleryContext()); - expect(queryByTestId("refresh-indicator")).toBeNull(); - }); - }); - - describe("with on click action", () => { - it("runs action on item click", async () => { - const execute = jest.fn(); - const props = mockProps({ onClick: listAction(mock => ({ ...mock(), execute })) }); - const { user, getAllByRole } = setup(withGalleryContext()); - const [item] = getAllByRole("listitem"); - - await user.click(item); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(1)); - }); - - it("runs action on Enter|Space press when item is in focus", async () => { - const execute = jest.fn(); - const props = mockProps({ onClick: listAction(mock => ({ ...mock(), execute })) }); - const { user, getAllByRole } = setup(withGalleryContext(} />)); - const [item] = getAllByRole("listitem"); - - await user.tab(); - expect(item).toHaveFocus(); - await user.keyboard("[Enter]"); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(1)); - await user.keyboard("[Space]"); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(2)); - }); - }); - - describe("with different configurations per platform", () => { - it("contains correct classes for desktop", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-lg-12"); - }); - - it("contains correct classes for tablet", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-md-6"); - }); - - it("contains correct classes for phone", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-sm-3"); - }); - }); - - describe("with custom classes", () => { - it("contains correct classes in the wrapper", () => { - const { container } = render(withGalleryContext()); - - expect(container.querySelector(".custom-class")).toBeVisible(); - }); - - it("contains correct classes in the items", () => { - const { getAllByRole } = render( - withGalleryContext( - - b.withItemClass(listExpression(() => "custom-class")) - )} - /> - ) - ); - const [item] = getAllByRole("listitem"); - - expect(item).toHaveClass("custom-class"); - }); - }); - - describe("with pagination", () => { - it("renders correctly", () => { - const { asFragment } = render( - withGalleryContext( - - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("triggers correct events on click next button", async () => { - const setPage = jest.fn(); - const { user, getByLabelText } = setup( - withGalleryContext( - - ) - ); - - const next = getByLabelText("Go to next page"); - await user.click(next); - await waitFor(() => expect(setPage).toHaveBeenCalledTimes(1)); - }); - }); - - describe("with empty option", () => { - it("renders correctly", () => { - const { asFragment } = render( - withGalleryContext( - renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); - - describe("with accessibility properties", () => { - it("renders correctly without items", () => { - const { asFragment } = render( - withGalleryContext( - renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders correctly with items", () => { - const { asFragment } = render( - withGalleryContext( - `title for '${item.id}'`} - headerTitle="filter title" - emptyMessageTitle="empty message" - emptyPlaceholderRenderer={renderWrapper => renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); - - describe("without filters", () => { - it("renders structure without header container", () => { - const props = { ...mockProps(), showHeader: false, header: undefined }; - const { asFragment } = render(withGalleryContext()); - - expect(asFragment()).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx new file mode 100644 index 0000000000..ac6d6b3f0f --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx @@ -0,0 +1,30 @@ +import { render, RenderOptions } from "@testing-library/react"; +import { Container } from "brandi"; +import { ContainerProvider } from "brandi-react"; +import { ReactElement } from "react"; +import { createGalleryContainer } from "../../model/containers/createGalleryContainer"; +import { mockContainerProps } from "../../utils/mock-container-props"; +import { GalleryRoot } from "../GalleryRoot"; + +/** Function to bind hook context to provided container. */ +const renderWithContainer = ( + ui: ReactElement, + ct: Container, + options?: RenderOptions +): ReturnType => { + return render( + + {ui} + , + options + ); +}; + +describe("GalleryRoot", () => { + it("should render with correct className, style and tabIndex", () => { + const props = mockContainerProps(); + const [container] = createGalleryContainer({ ...props, tabIndex: 42, style: { color: "blue" } }); + const { asFragment } = renderWithContainer(, container); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap b/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap deleted file mode 100644 index 3c0d5c3e18..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap +++ /dev/null @@ -1,492 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Gallery DOM Structure renders correctly 1`] = ` - -