Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
import { ContainerProvider } from "brandi-react";
import { observer } from "mobx-react-lite";
import { ReactElement, ReactNode, useCallback, useMemo } from "react";
import { ReactElement, useCallback, useMemo } from "react";
import { DatagridContainerProps } from "../typings/DatagridProps";
import { Cell } from "./components/Cell";
import { Widget } from "./components/Widget";
Expand Down Expand Up @@ -84,11 +84,6 @@ const DatagridRoot = observer((props: DatagridContainerProps): ReactElement => {
columnsResizable={props.columnsResizable}
columnsSortable={props.columnsSortable}
data={items}
emptyPlaceholderRenderer={useCallback(
(renderWrapper: (children: ReactNode) => ReactElement) =>
props.showEmptyPlaceholder === "custom" ? renderWrapper(props.emptyPlaceholder) : <div />,
[props.emptyPlaceholder, props.showEmptyPlaceholder]
)}
filterRenderer={useCallback(
(renderWrapper, columnIndex) => {
const columnFilter = columnsStore.columnFilters[columnIndex];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator";
import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination";
import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController";
import classNames from "classnames";
import { ListActionValue, ObjectItem } from "mendix";
import { observer } from "mobx-react-lite";
import { CSSProperties, Fragment, ReactElement, ReactNode } from "react";
Expand All @@ -12,6 +11,7 @@ import {
ShowPagingButtonsEnum
} from "../../typings/DatagridProps";

import { EmptyPlaceholder } from "../features/empty-message/EmptyPlaceholder";
import { SelectAllBar } from "../features/select-all/SelectAllBar";
import { SelectionProgressDialog } from "../features/select-all/SelectionProgressDialog";
import { SelectActionHelper } from "../helpers/SelectActionHelper";
Expand All @@ -38,7 +38,6 @@ export interface WidgetProps<C extends GridColumn, T extends ObjectItem = Object
columnsResizable: boolean;
columnsSortable: boolean;
data: T[];
emptyPlaceholderRenderer?: (renderWrapper: (children: ReactNode) => ReactElement) => ReactElement;
exporting: boolean;
filterRenderer: (renderWrapper: (children: ReactNode) => ReactElement, columnIndex: number) => ReactElement;
hasMoreItems: boolean;
Expand Down Expand Up @@ -117,7 +116,6 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
CellComponent,
columnsHidable,
data: rows,
emptyPlaceholderRenderer,
hasMoreItems,
headerContent,
headerTitle,
Expand All @@ -128,7 +126,6 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
paginationType,
paging,
pagingPosition,
preview,
showRefreshIndicator,
selectActionHelper,
setPage,
Expand Down Expand Up @@ -216,23 +213,7 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
eventsController={props.cellEventsController}
pageSize={props.pageSize}
/>
{(rows.length === 0 || preview) &&
emptyPlaceholderRenderer &&
emptyPlaceholderRenderer(children => (
<div
key="row-footer"
className={classNames("td", { "td-borders": !preview })}
style={{
gridColumn: `span ${
visibleColumns.length +
(columnsHidable ? 1 : 0) +
(selectActionHelper.showCheckboxColumn ? 1 : 0)
}`
}}
>
<div className="empty-placeholder">{children}</div>
</div>
))}
<EmptyPlaceholder />
</GridBody>
</Grid>
</WidgetContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ describe.skip("Table", () => {

it("renders the structure correctly with empty placeholder", () => {
const component = renderWithRootContext({
...mockWidgetProps(),
emptyPlaceholderRenderer: renderWrapper => renderWrapper(<div />)
...mockWidgetProps()
});

expect(component.asFragment()).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import classNames from "classnames";
import { observer } from "mobx-react-lite";
import { ReactNode } from "react";
import { useEmptyPlaceholderVM } from "./injection-hooks";

export const EmptyPlaceholder = observer(function EmptyPlaceholder(): ReactNode {
const vm = useEmptyPlaceholderVM();

if (!vm.content) return null;

return (
<div className={classNames("td", "td-borders")} style={vm.style}>
<div className="empty-placeholder">{vm.content}</div>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ComputedAtom } from "@mendix/widget-plugin-mobx-kit/main";
import { makeAutoObservable } from "mobx";
import { CSSProperties, ReactNode } from "react";

export class EmptyPlaceholderViewModel {
constructor(
private widgets: ComputedAtom<ReactNode>,
private visibleColumnsCount: ComputedAtom<number>,
private config: { checkboxColumnEnabled: boolean; selectorColumnEnabled: boolean }
) {
makeAutoObservable(this);
}

get content(): ReactNode {
return this.widgets.get();
}

get span(): number {
let span = this.visibleColumnsCount.get();
if (this.config.checkboxColumnEnabled) {
span += 1;
}
if (this.config.selectorColumnEnabled) {
span += 1;
}
return Math.max(span, 1);
}

get style(): CSSProperties {
return { gridColumn: `span ${this.span}` };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { computed, observable } from "mobx";
import { ReactNode } from "react";
import { EmptyPlaceholderViewModel } from "../EmptyPlaceholder.viewModel";

describe("EmptyPlaceholderViewModel", () => {
describe("style getter", () => {
it("reacts to changes in visible columns count", () => {
const mockWidgets = computed(() => "Empty message" as ReactNode);
const columnCount = observable.box(3);
const config = { checkboxColumnEnabled: false, selectorColumnEnabled: false };

const viewModel = new EmptyPlaceholderViewModel(mockWidgets, columnCount, config);

expect(viewModel.style).toEqual({ gridColumn: "span 3" });

columnCount.set(5);
expect(viewModel.style).toEqual({ gridColumn: "span 5" });

columnCount.set(0);
expect(viewModel.style).toEqual({ gridColumn: "span 1" });
});

it("reacts to changes in visible columns count with config flags enabled", () => {
const mockWidgets = computed(() => "Empty message" as ReactNode);
const columnCount = observable.box(3);
const config = { checkboxColumnEnabled: true, selectorColumnEnabled: true };

const viewModel = new EmptyPlaceholderViewModel(mockWidgets, columnCount, config);

expect(viewModel.style).toEqual({ gridColumn: "span 5" });

columnCount.set(5);
expect(viewModel.style).toEqual({ gridColumn: "span 7" });

columnCount.set(0);
expect(viewModel.style).toEqual({ gridColumn: "span 2" });
});
});

describe("content getter", () => {
it("returns widgets from atom", () => {
const message = "Empty message";
const atom = computed(() => message);
const columnCount = observable.box(3);
const config = { checkboxColumnEnabled: false, selectorColumnEnabled: false };

const viewModel = new EmptyPlaceholderViewModel(atom, columnCount, config);

expect(viewModel.content).toBe(message);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createInjectionHooks } from "brandi-react";
import { DG_TOKENS as DG } from "../../model/tokens";

export const [useEmptyPlaceholderVM] = createInjectionHooks(DG.emptyPlaceholderVM);
Original file line number Diff line number Diff line change
@@ -1,160 +1,54 @@
import { DerivedPropsGate, SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main";
import { DynamicValue, ListValue, SelectionMultiValue, SelectionSingleValue } from "mendix";
import { action, makeAutoObservable, reaction } from "mobx";

type DynamicProps = {
datasource: ListValue;
selectAllTemplate?: DynamicValue<string>;
selectAllText?: DynamicValue<string>;
itemSelection?: SelectionSingleValue | SelectionMultiValue;
allSelectedText?: DynamicValue<string>;
};

interface SelectService {
selectAllPages(): Promise<{ success: boolean }> | { success: boolean };
clearSelection(): void;
}

interface CounterService {
selectedCount: number;
selectedCountText: string;
clearButtonLabel: string;
}
import { SelectAllEvents } from "@mendix/widget-plugin-grid/select-all/select-all.model";
import { Emitter } from "@mendix/widget-plugin-mobx-kit/main";
import { makeAutoObservable } from "mobx";

/** @injectable */
export class SelectAllBarViewModel implements SetupComponent {
private barVisible = false;
private clearVisible = false;

pending = false;

export class SelectAllBarViewModel {
constructor(
host: SetupComponentHost,
private readonly gate: DerivedPropsGate<DynamicProps>,
private readonly selectService: SelectService,
private readonly count: CounterService,
private readonly enableSelectAll: boolean
private emitter: Emitter<SelectAllEvents>,
private state: { pending: boolean; visible: boolean; clearBtnVisible: boolean },
private selectionTexts: {
clearSelectionButtonLabel: string;
selectedCountText: string;
},
private selectAllTexts: {
selectAllLabel: string;
selectionStatus: string;
},
private enableSelectAll: boolean
) {
host.add(this);
type PrivateMembers = "setClearVisible" | "setPending" | "hideBar" | "showBar";
makeAutoObservable<this, PrivateMembers>(this, {
setClearVisible: action,
setPending: action,
hideBar: action,
showBar: action
});
}

private get props(): DynamicProps {
return this.gate.props;
}

private setClearVisible(value: boolean): void {
this.clearVisible = value;
}

private setPending(value: boolean): void {
this.pending = value;
}

private hideBar(): void {
this.barVisible = false;
this.clearVisible = false;
}

private showBar(): void {
this.barVisible = true;
}

private get total(): number {
return this.props.datasource.totalCount ?? 0;
}

private get selectAllFormat(): string {
return this.props.selectAllTemplate?.value ?? "Select all %d rows in the data source";
}

private get selectAllText(): string {
return this.props.selectAllText?.value ?? "Select all rows in the data source";
}

private get allSelectedText(): string {
const str = this.props.allSelectedText?.value ?? "All %d rows selected.";
return str.replace("%d", `${this.count.selectedCount}`);
}

private get isCurrentPageSelected(): boolean {
const selection = this.props.itemSelection;

if (!selection || selection.type === "Single") return false;

const pageIds = new Set(this.props.datasource.items?.map(item => item.id) ?? []);
const selectionSubArray = selection.selection.filter(item => pageIds.has(item.id));
return selectionSubArray.length === pageIds.size && pageIds.size > 0;
}

private get isAllItemsSelected(): boolean {
if (this.total > 0) return this.total === this.count.selectedCount;

const { offset, limit, items = [], hasMoreItems } = this.gate.props.datasource;
const noMoreItems = typeof hasMoreItems === "boolean" && hasMoreItems === false;
const fullyLoaded = offset === 0 && limit >= items.length;

return fullyLoaded && noMoreItems && items.length === this.count.selectedCount;
makeAutoObservable(this);
}

get selectAllLabel(): string {
if (this.total > 0) return this.selectAllFormat.replace("%d", `${this.total}`);
return this.selectAllText;
return this.selectAllTexts.selectAllLabel;
}

get clearSelectionLabel(): string {
return this.count.clearButtonLabel;
return this.selectionTexts.clearSelectionButtonLabel;
}

get selectionStatus(): string {
if (this.isAllItemsSelected) return this.allSelectedText;
return this.count.selectedCountText;
return this.selectAllTexts.selectionStatus;
}

get isBarVisible(): boolean {
return this.enableSelectAll && this.barVisible;
return this.enableSelectAll && this.state.visible;
}

get isClearVisible(): boolean {
return this.clearVisible;
return this.state.clearBtnVisible;
}

get isSelectAllDisabled(): boolean {
return this.pending;
}

setup(): (() => void) | void {
if (!this.enableSelectAll) return;

return reaction(
() => this.isCurrentPageSelected,
isCurrentPageSelected => {
if (isCurrentPageSelected === false) {
this.hideBar();
} else if (this.isAllItemsSelected === false) {
this.showBar();
}
}
);
return this.state.pending;
}

onClear(): void {
this.selectService.clearSelection();
this.emitter.emit("clear");
}

async onSelectAll(): Promise<void> {
this.setPending(true);
try {
const { success } = await this.selectService.selectAllPages();
this.setClearVisible(success);
} finally {
this.setPending(false);
}
onSelectAll(): void {
this.emitter.emit("startSelecting");
}
}

This file was deleted.

Loading
Loading