Skip to content

Commit 436bafa

Browse files
committed
feat: improve error reporting
1 parent 0b55deb commit 436bafa

6 files changed

Lines changed: 122 additions & 35 deletions

File tree

packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.editorPreview.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style";
2+
import { withSortAPI } from "@mendix/widget-plugin-sorting/react/hocs/withSortAPI";
13
import { createElement, ReactElement } from "react";
2-
import { SortComponent } from "./components/SortComponent";
34
import { DropdownSortPreviewProps } from "../typings/DropdownSortProps";
4-
import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style";
5+
import { SortComponent } from "./components/SortComponent";
6+
7+
const DropdownPreview = withSortAPI(SortComponent);
58

69
export function preview(props: DropdownSortPreviewProps): ReactElement {
710
return (
8-
<SortComponent
11+
<DropdownPreview
912
value={null}
1013
direction="asc"
1114
className={props.className}

packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { enableStaticRendering } from "mobx-react-lite";
22
enableStaticRendering(true);
33

4-
import { GUID, ObjectItem } from "mendix";
5-
import { createElement, ReactElement, ReactNode, useCallback } from "react";
4+
import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper";
65
import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController";
6+
import { getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection";
7+
import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context";
8+
import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost";
9+
import { GUID, ObjectItem } from "mendix";
10+
import { createElement, ReactElement, ReactNode, useCallback, useMemo } from "react";
711
import { GalleryPreviewProps } from "../typings/GalleryProps";
812
import { Gallery as GalleryComponent } from "./components/Gallery";
913
import { useItemEventsController } from "./features/item-interaction/ItemEventsController";
1014
import { useGridPositions } from "./features/useGridPositions";
11-
import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper";
1215
import { useItemPreviewHelper } from "./helpers/ItemPreviewHelper";
1316
import { useItemSelectHelper } from "./helpers/useItemSelectHelper";
14-
import { getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection";
1517
import "./ui/GalleryPreview.scss";
1618

1719
const numberOfItems = 3;
1820

21+
const SortAPI = getGlobalSortContext();
22+
1923
function Preview(props: GalleryPreviewProps): ReactElement {
2024
const { emptyPlaceholder } = props;
2125
const items: ObjectItem[] = Array.from({ length: numberOfItems }).map((_, index) => ({
@@ -51,6 +55,15 @@ function Preview(props: GalleryPreviewProps): ReactElement {
5155
props.itemSelectionMode
5256
);
5357

58+
const sortAPI = useMemo(
59+
() =>
60+
({
61+
version: 1,
62+
host: new SortStoreHost()
63+
}) as const,
64+
[]
65+
);
66+
5467
return (
5568
<GalleryComponent
5669
className={props.class}
@@ -64,9 +77,11 @@ function Preview(props: GalleryPreviewProps): ReactElement {
6477
[emptyPlaceholder]
6578
)}
6679
header={
67-
<props.filtersPlaceholder.renderer caption="Place widgets like filter widget(s) and action button(s) here">
68-
<div />
69-
</props.filtersPlaceholder.renderer>
80+
<SortAPI.Provider value={sortAPI}>
81+
<props.filtersPlaceholder.renderer caption="Place widgets like filter widget(s) and action button(s) here">
82+
<div />
83+
</props.filtersPlaceholder.renderer>
84+
</SortAPI.Provider>
7085
}
7186
showHeader
7287
hasMoreItems={false}
Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { Context, createContext, useContext } from "react";
1+
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
2+
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
3+
import { Context, createContext, useContext, useEffect } from "react";
24
import { SortStoreHost } from "../stores/SortStoreHost";
35
import { SortInstruction } from "../types/store";
6+
import { Result, error, value } from "./result-meta";
47

58
export interface SortAPI {
69
version: 1;
@@ -11,13 +14,39 @@ export interface SortAPI {
1114
const SORT_PATH = "com.mendix.widgets.web.sortable.sortContext";
1215

1316
export function getGlobalSortContext(): Context<SortAPI | null> {
14-
return ((window as any)[SORT_PATH] ??= createContext<SortAPI | null>(null));
17+
const scope = window.top === window ? window : window.top;
18+
return ((scope as any)[SORT_PATH] ??= createContext<SortAPI | null>(null));
1519
}
1620

17-
export function useSortAPI(): SortAPI {
21+
export function useSortAPI(): Result<SortAPI, Error> {
1822
const api = useContext(getGlobalSortContext());
1923
if (api === null) {
20-
throw new Error("SortAPI is not available. Widget is out of parent context.");
24+
return error(new Error("Error: widget is out of context. Please place the widget inside the Gallery header."));
2125
}
22-
return api;
26+
return value(api);
27+
}
28+
29+
export function useLockSortAPI(api: SortAPI): Result<SortAPI, Error> {
30+
const id = useLock(api);
31+
32+
if (api.host.usedBy !== id) {
33+
return error(
34+
new Error(
35+
`Error: Sort API is already in use by another widget. Remove other sort widgets and refresh the page.`
36+
)
37+
);
38+
}
39+
40+
return value(api);
41+
}
42+
43+
function useLock({ host }: SortAPI): string {
44+
const [unlock, id] = useConst(() => {
45+
const id = `useLock@${generateUUID()}`;
46+
return [host.lock(id), id] as const;
47+
});
48+
49+
useEffect(() => unlock, [unlock]);
50+
51+
return id;
2352
}

packages/shared/widget-plugin-sorting/src/react/hocs/withSortAPI.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1+
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
12
import { ErrorBoundary } from "@mendix/widget-plugin-component-kit/ErrorBoundary";
2-
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
3-
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
43
import { observer } from "mobx-react-lite";
5-
import { createElement, FC, useEffect } from "react";
6-
import { SortAPI, useSortAPI } from "../context";
4+
import { createElement, FC } from "react";
5+
import { SortAPI, useLockSortAPI, useSortAPI } from "../context";
76

87
export function withSortAPI<P extends object>(Component: FC<P & { sortAPI: SortAPI }>): FC<P> {
9-
const SortAPIInjector = observer<P>(function SortAPIInjector(props) {
10-
const id = useConst(() => `SortAPIInjector@${generateUUID()}`);
11-
const sortAPI = useSortAPI();
12-
13-
useEffect(() => sortAPI.host.useHost(id), [sortAPI.host, id]);
8+
const SortAPIGuard = observer(function SortAPIGuard(props: P & { sortAPI: SortAPI }): React.ReactElement {
9+
const sortAPI = useLockSortAPI(props.sortAPI);
1410

15-
if (sortAPI.host.usedBy !== null && sortAPI.host.usedBy !== id) {
16-
return null;
11+
if (sortAPI.hasError) {
12+
return <Alert bootstrapStyle="danger">{sortAPI.error.message}</Alert>;
1713
}
1814

19-
return <Component {...props} sortAPI={sortAPI} />;
15+
return <Component {...props} sortAPI={sortAPI.value} />;
2016
});
2117

18+
function SortAPIInjector(props: P): React.ReactElement {
19+
const sortAPI = useSortAPI();
20+
21+
if (sortAPI.hasError) {
22+
return <Alert bootstrapStyle="danger">{sortAPI.error.message}</Alert>;
23+
}
24+
25+
return <SortAPIGuard {...props} sortAPI={sortAPI.value} />;
26+
}
27+
2228
return function (props): React.ReactElement {
2329
return (
2430
<ErrorBoundary>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface Error<T> {
2+
hasError: true;
3+
error: T;
4+
}
5+
6+
export interface Success<T> {
7+
hasError: false;
8+
value: T;
9+
}
10+
11+
export type Result<TValue, TError> = Error<TError> | Success<TValue>;
12+
13+
export function error<T>(error: T): Error<T> {
14+
return { hasError: true, error };
15+
}
16+
17+
export function value<T>(value: T): Success<T> {
18+
return { hasError: false, value };
19+
}

packages/shared/widget-plugin-sorting/src/stores/SortStoreHost.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { action, computed, makeObservable, observable, runInAction } from "mobx";
1+
import { action, computed, makeObservable, observable } from "mobx";
22
import { ObservableSortStore, SortInstruction } from "../types/store";
33

44
export class SortStoreHost {
55
private _store: ObservableSortStore | null = null;
6-
private _usedBy: string | null = null;
6+
private _usedBy: string[] = [];
77

88
get sortOrder(): SortInstruction[] {
99
return this._store?.sortOrder ?? [];
1010
}
1111

1212
constructor() {
13-
makeObservable<this, "_usedBy">(this, {
13+
makeObservable<this, "_usedBy" | "_add" | "_remove">(this, {
1414
sortOrder: computed,
15-
useHost: action,
15+
lock: action,
16+
_add: action,
17+
_remove: action,
1618
_usedBy: observable,
1719
usedBy: computed,
1820
observe: action,
@@ -32,12 +34,25 @@ export class SortStoreHost {
3234
this._store = null;
3335
}
3436

35-
useHost(id: string): () => void {
36-
this._usedBy = id;
37-
return () => runInAction(() => (this._usedBy = null));
37+
lock(id: string): () => void {
38+
this._add(id);
39+
return () => this._remove(id);
40+
}
41+
42+
private _add(id: string): void {
43+
if (this._usedBy.indexOf(id) === -1) {
44+
this._usedBy.push(id);
45+
}
46+
}
47+
48+
private _remove(id: string): void {
49+
const index = this._usedBy.indexOf(id);
50+
if (index !== -1) {
51+
this._usedBy.splice(index, 1);
52+
}
3853
}
3954

4055
get usedBy(): string | null {
41-
return this._usedBy;
56+
return this._usedBy.at(0) ?? null;
4257
}
4358
}

0 commit comments

Comments
 (0)