Skip to content

Commit f8a90f1

Browse files
iobuhovleonardomendix
authored andcommitted
feat: add gallery state storage
1 parent 78688d0 commit f8a90f1

File tree

14 files changed

+195
-48
lines changed

14 files changed

+195
-48
lines changed

packages/pluggableWidgets/gallery-web/src/Gallery.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,37 @@
122122
</property>
123123
</propertyGroup>
124124
</propertyGroup>
125+
<propertyGroup caption="Personalization">
126+
<propertyGroup caption="Configuration">
127+
<property key="stateStorageType" type="enumeration" defaultValue="attribute">
128+
<caption>Store configuration in</caption>
129+
<description>When Browser local storage is selected, the configuration is scoped to a browser profile. This configuration is not tied to a Mendix user.</description>
130+
<enumerationValues>
131+
<enumerationValue key="attribute">Attribute</enumerationValue>
132+
<enumerationValue key="localStorage">Browser local storage</enumerationValue>
133+
</enumerationValues>
134+
</property>
135+
<property key="stateStorageAttr" type="attribute" required="false" onChange="onConfigurationChange">
136+
<caption>Attribute</caption>
137+
<description>Attribute containing the personalized configuration of the capabilities. This configuration is automatically stored and loaded. The attribute requires Unlimited String.</description>
138+
<attributeTypes>
139+
<attributeType name="String" />
140+
</attributeTypes>
141+
</property>
142+
<property key="storeFilters" type="boolean" defaultValue="true">
143+
<caption>Store filters</caption>
144+
<description />
145+
</property>
146+
<property key="storeSort" type="boolean" defaultValue="true">
147+
<caption>Store sort</caption>
148+
<description />
149+
</property>
150+
<property key="onConfigurationChange" type="action" required="false">
151+
<caption>On change</caption>
152+
<description />
153+
</property>
154+
</propertyGroup>
155+
</propertyGroup>
125156
<propertyGroup caption="Accessibility">
126157
<propertyGroup caption="Aria labels">
127158
<property key="filterSectionTitle" type="textTemplate" required="false">
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { PlainJs } from "@mendix/filter-commons/typings/settings";
2+
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
3+
import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
4+
import { EditableValue } from "mendix";
5+
import { computed, makeObservable } from "mobx";
6+
import { ObservableStorage } from "src/typings/storage";
7+
8+
type Gate = DerivedPropsGate<{
9+
stateStorageAttr: EditableValue<string>;
10+
}>;
11+
12+
export class AttributeStorage implements ObservableStorage, ReactiveController {
13+
private readonly _gate: Gate;
14+
15+
constructor(host: ReactiveControllerHost, gate: Gate) {
16+
host.addController(this);
17+
18+
this._gate = gate;
19+
makeObservable<this, "_attribute">(this, {
20+
_attribute: computed,
21+
data: computed.struct
22+
});
23+
}
24+
25+
setup(): () => void {
26+
return () => {};
27+
}
28+
29+
private get _attribute(): EditableValue<string> {
30+
return this._gate.props.stateStorageAttr;
31+
}
32+
33+
get data(): PlainJs {
34+
const jsonString = this._attribute.value;
35+
if (!jsonString) {
36+
return null;
37+
}
38+
try {
39+
return JSON.parse(jsonString) as PlainJs;
40+
} catch {
41+
console.warn("Invalid JSON configuration in the attribute. Resetting configuration.");
42+
this._attribute.setValue("");
43+
return null;
44+
}
45+
}
46+
47+
setData(data: PlainJs): void {
48+
data = data === "" ? null : data;
49+
this._attribute.setValue(JSON.stringify(data, null, 2));
50+
}
51+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { PlainJs } from "@mendix/filter-commons/typings/settings";
2+
import { ObservableStorage } from "src/typings/storage";
3+
4+
export class BrowserStorage implements ObservableStorage {
5+
constructor(private readonly _storageKey: string) {}
6+
7+
get data(): PlainJs {
8+
try {
9+
return JSON.parse(localStorage.getItem(this._storageKey) ?? "null");
10+
} catch {
11+
return null;
12+
}
13+
}
14+
15+
setData(data: PlainJs): void {
16+
localStorage.setItem(this._storageKey, JSON.stringify(data));
17+
}
18+
}

packages/pluggableWidgets/gallery-web/src/stores/GalleryPersistentStateController.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1-
import { Json, Serializable } from "@mendix/filter-commons/typings/settings";
1+
import { PlainJs, Serializable } from "@mendix/filter-commons/typings/settings";
22
import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
33
import { ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
44
import { action, comparer, computed, makeObservable, reaction } from "mobx";
5-
import { ObservableJsonStorage } from "src/typings/storage";
6-
7-
interface PersistentState {
8-
version: 1;
9-
[key: string]: Json;
10-
}
5+
import { ObservableStorage } from "src/typings/storage";
116

127
interface GalleryPersistentStateControllerSpec {
138
filtersHost: Serializable;
149
sortHost: Serializable;
15-
storage: ObservableJsonStorage;
10+
storage: ObservableStorage;
1611
}
1712

1813
export class GalleryPersistentStateController {
19-
private readonly _storage: ObservableJsonStorage;
14+
private readonly _storage: ObservableStorage;
2015
private readonly _filtersHost: Serializable;
2116
private readonly _sortHost: Serializable;
2217

18+
readonly schemaVersion: number = 1;
19+
2320
constructor(host: ReactiveControllerHost, spec: GalleryPersistentStateControllerSpec) {
2421
host.addController(this);
2522
this._storage = spec.storage;
@@ -65,23 +62,26 @@ export class GalleryPersistentStateController {
6562
return disposeAll;
6663
}
6764

68-
private get _persistentState(): PersistentState {
65+
private get _persistentState(): PlainJs {
6966
return this.toJSON();
7067
}
7168

72-
private _validate(data: Json): data is PersistentState {
73-
if (data == null || typeof data !== "object" || !("version" in data) || data.version !== 1) {
69+
private _validate(data: PlainJs): data is { [key: string]: PlainJs } {
70+
if (data == null || typeof data !== "object" || !("version" in data) || data.version !== this.schemaVersion) {
7471
return false;
7572
}
7673
return true;
7774
}
7875

79-
fromJSON(data: PersistentState): void {
76+
fromJSON(data: PlainJs): void {
77+
if (!this._validate(data)) {
78+
return;
79+
}
8080
this._filtersHost.fromJSON(data.filters);
8181
this._sortHost.fromJSON(data.sort);
8282
}
8383

84-
toJSON(): PersistentState {
84+
toJSON(): PlainJs {
8585
return {
8686
version: 1,
8787
filters: this._filtersHost.toJSON(),

packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
88
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
99
import { SortAPI } from "@mendix/widget-plugin-sorting/react/context";
1010
import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost";
11-
import { ListValue } from "mendix";
12-
import { PaginationEnum } from "../../typings/GalleryProps";
11+
import { EditableValue, ListValue } from "mendix";
12+
import { AttributeStorage } from "src/stores/AttributeStorage";
13+
import { BrowserStorage } from "src/stores/BrowserStorage";
14+
import { GalleryPersistentStateController } from "src/stores/GalleryPersistentStateController";
15+
import { ObservableStorage } from "src/typings/storage";
16+
import { PaginationEnum, StateStorageTypeEnum } from "../../typings/GalleryProps";
1317
import { QueryParamsController } from "../controllers/QueryParamsController";
1418

1519
interface DynamicProps {
1620
datasource: ListValue;
21+
stateStorageAttr?: EditableValue<string>;
1722
}
1823

1924
interface StaticProps {
@@ -22,6 +27,9 @@ interface StaticProps {
2227
showTotalCount: boolean;
2328
pageSize: number;
2429
name: string;
30+
stateStorageType: StateStorageTypeEnum;
31+
storeFilters: boolean;
32+
storeSort: boolean;
2533
}
2634

2735
export type GalleryPropsGate = DerivedPropsGate<DynamicProps>;
@@ -32,6 +40,9 @@ type GalleryStoreSpec = StaticProps & {
3240

3341
export class GalleryStore extends BaseControllerHost {
3442
private readonly _query: DatasourceController;
43+
private readonly _filtersHost: CustomFilterHost;
44+
private readonly _sortHost: SortStoreHost;
45+
private _storage: ObservableStorage | null = null;
3546

3647
readonly id: string = `GalleryStore@${generateUUID()}`;
3748
readonly name: string;
@@ -54,26 +65,49 @@ export class GalleryStore extends BaseControllerHost {
5465
showTotalCount: spec.showTotalCount
5566
});
5667

57-
const filterObserver = new CustomFilterHost();
58-
const sortObserver = new SortStoreHost();
68+
this._filtersHost = new CustomFilterHost();
69+
this._sortHost = new SortStoreHost();
5970

60-
const paramCtrl = new QueryParamsController(this, this._query, filterObserver, sortObserver);
71+
const paramCtrl = new QueryParamsController(this, this._query, this._filtersHost, this._sortHost);
6172

6273
this.filterAPI = createContextWithStub({
63-
filterObserver,
74+
filterObserver: this._filtersHost,
6475
parentChannelName: this.id,
6576
sharedInitFilter: paramCtrl.unzipFilter(spec.gate.props.datasource.filter)
6677
});
6778

6879
this.sortAPI = {
6980
version: 1,
70-
host: sortObserver,
81+
host: this._sortHost,
7182
initSortOrder: spec.gate.props.datasource.sortOrder
7283
};
7384

7485
new RefreshController(this, {
7586
delay: 0,
7687
query: this._query.derivedQuery
7788
});
89+
90+
this.initStateController(spec, spec.gate);
91+
}
92+
93+
initStateController(props: StaticProps, gate: GalleryPropsGate): void {
94+
if (props.stateStorageType === "localStorage") {
95+
this._storage = new BrowserStorage(this.name);
96+
} else if (gate.props.stateStorageAttr) {
97+
this._storage = new AttributeStorage(
98+
this,
99+
gate as DerivedPropsGate<{ stateStorageAttr: EditableValue<string> }>
100+
);
101+
}
102+
103+
if (!this._storage) {
104+
return;
105+
}
106+
107+
new GalleryPersistentStateController(this, {
108+
storage: this._storage,
109+
filtersHost: this._filtersHost,
110+
sortHost: this._sortHost
111+
});
78112
}
79113
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Json } from "@mendix/filter-commons/typings/settings";
1+
import { PlainJs } from "@mendix/filter-commons/typings/settings";
22

3-
export interface ObservableJsonStorage {
4-
data: Json;
5-
setData(data: Json): void;
3+
export interface ObservableStorage {
4+
data: PlainJs;
5+
setData(data: PlainJs): void;
66
}

packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @author Mendix Widgets Framework Team
55
*/
66
import { ComponentType, CSSProperties, ReactNode } from "react";
7-
import { ActionValue, DynamicValue, ListValue, ListActionValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
7+
import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
88

99
export type ItemSelectionModeEnum = "toggle" | "clear";
1010

@@ -18,6 +18,8 @@ export type ShowEmptyPlaceholderEnum = "none" | "custom";
1818

1919
export type OnClickTriggerEnum = "single" | "double";
2020

21+
export type StateStorageTypeEnum = "attribute" | "localStorage";
22+
2123
export interface GalleryContainerProps {
2224
name: string;
2325
class: string;
@@ -42,6 +44,10 @@ export interface GalleryContainerProps {
4244
onClickTrigger: OnClickTriggerEnum;
4345
onClick?: ListActionValue;
4446
onSelectionChange?: ActionValue;
47+
stateStorageType: StateStorageTypeEnum;
48+
stateStorageAttr?: EditableValue<string>;
49+
storeFilters: boolean;
50+
storeSort: boolean;
4551
filterSectionTitle?: DynamicValue<string>;
4652
emptyMessageTitle?: DynamicValue<string>;
4753
ariaLabelListBox?: DynamicValue<string>;
@@ -78,6 +84,11 @@ export interface GalleryPreviewProps {
7884
onClickTrigger: OnClickTriggerEnum;
7985
onClick: {} | null;
8086
onSelectionChange: {} | null;
87+
stateStorageType: StateStorageTypeEnum;
88+
stateStorageAttr: string;
89+
storeFilters: boolean;
90+
storeSort: boolean;
91+
onConfigurationChange: {} | null;
8192
filterSectionTitle: string;
8293
emptyMessageTitle: string;
8394
ariaLabelListBox: string;

packages/shared/filter-commons/src/typings/settings.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ export type FilterData = InputData | SelectData | null | undefined;
88

99
export type FiltersSettingsMap<T> = Map<T, FilterData>;
1010

11-
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
11+
export type PlainJs = string | number | boolean | null | PlainJs[] | { [key: string]: PlainJs };
1212

1313
export interface Serializable {
14-
toJSON(): Json;
15-
fromJSON(data: Json): void;
14+
toJSON(): PlainJs;
15+
fromJSON(data: PlainJs): void;
1616
}

packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { tag } from "@mendix/filter-commons/condition-utils";
2-
import { FilterData, FiltersSettingsMap, Json, Serializable } from "@mendix/filter-commons/typings/settings";
2+
import { FilterData, FiltersSettingsMap, PlainJs, Serializable } from "@mendix/filter-commons/typings/settings";
33
import { FilterCondition } from "mendix/filters";
44
import { and } from "mendix/filters/builders";
55
import { autorun, makeAutoObservable } from "mobx";
@@ -47,11 +47,11 @@ export class CustomFilterHost implements ObservableFilterHost, Serializable {
4747
}
4848
}
4949

50-
toJSON(): Json {
51-
return [...this.settings.entries()] as Json[];
50+
toJSON(): PlainJs {
51+
return [...this.settings.entries()] as PlainJs;
5252
}
5353

54-
fromJSON(data: Json): void {
54+
fromJSON(data: PlainJs): void {
5555
if (data == null || !Array.isArray(data)) {
5656
return;
5757
}

packages/shared/widget-plugin-sorting/src/__tests__/SortStoreHost.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ describe("SortStoreHost", () => {
1212
sortOrder: [
1313
[attrId("attr1"), "asc"],
1414
[attrId("attr2"), "desc"]
15-
] as SortInstruction[]
15+
] as SortInstruction[],
16+
setSortOrder: jest.fn()
1617
};
1718
});
1819

@@ -58,7 +59,8 @@ describe("SortStoreHost", () => {
5859

5960
it("should replace previously observed store", () => {
6061
const anotherMockStore: ObservableSortStore = {
61-
sortOrder: [[attrId("attr3"), "asc"]] as SortInstruction[]
62+
sortOrder: [[attrId("attr3"), "asc"]] as SortInstruction[],
63+
setSortOrder: jest.fn()
6264
};
6365

6466
sortStoreHost.observe(mockStore);
@@ -203,7 +205,8 @@ describe("SortStoreHost", () => {
203205

204206
it("should handle store changes after observation", () => {
205207
const mutableStore = {
206-
sortOrder: [[attrId("attr1"), "asc"]] as SortInstruction[]
208+
sortOrder: [[attrId("attr1"), "asc"]] as SortInstruction[],
209+
setSortOrder: jest.fn()
207210
};
208211

209212
sortStoreHost.observe(mutableStore);

0 commit comments

Comments
 (0)