Skip to content

Commit 9a0f933

Browse files
committed
refactor: extract logic to grid plugin
1 parent bcb9794 commit 9a0f933

18 files changed

Lines changed: 881 additions & 2 deletions

packages/shared/widget-plugin-grid/src/core/Datasource.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,12 @@ export class DatasourceService implements SetupComponent, QueryService {
124124
// Subscribe to items to reschedule timer on items change
125125
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
126126
this.items;
127-
clearInterval(timerId);
127+
clearTimeout(timerId);
128128
timerId = window.setTimeout(() => this.backgroundRefresh(), this.refreshIntervalMs);
129129
});
130130
add(() => {
131131
clearAutorun();
132-
clearInterval(timerId);
132+
clearTimeout(timerId);
133133
});
134134
}
135135

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { DerivedGate, GateProvider } from "@mendix/widget-plugin-mobx-kit/main";
2+
import { autorun, computed, observable } from "mobx";
3+
import { ReactNode } from "react";
4+
import "../../utils/mobx-test-setup.js";
5+
import { emptyStateWidgetsAtom } from "../models/empty-state.model.js";
6+
7+
describe("emptyStateWidgetsAtom", () => {
8+
it("returns null when emptyPlaceholder is undefined", () => {
9+
const gate = new DerivedGate({ props: { emptyPlaceholder: undefined } });
10+
const itemsCount = computed(() => 0);
11+
const atom = emptyStateWidgetsAtom(gate, itemsCount);
12+
13+
expect(atom.get()).toBe(null);
14+
});
15+
16+
it("returns null when items count is greater than 0", () => {
17+
const gate = new DerivedGate({ props: { emptyPlaceholder: "Empty state message" } });
18+
const itemsCount = computed(() => 5);
19+
const atom = emptyStateWidgetsAtom(gate, itemsCount);
20+
21+
expect(atom.get()).toBe(null);
22+
});
23+
24+
it("returns null when items count is -1 (loading state)", () => {
25+
const gate = new DerivedGate({ props: { emptyPlaceholder: "Empty state message" } });
26+
const itemsCount = computed(() => -1);
27+
const atom = emptyStateWidgetsAtom(gate, itemsCount);
28+
29+
expect(atom.get()).toBe(null);
30+
});
31+
32+
it("returns emptyPlaceholder when both emptyPlaceholder is defined and itemsCount is exactly 0", () => {
33+
const message = "Empty state message";
34+
const gate = new DerivedGate({ props: { emptyPlaceholder: message } });
35+
const itemsCount = computed(() => 0);
36+
const atom = emptyStateWidgetsAtom(gate, itemsCount);
37+
38+
expect(atom.get()).toBe(message);
39+
});
40+
41+
describe("reactive behavior", () => {
42+
it("reacts to changes in both emptyPlaceholder and itemsCount", () => {
43+
const gateProvider = new GateProvider({
44+
emptyPlaceholder: undefined as ReactNode
45+
});
46+
const itemCountBox = observable.box(5);
47+
const atom = emptyStateWidgetsAtom(gateProvider.gate, itemCountBox);
48+
const values: ReactNode[] = [];
49+
50+
const dispose = autorun(() => values.push(atom.get()));
51+
52+
// Initial state: no placeholder, items > 0 → null
53+
expect(values.at(-1)).toBe(null);
54+
55+
// Add placeholder but items count > 0 → still null
56+
gateProvider.setProps({ emptyPlaceholder: "Empty message" });
57+
expect(values.at(-1)).toBe(null);
58+
59+
// Set items count to 0 → should show placeholder
60+
itemCountBox.set(0);
61+
expect(values.at(-1)).toBe("Empty message");
62+
63+
// Remove placeholder while count is 0 → null
64+
gateProvider.setProps({ emptyPlaceholder: undefined });
65+
expect(values.at(-1)).toBe(null);
66+
67+
// Add different placeholder back with count still 0 → show new placeholder
68+
gateProvider.setProps({ emptyPlaceholder: "No data available" });
69+
expect(values.at(-1)).toBe("No data available");
70+
71+
// Increase count while placeholder exists → null
72+
itemCountBox.set(3);
73+
expect(values.at(-1)).toBe(null);
74+
75+
expect(values).toEqual([null, "Empty message", null, "No data available", null]);
76+
77+
dispose();
78+
});
79+
});
80+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { GateProvider } from "@mendix/widget-plugin-mobx-kit/main";
2+
import { autorun } from "mobx";
3+
import { hasMoreItemsAtom } from "../models/datasource.model.js";
4+
5+
describe("hasMoreItemsAtom", () => {
6+
it("reacts to datasource hasMoreItems changes", () => {
7+
const gateProvider = new GateProvider<{ datasource: { hasMoreItems?: boolean } }>({
8+
datasource: { hasMoreItems: undefined }
9+
});
10+
const atom = hasMoreItemsAtom(gateProvider.gate);
11+
const values: Array<boolean | undefined> = [];
12+
13+
autorun(() => values.push(atom.get()));
14+
15+
expect(values.at(0)).toBe(undefined);
16+
17+
gateProvider.setProps({ datasource: { hasMoreItems: true } });
18+
gateProvider.setProps({ datasource: { hasMoreItems: false } });
19+
gateProvider.setProps({ datasource: { hasMoreItems: true } });
20+
gateProvider.setProps({ datasource: { hasMoreItems: undefined } });
21+
gateProvider.setProps({ datasource: { hasMoreItems: false } });
22+
23+
expect(values).toEqual([undefined, true, false, true, undefined, false]);
24+
});
25+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { autorun, computed, observable } from "mobx";
2+
import { isAllItemsPresent, isAllItemsPresentAtom } from "../models/datasource.model.js";
3+
4+
import "../../utils/mobx-test-setup.js";
5+
6+
describe("isAllItemsPresent", () => {
7+
it("returns true when offset is 0 and hasMoreItems is false", () => {
8+
expect(isAllItemsPresent(0, false)).toBe(true);
9+
});
10+
11+
it("returns false when offset is 0 and hasMoreItems is true", () => {
12+
expect(isAllItemsPresent(0, true)).toBe(false);
13+
});
14+
15+
it("returns false when offset is 0 and hasMoreItems is undefined", () => {
16+
expect(isAllItemsPresent(0, undefined)).toBe(false);
17+
});
18+
19+
it("returns false when offset is greater than 0 and hasMoreItems is false", () => {
20+
expect(isAllItemsPresent(10, false)).toBe(false);
21+
});
22+
23+
it("returns false when offset is greater than 0 and hasMoreItems is true", () => {
24+
expect(isAllItemsPresent(10, true)).toBe(false);
25+
});
26+
27+
it("returns false when offset is greater than 0 and hasMoreItems is undefined", () => {
28+
expect(isAllItemsPresent(10, undefined)).toBe(false);
29+
});
30+
31+
it("returns false when offset is negative and hasMoreItems is false", () => {
32+
expect(isAllItemsPresent(-1, false)).toBe(false);
33+
});
34+
});
35+
36+
describe("isAllItemsPresentAtom", () => {
37+
it("reacts to changes in offset and hasMoreItems", () => {
38+
const offsetState = observable.box(0);
39+
const hasMoreItemsState = observable.box<boolean | undefined>(false);
40+
41+
const offsetComputed = computed(() => offsetState.get());
42+
const hasMoreItemsComputed = computed(() => hasMoreItemsState.get());
43+
44+
const atom = isAllItemsPresentAtom(offsetComputed, hasMoreItemsComputed);
45+
const values: boolean[] = [];
46+
47+
autorun(() => values.push(atom.get()));
48+
49+
expect(values.at(0)).toBe(true);
50+
51+
hasMoreItemsState.set(true);
52+
expect(atom.get()).toBe(false);
53+
54+
offsetState.set(10);
55+
expect(atom.get()).toBe(false);
56+
57+
hasMoreItemsState.set(false);
58+
expect(atom.get()).toBe(false);
59+
60+
offsetState.set(0);
61+
expect(atom.get()).toBe(true);
62+
63+
hasMoreItemsState.set(undefined);
64+
expect(atom.get()).toBe(false);
65+
66+
expect(values).toEqual([true, false, true, false]);
67+
});
68+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { computed, configure, observable } from "mobx";
2+
import { isAllItemsSelected, isAllItemsSelectedAtom } from "../models/selection.model.js";
3+
4+
describe("isAllItemsSelected", () => {
5+
describe("when selectedCount is -1 (not in multi-selection mode)", () => {
6+
it("returns false regardless of other parameters", () => {
7+
expect(isAllItemsSelected(-1, 10, 100, true)).toBe(false);
8+
expect(isAllItemsSelected(-1, 0, 0, true)).toBe(false);
9+
expect(isAllItemsSelected(-1, 10, 100, false)).toBe(false);
10+
});
11+
});
12+
13+
describe("when totalCount is -1 and isAllItemsPresent is false", () => {
14+
it("returns false even when selectedCount equals itemCount", () => {
15+
expect(isAllItemsSelected(50, 50, -1, false)).toBe(false);
16+
});
17+
18+
it("returns false when selectedCount is less than itemCount", () => {
19+
expect(isAllItemsSelected(25, 50, -1, false)).toBe(false);
20+
});
21+
22+
it("returns false when selectedCount is greater than itemCount", () => {
23+
expect(isAllItemsSelected(75, 50, -1, false)).toBe(false);
24+
});
25+
26+
it("returns false even when both selectedCount and itemCount are 0", () => {
27+
expect(isAllItemsSelected(0, 0, -1, false)).toBe(false);
28+
});
29+
});
30+
31+
describe("edge cases", () => {
32+
it("returns false when selectedCount is 0 and there are items", () => {
33+
expect(isAllItemsSelected(0, 10, 100, true)).toBe(false);
34+
});
35+
36+
it("handles case where itemCount exceeds totalCount (data inconsistency)", () => {
37+
expect(isAllItemsSelected(100, 150, 100, true)).toBe(true);
38+
});
39+
40+
it("handles negative itemCount edge case", () => {
41+
expect(isAllItemsSelected(5, -1, 0, true)).toBe(false);
42+
});
43+
44+
it("handles negative totalCount edge case", () => {
45+
expect(isAllItemsSelected(5, 10, -1, true)).toBe(false);
46+
});
47+
});
48+
});
49+
50+
describe("isAllItemsSelectedAtom", () => {
51+
configure({
52+
enforceActions: "never"
53+
});
54+
55+
it("returns true when all items are selected based on totalCount", () => {
56+
const selectedCount = computed(() => 100);
57+
const itemCount = computed(() => 50);
58+
const totalCount = computed(() => 100);
59+
const isAllItemsPresent = computed(() => true);
60+
61+
const atom = isAllItemsSelectedAtom(selectedCount, itemCount, totalCount, isAllItemsPresent);
62+
expect(atom.get()).toBe(true);
63+
});
64+
65+
it("returns false when selectedCount is less than totalCount", () => {
66+
const selectedCount = computed(() => 50);
67+
const itemCount = computed(() => 50);
68+
const totalCount = computed(() => 100);
69+
const isAllItemsPresent = computed(() => true);
70+
71+
const atom = isAllItemsSelectedAtom(selectedCount, itemCount, totalCount, isAllItemsPresent);
72+
expect(atom.get()).toBe(false);
73+
});
74+
75+
it("returns true when all items selected with isAllItemsPresent", () => {
76+
const selectedCount = computed(() => 50);
77+
const itemCount = computed(() => 50);
78+
const totalCount = computed(() => 0);
79+
const isAllItemsPresent = computed(() => true);
80+
81+
const atom = isAllItemsSelectedAtom(selectedCount, itemCount, totalCount, isAllItemsPresent);
82+
expect(atom.get()).toBe(true);
83+
});
84+
85+
it("returns false when selectedCount is -1", () => {
86+
const selectedCount = computed(() => -1);
87+
const itemCount = computed(() => 10);
88+
const totalCount = computed(() => 100);
89+
const isAllItemsPresent = computed(() => true);
90+
91+
const atom = isAllItemsSelectedAtom(selectedCount, itemCount, totalCount, isAllItemsPresent);
92+
expect(atom.get()).toBe(false);
93+
});
94+
95+
it("updates reactively when selectedCount changes", () => {
96+
const selectedCountBox = observable.box(50);
97+
const itemCount = computed(() => 50);
98+
const totalCount = computed(() => 100);
99+
const isAllItemsPresent = computed(() => true);
100+
101+
const atom = isAllItemsSelectedAtom(selectedCountBox, itemCount, totalCount, isAllItemsPresent);
102+
103+
expect(atom.get()).toBe(false);
104+
105+
selectedCountBox.set(100);
106+
expect(atom.get()).toBe(true);
107+
});
108+
109+
it("updates reactively when totalCount changes", () => {
110+
const totalCountBox = observable.box(100);
111+
const selectedCount = computed(() => 50);
112+
const itemCount = computed(() => 50);
113+
const isAllItemsPresent = computed(() => true);
114+
115+
const atom = isAllItemsSelectedAtom(selectedCount, itemCount, totalCountBox, isAllItemsPresent);
116+
117+
expect(atom.get()).toBe(false);
118+
119+
totalCountBox.set(50);
120+
expect(atom.get()).toBe(true);
121+
});
122+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { configure, observable } from "mobx";
2+
import { isCurrentPageSelectedAtom } from "../models/selection.model.js";
3+
4+
describe("isCurrentPageSelectedAtom", () => {
5+
configure({
6+
enforceActions: "never"
7+
});
8+
9+
it("returns true when all current page items are selected", () => {
10+
const gate = observable({
11+
props: {
12+
itemSelection: { type: "Multi" as const, selection: [{ id: "1" }, { id: "2" }] },
13+
datasource: { items: [{ id: "1" }, { id: "2" }] }
14+
}
15+
});
16+
const atom = isCurrentPageSelectedAtom(gate);
17+
expect(atom.get()).toBe(true);
18+
});
19+
20+
it("returns false when only some page items are selected", () => {
21+
const gate = observable({
22+
props: {
23+
itemSelection: { type: "Multi" as const, selection: [{ id: "1" }] },
24+
datasource: { items: [{ id: "1" }, { id: "2" }] }
25+
}
26+
});
27+
const atom = isCurrentPageSelectedAtom(gate);
28+
expect(atom.get()).toBe(false);
29+
});
30+
31+
it("returns false when selection type is Single", () => {
32+
const gate = observable({
33+
props: {
34+
itemSelection: { type: "Single" as const },
35+
datasource: { items: [{ id: "1" }] }
36+
}
37+
});
38+
const atom = isCurrentPageSelectedAtom(gate);
39+
expect(atom.get()).toBe(false);
40+
});
41+
42+
it("returns false when itemSelection is undefined", () => {
43+
const gate = observable({
44+
props: {
45+
datasource: { items: [{ id: "1" }] }
46+
}
47+
});
48+
const atom = isCurrentPageSelectedAtom(gate);
49+
expect(atom.get()).toBe(false);
50+
});
51+
52+
it("returns false when there are no items", () => {
53+
const gate = observable({
54+
props: {
55+
itemSelection: { type: "Multi" as const, selection: [] },
56+
datasource: { items: [] }
57+
}
58+
});
59+
const atom = isCurrentPageSelectedAtom(gate);
60+
expect(atom.get()).toBe(false);
61+
});
62+
63+
it("updates reactively when selection changes", () => {
64+
const gate = observable({
65+
props: {
66+
itemSelection: { type: "Multi" as const, selection: [{ id: "1" }] },
67+
datasource: { items: [{ id: "1" }, { id: "2" }] }
68+
}
69+
});
70+
const atom = isCurrentPageSelectedAtom(gate);
71+
72+
expect(atom.get()).toBe(false);
73+
74+
gate.props.itemSelection.selection.push({ id: "2" });
75+
expect(atom.get()).toBe(true);
76+
});
77+
});

0 commit comments

Comments
 (0)