diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 9159447bd4..e311d2e0d9 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -429,86 +429,6 @@ $root: ".widget-datagrid"; display: contents; } - &-refresh-container { - grid-column: 1 / -1; - padding: 0; - position: relative; - } - - &-refresh-indicator { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: var(--border-color-default, #ced0d3); - border: none; - border-radius: 2px; - color: var(--brand-primary, $dg-brand-primary); - height: 4px; - width: 100%; - position: absolute; - left: 0; - right: 0; - - &::-webkit-progress-bar { - background-color: transparent; - } - - &::-webkit-progress-value { - background-color: currentColor; - transition: all 0.2s; - } - - &::-moz-progress-bar { - background-color: currentColor; - transition: all 0.2s; - } - - &::-ms-fill { - border: none; - background-color: currentColor; - transition: all 0.2s; - } - - &:indeterminate { - background-size: 200% 100%; - background-image: linear-gradient( - to right, - transparent 50%, - currentColor 50%, - currentColor 60%, - transparent 60%, - transparent 71.5%, - currentColor 71.5%, - currentColor 84%, - transparent 84% - ); - animation: progress-linear 3s infinite linear; - } - - &:indeterminate::-moz-progress-bar { - background-color: transparent; - } - - &:indeterminate::-ms-fill { - animation-name: none; - } - - @keyframes progress-linear { - 0% { - background-size: 200% 100%; - background-position: left -31.25% top 0%; - } - 50% { - background-size: 800% 100%; - background-position: left -49% top 0%; - } - 100% { - background-size: 400% 100%; - background-position: left -102% top 0%; - } - } - } - &.widget-datagrid-selection-method-click { .tr.tr-selected .td { background-color: $dg-grid-selected-row-background; diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss new file mode 100644 index 0000000000..8213ceab85 --- /dev/null +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss @@ -0,0 +1,83 @@ +.mx-refresh-container { + grid-column: 1 / -1; + padding: 0; + position: relative; + + &-padding { + padding: var(--spacing-small) 0; + } +} + +.mx-refresh-indicator { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--border-color-default, #ced0d3); + border: none; + border-radius: 2px; + color: var(--brand-primary, $dg-brand-primary); + height: 4px; + width: 100%; + position: absolute; + left: 0; + right: 0; + + &::-webkit-progress-bar { + background-color: transparent; + } + + &::-webkit-progress-value { + background-color: currentColor; + transition: all 0.2s; + } + + &::-moz-progress-bar { + background-color: currentColor; + transition: all 0.2s; + } + + &::-ms-fill { + border: none; + background-color: currentColor; + transition: all 0.2s; + } + + &:indeterminate { + background-size: 200% 100%; + background-image: linear-gradient( + to right, + transparent 50%, + currentColor 50%, + currentColor 60%, + transparent 60%, + transparent 71.5%, + currentColor 71.5%, + currentColor 84%, + transparent 84% + ); + animation: progress-linear 3s infinite linear; + } + + &:indeterminate::-moz-progress-bar { + background-color: transparent; + } + + &:indeterminate::-ms-fill { + animation-name: none; + } + + @keyframes progress-linear { + 0% { + background-size: 200% 100%; + background-position: left -31.25% top 0%; + } + 50% { + background-size: 800% 100%; + background-position: left -49% top 0%; + } + 100% { + background-size: 400% 100%; + background-position: left -102% top 0%; + } + } +} diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss index 19fbcb8846..c1d5304319 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss @@ -6,6 +6,7 @@ @import "drop-down-sort"; @import "gallery"; @import "gallery-design-properties"; +@import "refresh-indicator"; @import "three-state-checkbox"; @import "tree-node"; @import "tree-node-design-properties"; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx deleted file mode 100644 index 79f3f3a10f..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createElement, ReactElement } from "react"; - -export function RefreshIndicator(): ReactElement { - return ( -
-
- -
-
- ); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index f169d66fa8..f94ad45f91 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -1,3 +1,4 @@ +import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator"; import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination"; import { SelectionStatus } from "@mendix/widget-plugin-grid/selection"; import classNames from "classnames"; @@ -25,7 +26,6 @@ import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navig import { observer } from "mobx-react-lite"; import { RowsRenderer } from "./RowsRenderer"; import { GridHeader } from "./GridHeader"; -import { RefreshIndicator } from "./RefreshIndicator"; export interface WidgetProps { CellComponent: CellComponent; diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md index 07d419d898..04c091a039 100644 --- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We implemented a new property to show a refresh indicator. With the refresh indicator, any datasource change shows a progress bar on top of Gallery. + ## [3.2.0] - 2025-08-18 ### Changed diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json index 70c2de87a9..97cfa5a144 100644 --- a/packages/pluggableWidgets/gallery-web/package.json +++ b/packages/pluggableWidgets/gallery-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/gallery-web", "widgetName": "Gallery", - "version": "3.2.0", + "version": "3.3.0", "description": "A flexible gallery widget that renders columns, rows and layouts.", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index e6471a9e03..13ca8e18cf 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -104,6 +104,7 @@ function Preview(props: GalleryPreviewProps): ReactElement { itemEventsController={itemEventsController} focusController={focusController} getPosition={getPositionCallback} + showRefreshIndicator={false} preview /> ); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index 37e665ed95..d769ffbc1e 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -91,6 +91,7 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp focusController={focusController} getPosition={getPositionCallback} loadMoreButtonCaption={props.loadMoreButtonCaption?.value} + showRefreshIndicator={rootStore.loaderCtrl.showRefreshIndicator} /> ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index c613422fa9..5853598503 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -37,6 +37,10 @@ Content placeholder + + Show refresh indicator + Show a refresh indicator when the data is being loaded. + diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index 485e00f053..9407dafaf8 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -1,3 +1,4 @@ +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"; @@ -51,6 +52,7 @@ export interface GalleryProps { selectHelper: SelectActionHandler; getPosition: (index: number) => PositionInGrid; loadMoreButtonCaption?: string; + showRefreshIndicator: boolean; } export function Gallery(props: GalleryProps): ReactElement { @@ -86,6 +88,7 @@ export function Gallery(props: GalleryProps): ReactElem > {showTopPagination && pagination} {props.showHeader && {props.header}} + {props.showRefreshIndicator ? : null} - + diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts index 59417e1e73..e0ab4c84f0 100644 --- a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts +++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts @@ -11,6 +11,7 @@ import { SortAPI } from "@mendix/widget-plugin-sorting/react/context"; import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; import { EditableValue, ListValue } from "mendix"; import { PaginationEnum, StateStorageTypeEnum } from "../../typings/GalleryProps"; +import { DerivedLoaderController } from "../controllers/DerivedLoaderController"; import { QueryParamsController } from "../controllers/QueryParamsController"; import { ObservableStorage } from "../typings/storage"; import { AttributeStorage } from "./AttributeStorage"; @@ -31,6 +32,7 @@ interface StaticProps { stateStorageType: StateStorageTypeEnum; storeFilters: boolean; storeSort: boolean; + refreshIndicator: boolean; } export type GalleryPropsGate = DerivedPropsGate; @@ -50,6 +52,7 @@ export class GalleryStore extends BaseControllerHost { readonly paging: PaginationController; readonly filterAPI: FilterAPI; readonly sortAPI: SortAPI; + loaderCtrl: DerivedLoaderController; constructor(spec: GalleryStoreSpec) { super(); @@ -86,6 +89,8 @@ export class GalleryStore extends BaseControllerHost { host: this._sortHost }; + this.loaderCtrl = new DerivedLoaderController(this._query, spec.refreshIndicator); + new RefreshController(this, { delay: 0, query: this._query.derivedQuery diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 3d53682d85..5dc93b61b2 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -55,7 +55,8 @@ export function createMockGalleryContext(): GalleryRootScope { onClickTrigger: "single", stateStorageType: "localStorage", storeFilters: false, - storeSort: false + storeSort: false, + refreshIndicator: false }; // Create a proper gate provider and gate @@ -72,7 +73,8 @@ export function createMockGalleryContext(): GalleryRootScope { pageSize: 10, stateStorageType: "localStorage", storeFilters: false, - storeSort: false + storeSort: false, + refreshIndicator: false }); const mockSelectHelper = new SelectActionHandler("None", undefined); @@ -148,6 +150,7 @@ export function mockProps(params: Helpers & Mocks = {}): GalleryProps, itemEventsController, focusController, - getPosition: (index: number) => getColumnAndRowBasedOnIndex(3, 3, index) + getPosition: (index: number) => getColumnAndRowBasedOnIndex(3, 3, index), + showRefreshIndicator: false }; } diff --git a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts index a1cd505ccb..2f32acbdc7 100644 --- a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts +++ b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts @@ -30,6 +30,7 @@ export interface GalleryContainerProps { itemSelection?: SelectionSingleValue | SelectionMultiValue; itemSelectionMode: ItemSelectionModeEnum; content?: ListWidgetValue; + refreshIndicator: boolean; desktopItems: number; tabletItems: number; phoneItems: number; @@ -71,6 +72,7 @@ export interface GalleryPreviewProps { itemSelection: "None" | "Single" | "Multi"; itemSelectionMode: ItemSelectionModeEnum; content: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; + refreshIndicator: boolean; desktopItems: number | null; tabletItems: number | null; phoneItems: number | null; diff --git a/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx b/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx new file mode 100644 index 0000000000..a1f4789689 --- /dev/null +++ b/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx @@ -0,0 +1,16 @@ +import classNames from "classnames"; +import { createElement, ReactElement } from "react"; + +type RefreshIndicatorProps = { + className?: string; +}; + +export function RefreshIndicator({ className }: RefreshIndicatorProps): ReactElement { + return ( +
+
+ +
+
+ ); +}