wenay-react2 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/doc/EXAMPLE_USAGE.md +915 -0
- package/doc/PROJECT_FUNCTIONALITY.md +401 -0
- package/doc/PROJECT_RULES.md +80 -0
- package/doc/WENAY_REACT2_RENAMES.md +177 -0
- package/doc/changes/v1.0.35.md +26 -0
- package/doc/changes/v1.0.37.md +13 -0
- package/doc/changes/v1.0.38.md +48 -0
- package/doc/changes/v1.0.39.md +35 -0
- package/doc/changes/v1.0.40.md +15 -0
- package/doc/changes/v1.0.41.md +13 -0
- package/doc/progress/README.md +14 -0
- package/doc/progress/hook-controller-opportunities.md +367 -0
- package/doc/progress/hook-extraction-audit.md +195 -0
- package/doc/progress/public-surface-normalization.md +368 -0
- package/doc/progress/style-system-normalization.md +121 -0
- package/doc/target/README.md +33 -0
- package/doc/target/my.md +109 -0
- package/doc/wenay-react2-1.0.8.tgz +0 -0
- package/doc/wenay-react2-rare.md +702 -0
- package/doc/wenay-react2.md +475 -0
- package/lib/common/src/components/Dnd/FloatingWindow.d.ts +29 -12
- package/lib/common/src/components/Dnd/FloatingWindow.js +100 -91
- package/lib/common/src/components/Menu/RightMenu.d.ts +28 -1
- package/lib/common/src/components/Menu/RightMenu.js +62 -35
- package/lib/common/src/components/Modal/LeftModal.js +5 -4
- package/lib/common/src/components/Modal/Modal.js +8 -7
- package/lib/common/src/components/ParamsEditor.d.ts +15 -0
- package/lib/common/src/components/ParamsEditor.js +45 -18
- package/lib/common/src/components/Settings/SettingsDialog.d.ts +68 -6
- package/lib/common/src/components/Settings/SettingsDialog.js +61 -12
- package/lib/common/src/components/Toolbar/Toolbar.js +64 -19
- package/lib/common/src/components/UiSlot/UiSlot.js +5 -4
- package/lib/common/src/grid/columnState/columnState.js +8 -6
- package/lib/common/src/hooks/useKeyboard.js +4 -3
- package/lib/common/src/logs/logs.d.ts +40 -133
- package/lib/common/src/logs/logs.js +96 -66
- package/lib/common/src/logs/logsContext.d.ts +26 -0
- package/lib/common/src/logs/logsContext.js +25 -20
- package/lib/common/src/logs/logsController.d.ts +88 -0
- package/lib/common/src/logs/logsController.js +54 -0
- package/lib/common/src/logs/miniLogs.d.ts +69 -3
- package/lib/common/src/logs/miniLogs.js +70 -15
- package/lib/common/src/menu/menu.d.ts +15 -3
- package/lib/common/src/menu/menu.js +81 -17
- package/lib/common/src/menu/menuMouse.d.ts +18 -0
- package/lib/common/src/menu/menuMouse.js +43 -1
- package/lib/common/src/styles/tokens.d.ts +7 -6
- package/lib/common/src/styles/tokens.js +8 -5
- package/lib/common/src/utils/memoryStore.d.ts +1 -10
- package/lib/common/src/utils/searchHistory.js +4 -3
- package/lib/style/menuRight.css +29 -23
- package/lib/style/style.css +15 -9
- package/lib/style/tokens.css +8 -4
- package/package.json +50 -49
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Params } from "wenay-common2";
|
|
2
|
+
export const getSettingLogs = () => ({
|
|
3
|
+
minVarLogs: { name: "min. importance for notifications", range: { min: 0, max: 25, step: 1 }, value: 0 },
|
|
4
|
+
minVarMessage: { name: "min. importance for log table", range: { min: 0, max: 25, step: 1 }, value: 0 },
|
|
5
|
+
timeShow: { name: "screen display time", range: { min: 1, max: 20, step: 1 }, value: 2 },
|
|
6
|
+
show: { name: "show", value: true }
|
|
7
|
+
});
|
|
8
|
+
function addToArr(arr, data, limit) {
|
|
9
|
+
arr.unshift(data);
|
|
10
|
+
if (arr.length > limit)
|
|
11
|
+
arr.length = limit;
|
|
12
|
+
}
|
|
13
|
+
export function createLogsControllerState(state = {}) {
|
|
14
|
+
return {
|
|
15
|
+
full: state.full ?? { map: new Map() },
|
|
16
|
+
mini: state.mini ?? { last: [] },
|
|
17
|
+
settings: state.settings ?? { params: Params.GetSimpleParams(getSettingLogs()) },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createLogsController({ options, state = createLogsControllerState(), onFullChange, onMiniChange, onSettingsChange, }) {
|
|
21
|
+
let num = 0;
|
|
22
|
+
return {
|
|
23
|
+
state,
|
|
24
|
+
options,
|
|
25
|
+
addLogs(input) {
|
|
26
|
+
const item = { ...input, num: num++ };
|
|
27
|
+
addToArr(state.mini.last, item, options.limit ?? 50);
|
|
28
|
+
const perId = state.full.map.get(input.id) ?? state.full.map.set(input.id, []).get(input.id);
|
|
29
|
+
addToArr(perId, item, options.limitPer);
|
|
30
|
+
onFullChange?.();
|
|
31
|
+
onMiniChange?.();
|
|
32
|
+
return item;
|
|
33
|
+
},
|
|
34
|
+
getRows() {
|
|
35
|
+
return [...state.full.map.values()].flat();
|
|
36
|
+
},
|
|
37
|
+
getMiniRows() {
|
|
38
|
+
return state.mini.last.slice();
|
|
39
|
+
},
|
|
40
|
+
getLatest() {
|
|
41
|
+
return state.mini.last[0];
|
|
42
|
+
},
|
|
43
|
+
params: {
|
|
44
|
+
def: getSettingLogs,
|
|
45
|
+
get() { return state.settings.params; },
|
|
46
|
+
set(settings) {
|
|
47
|
+
state.settings.params = settings;
|
|
48
|
+
onSettingsChange?.();
|
|
49
|
+
onMiniChange?.();
|
|
50
|
+
onFullChange?.();
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,5 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { CellMouseDownEvent, ColDef, GridApi, GridPreDestroyedEvent, GridReadyEvent } from "ag-grid-community";
|
|
3
|
+
import { type AgGridTableProps } from "../grid/agGrid4";
|
|
4
|
+
export declare const miniLogsColumnDefs: ({
|
|
5
|
+
field: string;
|
|
6
|
+
sort: "desc";
|
|
7
|
+
width: number;
|
|
8
|
+
valueFormatter: (e: import("ag-grid-community").ValueFormatterParams<any, any, any>) => any;
|
|
9
|
+
wrapText?: undefined;
|
|
10
|
+
autoHeight?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
field: string;
|
|
13
|
+
width: number;
|
|
14
|
+
sort?: undefined;
|
|
15
|
+
valueFormatter?: undefined;
|
|
16
|
+
wrapText?: undefined;
|
|
17
|
+
autoHeight?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
field: string;
|
|
20
|
+
wrapText: true;
|
|
21
|
+
autoHeight: true;
|
|
22
|
+
width: number;
|
|
23
|
+
sort?: undefined;
|
|
24
|
+
valueFormatter?: undefined;
|
|
25
|
+
})[];
|
|
26
|
+
export declare const miniLogsDefaultColDef: {
|
|
27
|
+
wrapText: true;
|
|
28
|
+
headerClass: () => string;
|
|
29
|
+
resizable: true;
|
|
30
|
+
cellStyle: {
|
|
31
|
+
textAlign: string;
|
|
32
|
+
};
|
|
33
|
+
sortable: true;
|
|
34
|
+
filter: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type UseMiniLogsTableOptions<T = any> = {
|
|
3
37
|
data: T[];
|
|
4
38
|
onClick?: (e: CellMouseDownEvent<T>) => any;
|
|
5
|
-
|
|
39
|
+
columnDefs?: ColDef<T>[];
|
|
40
|
+
defaultColDef?: ColDef<T>;
|
|
41
|
+
};
|
|
42
|
+
export type MiniLogsTableController<T = any> = {
|
|
43
|
+
rows: T[];
|
|
44
|
+
columns: ColDef<T>[];
|
|
45
|
+
columnDefs: ColDef<T>[];
|
|
46
|
+
defaultColDef: ColDef<T>;
|
|
47
|
+
apiRef: React.RefObject<GridApi<T> | null>;
|
|
48
|
+
fit: () => boolean;
|
|
49
|
+
getApi: () => GridApi<T> | null;
|
|
50
|
+
withApi: <R>(fn: (api: GridApi<T>) => R) => R | undefined;
|
|
51
|
+
onGridReady: (e: GridReadyEvent<T>) => void;
|
|
52
|
+
onGridPreDestroyed: (e: GridPreDestroyedEvent<T>) => void;
|
|
53
|
+
onCellMouseDown: (e: CellMouseDownEvent<T>) => void;
|
|
54
|
+
props: AgGridTableProps<T>;
|
|
55
|
+
tableProps: AgGridTableProps<T>;
|
|
56
|
+
gridProps: AgGridTableProps<T>;
|
|
57
|
+
};
|
|
58
|
+
export type MiniLogsController<T = any> = MiniLogsTableController<T>;
|
|
59
|
+
export type MiniLogsViewProps<T = any> = {
|
|
60
|
+
controller: MiniLogsTableController<T>;
|
|
61
|
+
className?: string;
|
|
62
|
+
style?: React.CSSProperties;
|
|
63
|
+
};
|
|
64
|
+
export type MiniLogsTableProps<T = any> = UseMiniLogsTableOptions<T> & {
|
|
65
|
+
className?: string;
|
|
66
|
+
style?: React.CSSProperties;
|
|
67
|
+
};
|
|
68
|
+
export declare function useMiniLogsTable<T = any>(options: UseMiniLogsTableOptions<T>): MiniLogsTableController<T>;
|
|
69
|
+
export declare function MiniLogsView<T = any>({ controller, className, style }: MiniLogsViewProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
70
|
+
export declare function MiniLogsTable<T = any>({ className, style, ...options }: MiniLogsTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
71
|
+
export declare function MiniLogs<T = any>(props: MiniLogsTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { timeLocalToStr_hhmmss } from "wenay-common2";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
4
|
+
import { AgGridTable, colDefCentered } from "../grid/agGrid4";
|
|
5
|
+
export const miniLogsColumnDefs = [
|
|
5
6
|
{
|
|
6
7
|
field: "time",
|
|
7
8
|
sort: "desc",
|
|
@@ -27,17 +28,71 @@ const columns = [
|
|
|
27
28
|
width: 150,
|
|
28
29
|
},
|
|
29
30
|
];
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
export const miniLogsDefaultColDef = { ...colDefCentered, wrapText: true };
|
|
32
|
+
export function useMiniLogsTable(options) {
|
|
33
|
+
const { data, onClick, columnDefs, defaultColDef: defaultColDefOverride } = options;
|
|
34
|
+
const apiRef = useRef(null);
|
|
35
|
+
const columns = useMemo(() => (columnDefs ?? miniLogsColumnDefs), [columnDefs]);
|
|
36
|
+
const defaultColDef = useMemo(() => ({ ...miniLogsDefaultColDef, ...defaultColDefOverride }), [defaultColDefOverride]);
|
|
37
|
+
const fit = useCallback(() => {
|
|
38
|
+
const api = apiRef.current;
|
|
39
|
+
if (!api)
|
|
40
|
+
return false;
|
|
41
|
+
api.sizeColumnsToFit();
|
|
42
|
+
return true;
|
|
43
|
+
}, []);
|
|
44
|
+
const getApi = useCallback(() => apiRef.current, []);
|
|
45
|
+
const withApi = useCallback(function withApi(fn) {
|
|
46
|
+
const api = apiRef.current;
|
|
47
|
+
return api ? fn(api) : undefined;
|
|
48
|
+
}, []);
|
|
49
|
+
const onGridReady = useCallback((e) => {
|
|
50
|
+
apiRef.current = e.api;
|
|
51
|
+
fit();
|
|
52
|
+
}, [fit]);
|
|
53
|
+
const onGridPreDestroyed = useCallback((_e) => {
|
|
54
|
+
apiRef.current = null;
|
|
55
|
+
}, []);
|
|
56
|
+
const onCellMouseDown = useCallback((e) => {
|
|
57
|
+
onClick?.(e);
|
|
58
|
+
}, [onClick]);
|
|
59
|
+
const props = useMemo(() => ({
|
|
60
|
+
suppressCellFocus: true,
|
|
61
|
+
onGridReady,
|
|
62
|
+
onGridPreDestroyed,
|
|
63
|
+
defaultColDef,
|
|
64
|
+
headerHeight: 30,
|
|
65
|
+
rowHeight: 26,
|
|
66
|
+
autoSizePadding: 1,
|
|
67
|
+
rowData: data,
|
|
68
|
+
columnDefs: columns,
|
|
69
|
+
onCellMouseDown,
|
|
70
|
+
}), [columns, data, defaultColDef, onCellMouseDown, onGridPreDestroyed, onGridReady]);
|
|
71
|
+
return useMemo(() => ({
|
|
72
|
+
rows: data,
|
|
73
|
+
columns,
|
|
74
|
+
columnDefs: columns,
|
|
75
|
+
defaultColDef,
|
|
76
|
+
apiRef,
|
|
77
|
+
fit,
|
|
78
|
+
getApi,
|
|
79
|
+
withApi,
|
|
80
|
+
onGridReady,
|
|
81
|
+
onGridPreDestroyed,
|
|
82
|
+
onCellMouseDown,
|
|
83
|
+
props,
|
|
84
|
+
tableProps: props,
|
|
85
|
+
gridProps: props,
|
|
86
|
+
}), [columns, data, defaultColDef, fit, getApi, onCellMouseDown, onGridPreDestroyed, onGridReady, props, withApi]);
|
|
87
|
+
}
|
|
88
|
+
export function MiniLogsView({ controller, className, style }) {
|
|
89
|
+
const classNames = ["maxSize", className].filter(Boolean).join(" ");
|
|
90
|
+
return _jsx("div", { className: classNames, style: style, children: _jsx(AgGridTable, { ...controller.props }) });
|
|
91
|
+
}
|
|
92
|
+
export function MiniLogsTable({ className, style, ...options }) {
|
|
93
|
+
const table = useMiniLogsTable(options);
|
|
94
|
+
return _jsx(MiniLogsView, { controller: table, className: className, style: style });
|
|
95
|
+
}
|
|
96
|
+
export function MiniLogs(props) {
|
|
97
|
+
return _jsx(MiniLogsTable, { ...props });
|
|
43
98
|
}
|
|
@@ -4,6 +4,8 @@ import React, { ReactElement } from 'react';
|
|
|
4
4
|
*******************************************************/
|
|
5
5
|
export type MenuItemStrict<T = any> = {
|
|
6
6
|
name: string | ((status?: T) => string);
|
|
7
|
+
/** Stable diagnostics key. Stats never fall back to visible labels. */
|
|
8
|
+
actionKey?: string | null;
|
|
7
9
|
getStatus?: (() => T) | null;
|
|
8
10
|
onClick?: ((e: any) => void | undefined | null | ((void | undefined | null | Promise<any> | (() => Promise<any>))[]) | Promise<any>) | null;
|
|
9
11
|
active?: (() => boolean) | null;
|
|
@@ -14,6 +16,14 @@ export type MenuItemStrict<T = any> = {
|
|
|
14
16
|
menuElement?: typeof MenuElement;
|
|
15
17
|
};
|
|
16
18
|
export type MenuItem<T = any> = MenuItemStrict<T> | false | null | undefined;
|
|
19
|
+
export type MenuActionEventType = "click" | "ok" | "error" | "taskOk" | "taskError" | "submenuOpen" | "submenuOk" | "submenuError" | "funcOpen" | "funcOk" | "funcError" | "focusOpen" | "focusOk" | "focusError";
|
|
20
|
+
export type MenuActionEvent = {
|
|
21
|
+
type: MenuActionEventType;
|
|
22
|
+
item: MenuItemStrict;
|
|
23
|
+
actionKey?: string;
|
|
24
|
+
error?: unknown;
|
|
25
|
+
};
|
|
26
|
+
export type MenuActionHandler = (event: MenuActionEvent) => void;
|
|
17
27
|
/*******************************************************
|
|
18
28
|
* Helper type
|
|
19
29
|
*******************************************************/
|
|
@@ -31,12 +41,13 @@ declare function MenuProgress({ data }: {
|
|
|
31
41
|
/*******************************************************
|
|
32
42
|
* Main menu element with onClick and counters
|
|
33
43
|
*******************************************************/
|
|
34
|
-
declare function MenuElement({ data: item, toLeft, className, update, open, }: {
|
|
35
|
-
data: Pick<MenuItemStrict, "onClick" | "active" | "name" | "getStatus">;
|
|
44
|
+
declare function MenuElement({ data: item, toLeft, className, update, open, onActionEvent, }: {
|
|
45
|
+
data: Pick<MenuItemStrict, "onClick" | "active" | "name" | "getStatus" | "actionKey">;
|
|
36
46
|
toLeft: boolean;
|
|
37
47
|
className?: (active?: boolean) => string;
|
|
38
48
|
update: () => void;
|
|
39
49
|
open?: boolean;
|
|
50
|
+
onActionEvent?: MenuActionHandler;
|
|
40
51
|
}): ReactElement;
|
|
41
52
|
/*******************************************************
|
|
42
53
|
* Menu renders the popup menu with support for
|
|
@@ -62,6 +73,7 @@ type MenuProps = {
|
|
|
62
73
|
data: MenuItem[];
|
|
63
74
|
zIndex?: number;
|
|
64
75
|
className?: (active?: boolean) => string;
|
|
76
|
+
onActionEvent?: MenuActionHandler;
|
|
65
77
|
coordinate?: {
|
|
66
78
|
x: number;
|
|
67
79
|
y: number;
|
|
@@ -69,5 +81,5 @@ type MenuProps = {
|
|
|
69
81
|
left?: number;
|
|
70
82
|
};
|
|
71
83
|
};
|
|
72
|
-
export declare function Menu({ coordinate, data, zIndex, menu, className, menuElement, }: MenuProps): ReactElement;
|
|
84
|
+
export declare function Menu({ coordinate, data, zIndex, menu, className, menuElement, onActionEvent, }: MenuProps): ReactElement;
|
|
73
85
|
export { MenuProgress, MenuElement };
|
|
@@ -29,7 +29,7 @@ function MenuProgress({ data }) {
|
|
|
29
29
|
/*******************************************************
|
|
30
30
|
* Main menu element with onClick and counters
|
|
31
31
|
*******************************************************/
|
|
32
|
-
function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
32
|
+
function MenuElement({ data: item, toLeft, className, update, open, onActionEvent, }) {
|
|
33
33
|
const unsubOk = useRef(null);
|
|
34
34
|
const unsubErr = useRef(null);
|
|
35
35
|
useEffect(() => {
|
|
@@ -47,8 +47,18 @@ function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
|
47
47
|
"MenuR " + (active ? "toButtonA" : "toButton"), style: { float: toLeft ? "left" : "right" }, onClick: () => {
|
|
48
48
|
if (!item.onClick)
|
|
49
49
|
return;
|
|
50
|
-
const
|
|
50
|
+
const actionKey = item.actionKey ?? undefined;
|
|
51
|
+
onActionEvent?.({ type: "click", item: item, actionKey });
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = item.onClick(item);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
onActionEvent?.({ type: "error", item: item, actionKey, error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
51
60
|
if (!result) {
|
|
61
|
+
onActionEvent?.({ type: "ok", item: item, actionKey });
|
|
52
62
|
update();
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
@@ -72,8 +82,14 @@ function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
|
72
82
|
setProgress(null);
|
|
73
83
|
}
|
|
74
84
|
};
|
|
75
|
-
unsubOk.current = pa.onOk((data, i, countOk, countError, count) =>
|
|
76
|
-
|
|
85
|
+
unsubOk.current = pa.onOk((data, i, countOk, countError, count) => {
|
|
86
|
+
onActionEvent?.({ type: "taskOk", item: item, actionKey });
|
|
87
|
+
return onTick(countOk, countError, count);
|
|
88
|
+
});
|
|
89
|
+
unsubErr.current = pa.onError((error, i, countOk, countError, count) => {
|
|
90
|
+
onActionEvent?.({ type: "taskError", item: item, actionKey, error });
|
|
91
|
+
return onTick(countOk, countError, count);
|
|
92
|
+
});
|
|
77
93
|
void pa.allSettled();
|
|
78
94
|
}
|
|
79
95
|
// If this is a single promise
|
|
@@ -81,6 +97,7 @@ function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
|
81
97
|
setProgress({});
|
|
82
98
|
result
|
|
83
99
|
.then(async (val) => {
|
|
100
|
+
onActionEvent?.({ type: "ok", item: item, actionKey });
|
|
84
101
|
if (Array.isArray(val) && val.length) {
|
|
85
102
|
// If an array from Promise.allSettled was returned
|
|
86
103
|
// Count ok/error results
|
|
@@ -99,6 +116,10 @@ function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
|
99
116
|
setProgress({ ok: 1 });
|
|
100
117
|
await sleepAsync(0);
|
|
101
118
|
}
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
onActionEvent?.({ type: "error", item: item, actionKey, error });
|
|
122
|
+
throw error;
|
|
102
123
|
})
|
|
103
124
|
.finally(async () => {
|
|
104
125
|
await sleepAsync(500);
|
|
@@ -106,73 +127,116 @@ function MenuElement({ data: item, toLeft, className, update, open, }) {
|
|
|
106
127
|
});
|
|
107
128
|
}
|
|
108
129
|
else {
|
|
130
|
+
onActionEvent?.({ type: "ok", item: item, actionKey });
|
|
109
131
|
update();
|
|
110
132
|
}
|
|
111
133
|
}, children: _jsxs("div", { className: "toLine", children: [typeof item.name === "string"
|
|
112
134
|
? item.name
|
|
113
135
|
: item.name(item.getStatus?.()), progress && _jsx(MenuProgress, { data: progress })] }) }));
|
|
114
136
|
}
|
|
115
|
-
const MenuItemWrapper = ({ item, index, update, className, isLeftAligned, leftPos, menuElement, open, setOpenIndex, }) => {
|
|
137
|
+
const MenuItemWrapper = ({ item, index, update, className, isLeftAligned, leftPos, menuElement, open, setOpenIndex, onActionEvent, }) => {
|
|
116
138
|
const [childMenu, setChildMenu] = useState([]);
|
|
117
139
|
const [asyncFuncElement, setAsyncFuncElement] = useState(null);
|
|
118
140
|
const [onFocusMenu, setOnFocusMenu] = useState([]);
|
|
119
141
|
useEffect(() => {
|
|
120
142
|
if (open && item.next) {
|
|
143
|
+
const actionKey = item.actionKey ?? undefined;
|
|
144
|
+
onActionEvent?.({ type: "submenuOpen", item, actionKey });
|
|
121
145
|
let alive = true; // guard: do not set state after unmount or item change
|
|
122
|
-
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
result = item.next();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
onActionEvent?.({ type: "submenuError", item, actionKey, error });
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
123
154
|
if (result instanceof Promise) {
|
|
124
155
|
result.then((val) => {
|
|
125
156
|
if (alive)
|
|
126
157
|
setChildMenu(val.filter(Boolean));
|
|
158
|
+
onActionEvent?.({ type: "submenuOk", item, actionKey });
|
|
159
|
+
}).catch((error) => {
|
|
160
|
+
onActionEvent?.({ type: "submenuError", item, actionKey, error });
|
|
161
|
+
throw error;
|
|
127
162
|
});
|
|
128
163
|
}
|
|
129
164
|
else {
|
|
130
165
|
setChildMenu(result.filter(Boolean));
|
|
166
|
+
onActionEvent?.({ type: "submenuOk", item, actionKey });
|
|
131
167
|
}
|
|
132
168
|
return () => { alive = false; };
|
|
133
169
|
}
|
|
134
170
|
else {
|
|
135
171
|
setChildMenu([]);
|
|
136
172
|
}
|
|
137
|
-
}, [open, item.next]);
|
|
173
|
+
}, [open, item, item.next, onActionEvent]);
|
|
138
174
|
useEffect(() => {
|
|
139
175
|
if (open && item.func) {
|
|
176
|
+
const actionKey = item.actionKey ?? undefined;
|
|
177
|
+
onActionEvent?.({ type: "funcOpen", item, actionKey });
|
|
140
178
|
let alive = true;
|
|
141
|
-
|
|
179
|
+
let result;
|
|
180
|
+
try {
|
|
181
|
+
result = item.func();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
onActionEvent?.({ type: "funcError", item, actionKey, error });
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
142
187
|
if (result instanceof Promise) {
|
|
143
188
|
result.then((val) => {
|
|
144
189
|
if (alive)
|
|
145
190
|
setAsyncFuncElement(val);
|
|
191
|
+
onActionEvent?.({ type: "funcOk", item, actionKey });
|
|
192
|
+
}).catch((error) => {
|
|
193
|
+
onActionEvent?.({ type: "funcError", item, actionKey, error });
|
|
194
|
+
throw error;
|
|
146
195
|
});
|
|
147
196
|
}
|
|
148
197
|
else {
|
|
149
198
|
setAsyncFuncElement(result);
|
|
199
|
+
onActionEvent?.({ type: "funcOk", item, actionKey });
|
|
150
200
|
}
|
|
151
201
|
return () => { alive = false; };
|
|
152
202
|
}
|
|
153
203
|
else {
|
|
154
204
|
setAsyncFuncElement(null);
|
|
155
205
|
}
|
|
156
|
-
}, [open, item.func]);
|
|
206
|
+
}, [open, item, item.func, onActionEvent]);
|
|
157
207
|
useEffect(() => {
|
|
158
208
|
if (open && item.onFocus) {
|
|
209
|
+
const actionKey = item.actionKey ?? undefined;
|
|
210
|
+
onActionEvent?.({ type: "focusOpen", item, actionKey });
|
|
159
211
|
let alive = true;
|
|
160
|
-
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = item.onFocus();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
onActionEvent?.({ type: "focusError", item, actionKey, error });
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
161
220
|
if (result instanceof Promise) {
|
|
162
221
|
result.then((val) => {
|
|
163
222
|
if (alive)
|
|
164
223
|
setOnFocusMenu(val.filter(Boolean));
|
|
224
|
+
onActionEvent?.({ type: "focusOk", item, actionKey });
|
|
225
|
+
}).catch((error) => {
|
|
226
|
+
onActionEvent?.({ type: "focusError", item, actionKey, error });
|
|
227
|
+
throw error;
|
|
165
228
|
});
|
|
166
229
|
}
|
|
167
230
|
else {
|
|
168
231
|
setOnFocusMenu(result.filter(Boolean));
|
|
232
|
+
onActionEvent?.({ type: "focusOk", item, actionKey });
|
|
169
233
|
}
|
|
170
234
|
return () => { alive = false; };
|
|
171
235
|
}
|
|
172
236
|
else {
|
|
173
237
|
setOnFocusMenu([]);
|
|
174
238
|
}
|
|
175
|
-
}, [open, item.onFocus]);
|
|
239
|
+
}, [open, item, item.onFocus, onActionEvent]);
|
|
176
240
|
const onMouseEnter = () => {
|
|
177
241
|
if (open)
|
|
178
242
|
return;
|
|
@@ -186,24 +250,24 @@ const MenuItemWrapper = ({ item, index, update, className, isLeftAligned, leftPo
|
|
|
186
250
|
data: viewItem,
|
|
187
251
|
className,
|
|
188
252
|
update,
|
|
189
|
-
}) ?? (_jsx(MenuElement, { toLeft: isLeftAligned, data: viewItem, className: className, update: update, open: open })), _jsxs("div", { children: [open && childMenu.length > 0 && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { data: childMenu, coordinate: {
|
|
253
|
+
}) ?? (_jsx(MenuElement, { toLeft: isLeftAligned, data: viewItem, className: className, update: update, open: open, onActionEvent: onActionEvent })), _jsxs("div", { children: [open && childMenu.length > 0 && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { data: childMenu, coordinate: {
|
|
190
254
|
x: 3,
|
|
191
255
|
y: 0,
|
|
192
256
|
toLeft: isLeftAligned,
|
|
193
257
|
left: leftPos,
|
|
194
|
-
} }) })), open && asyncFuncElement && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { menu: () => asyncFuncElement, data: [], coordinate: {
|
|
258
|
+
}, onActionEvent: onActionEvent }) })), open && asyncFuncElement && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { menu: () => asyncFuncElement, data: [], coordinate: {
|
|
195
259
|
x: 3,
|
|
196
260
|
y: 0,
|
|
197
261
|
toLeft: isLeftAligned,
|
|
198
262
|
left: leftPos,
|
|
199
|
-
} }) })), open && onFocusMenu.length > 0 && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { data: onFocusMenu, coordinate: {
|
|
263
|
+
}, onActionEvent: onActionEvent }) })), open && onFocusMenu.length > 0 && (_jsx("div", { style: { position: "relative" }, children: _jsx(Menu, { data: onFocusMenu, coordinate: {
|
|
200
264
|
x: 3,
|
|
201
265
|
y: 0,
|
|
202
266
|
toLeft: isLeftAligned,
|
|
203
267
|
left: leftPos,
|
|
204
|
-
} }) }))] })] }));
|
|
268
|
+
}, onActionEvent: onActionEvent }) }))] })] }));
|
|
205
269
|
};
|
|
206
|
-
export function Menu({ coordinate = { x: 0, y: 0, toLeft: false, left: 0 }, data, zIndex, menu, className, menuElement, }) {
|
|
270
|
+
export function Menu({ coordinate = { x: 0, y: 0, toLeft: false, left: 0 }, data, zIndex, menu, className, menuElement, onActionEvent, }) {
|
|
207
271
|
const [, forceUpdate] = useState(false);
|
|
208
272
|
const update = () => forceUpdate((p) => !p);
|
|
209
273
|
const refMenu = useRef(null);
|
|
@@ -257,6 +321,6 @@ export function Menu({ coordinate = { x: 0, y: 0, toLeft: false, left: 0 }, data
|
|
|
257
321
|
...alignStyle,
|
|
258
322
|
}, children: menu
|
|
259
323
|
? menu(dataMemo)
|
|
260
|
-
: dataMemo.map((item, i, arr) => (_jsx(MenuItemWrapper, { item: item, index: i, update: update, className: className, isLeftAligned: isLeftAligned, leftPos: leftPos, menuElement: menuElement, open: activeIndex === i, setOpenIndex: setActiveIndex }, typeof item.name === "string" ? item.name : i))) }));
|
|
324
|
+
: dataMemo.map((item, i, arr) => (_jsx(MenuItemWrapper, { item: item, index: i, update: update, className: className, isLeftAligned: isLeftAligned, leftPos: leftPos, menuElement: menuElement, open: activeIndex === i, setOpenIndex: setActiveIndex, onActionEvent: onActionEvent }, typeof item.name === "string" ? item.name : i))) }));
|
|
261
325
|
}
|
|
262
326
|
export { MenuProgress, MenuElement };
|
|
@@ -28,6 +28,22 @@ export type ContextMenuLayerProps = {
|
|
|
28
28
|
onConsume?: () => void;
|
|
29
29
|
className?: (active?: boolean) => string;
|
|
30
30
|
};
|
|
31
|
+
export type ContextMenuActionCounters = {
|
|
32
|
+
click: number;
|
|
33
|
+
ok: number;
|
|
34
|
+
error: number;
|
|
35
|
+
taskOk: number;
|
|
36
|
+
taskError: number;
|
|
37
|
+
submenuOpen: number;
|
|
38
|
+
submenuOk: number;
|
|
39
|
+
submenuError: number;
|
|
40
|
+
funcOpen: number;
|
|
41
|
+
funcOk: number;
|
|
42
|
+
funcError: number;
|
|
43
|
+
focusOpen: number;
|
|
44
|
+
focusOk: number;
|
|
45
|
+
focusError: number;
|
|
46
|
+
};
|
|
31
47
|
export type ContextMenuStatsSnapshot = {
|
|
32
48
|
openAt: number;
|
|
33
49
|
openAtPoint: number;
|
|
@@ -37,6 +53,8 @@ export type ContextMenuStatsSnapshot = {
|
|
|
37
53
|
empty: number;
|
|
38
54
|
sources: Record<string, number>;
|
|
39
55
|
layers: Record<string, number>;
|
|
56
|
+
actionTotals: ContextMenuActionCounters;
|
|
57
|
+
actions: Record<string, ContextMenuActionCounters>;
|
|
40
58
|
};
|
|
41
59
|
export type ContextMenuStats = {
|
|
42
60
|
getSnapshot(): ContextMenuStatsSnapshot;
|
|
@@ -2,6 +2,33 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Menu } from "./menu";
|
|
4
4
|
import { OutsideClickArea } from "../hooks/useOutside";
|
|
5
|
+
function createActionCounters() {
|
|
6
|
+
return {
|
|
7
|
+
click: 0,
|
|
8
|
+
ok: 0,
|
|
9
|
+
error: 0,
|
|
10
|
+
taskOk: 0,
|
|
11
|
+
taskError: 0,
|
|
12
|
+
submenuOpen: 0,
|
|
13
|
+
submenuOk: 0,
|
|
14
|
+
submenuError: 0,
|
|
15
|
+
funcOpen: 0,
|
|
16
|
+
funcOk: 0,
|
|
17
|
+
funcError: 0,
|
|
18
|
+
focusOpen: 0,
|
|
19
|
+
focusOk: 0,
|
|
20
|
+
focusError: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function cloneActionCounters(counters) {
|
|
24
|
+
return { ...counters };
|
|
25
|
+
}
|
|
26
|
+
function cloneActions(actions) {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, counters] of Object.entries(actions))
|
|
29
|
+
result[key] = cloneActionCounters(counters);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
5
32
|
function normalizeItems(items) {
|
|
6
33
|
return (items ?? []).filter(Boolean);
|
|
7
34
|
}
|
|
@@ -42,6 +69,8 @@ export function createContextMenu(data) {
|
|
|
42
69
|
empty: 0,
|
|
43
70
|
sources: {},
|
|
44
71
|
layers: {},
|
|
72
|
+
actionTotals: createActionCounters(),
|
|
73
|
+
actions: {},
|
|
45
74
|
};
|
|
46
75
|
const layers = new Set();
|
|
47
76
|
let layerSeq = 0;
|
|
@@ -64,6 +93,8 @@ export function createContextMenu(data) {
|
|
|
64
93
|
empty: statsState.empty,
|
|
65
94
|
sources: { ...statsState.sources },
|
|
66
95
|
layers: { ...statsState.layers },
|
|
96
|
+
actionTotals: cloneActionCounters(statsState.actionTotals),
|
|
97
|
+
actions: cloneActions(statsState.actions),
|
|
67
98
|
};
|
|
68
99
|
}
|
|
69
100
|
function emitStats() {
|
|
@@ -80,6 +111,15 @@ export function createContextMenu(data) {
|
|
|
80
111
|
return;
|
|
81
112
|
map[key] = (map[key] ?? 0) + 1;
|
|
82
113
|
}
|
|
114
|
+
function recordMenuAction(event) {
|
|
115
|
+
const stat = event.type;
|
|
116
|
+
statsState.actionTotals[stat] += 1;
|
|
117
|
+
if (event.actionKey) {
|
|
118
|
+
const counters = statsState.actions[event.actionKey] ??= createActionCounters();
|
|
119
|
+
counters[stat] += 1;
|
|
120
|
+
}
|
|
121
|
+
emitStats();
|
|
122
|
+
}
|
|
83
123
|
const stats = {
|
|
84
124
|
getSnapshot: statsSnapshot,
|
|
85
125
|
reset() {
|
|
@@ -91,6 +131,8 @@ export function createContextMenu(data) {
|
|
|
91
131
|
statsState.empty = 0;
|
|
92
132
|
statsState.sources = {};
|
|
93
133
|
statsState.layers = {};
|
|
134
|
+
statsState.actionTotals = createActionCounters();
|
|
135
|
+
statsState.actions = {};
|
|
94
136
|
emitStats();
|
|
95
137
|
},
|
|
96
138
|
onChange(cb) {
|
|
@@ -249,7 +291,7 @@ export function createContextMenu(data) {
|
|
|
249
291
|
if (!state.open || hasQueuedItems(other))
|
|
250
292
|
openQueued(event);
|
|
251
293
|
}
|
|
252
|
-
}, children: [children, state.open && enabled && state.layerId == layerId && _jsx(OutsideClickArea, { outsideClick: handleClose, children: _jsx(Menu, { className: className, data: state.items, coordinate: relativePoint(), zIndex: zIndex }) })] });
|
|
294
|
+
}, children: [children, state.open && enabled && state.layerId == layerId && _jsx(OutsideClickArea, { outsideClick: handleClose, children: _jsx(Menu, { className: className, data: state.items, coordinate: relativePoint(), zIndex: zIndex, onActionEvent: recordMenuAction }) })] });
|
|
253
295
|
}
|
|
254
296
|
return {
|
|
255
297
|
bb,
|
|
@@ -10,19 +10,20 @@ export declare const tokens: {
|
|
|
10
10
|
readonly borderCommon: "rgb(50, 62, 71)";
|
|
11
11
|
};
|
|
12
12
|
readonly menu: {
|
|
13
|
-
readonly bgColor: "
|
|
14
|
-
readonly blur: "
|
|
13
|
+
readonly bgColor: "#0c0c0c";
|
|
14
|
+
readonly blur: "0px";
|
|
15
15
|
readonly border: "1px solid rgba(255, 255, 255, 0.41)";
|
|
16
|
-
readonly activeBackground: "
|
|
16
|
+
readonly activeBackground: "#262626";
|
|
17
17
|
readonly itemColor: "#fff";
|
|
18
|
+
readonly itemActiveBgColor: "#262626";
|
|
19
|
+
readonly itemActiveColor: "#fff";
|
|
18
20
|
readonly itemHoverColor: "#101010";
|
|
19
21
|
readonly itemHoverBgColor: "#fff";
|
|
22
|
+
readonly itemPressedColor: "#101010";
|
|
23
|
+
readonly itemPressedBgColor: "#f2c94c";
|
|
20
24
|
readonly outlineColor: "#007bff";
|
|
21
25
|
readonly shadow: "0 0 20px 14px rgba(34, 60, 80, 0.2)";
|
|
22
26
|
};
|
|
23
|
-
/** FloatingWindow window chrome (--wnd-*). Defaults = legacy look; apps re-skin via :root[data-theme].
|
|
24
|
-
* --wnd-header-height/color/font-size/letter-spacing/transform are intentionally undeclared
|
|
25
|
-
* (inherit/auto) until a theme sets them. */
|
|
26
27
|
readonly wnd: {
|
|
27
28
|
readonly bg: "transparent";
|
|
28
29
|
readonly border: "none";
|