@monolith-forensics/monolith-ui 1.5.0 → 1.5.2-dev.0

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.
@@ -381,7 +381,7 @@ triggerFocus = false, triggerOpen = false, onFocused, onOpened, }) => {
381
381
  // ============================================================================
382
382
  // Render
383
383
  // ============================================================================
384
- return (_jsxs(StyledContainer, { className: className, children: [label && (_jsx(FieldLabel, { error: error, asterisk: required, size: size, description: description, children: label })), _jsxs(StyledInputContainer, { ref: refs.setReference, onMouseDown: () => setIsOpen(true), width: width, onKeyDown: handleKeyDown, "data-open": isOpen, "data-disabled": disabled, tabIndex: -1, children: [_jsx(Input, { ref: inputRef, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, autoFocus: focused, disabled: disabled, tabIndex: disabled ? -1 : 0, placeholder: placeholder, size: size, readOnly: !searchable && !allowCustomValue, "data-button-right": arrow || clearable, style: isOpen ? { borderColor: theme.palette.primary.main } : {} }), renderActionButton()] }), isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, { ref: (ref) => {
384
+ return (_jsxs(StyledContainer, { className: className, children: [label && (_jsx(FieldLabel, { error: error, asterisk: required, size: size, description: description, children: label })), _jsxs(StyledInputContainer, { ref: refs.setReference, onMouseDown: () => !disabled && setIsOpen(true), width: width, onKeyDown: handleKeyDown, "data-open": isOpen, "data-disabled": disabled, tabIndex: -1, children: [_jsx(Input, { ref: inputRef, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, autoFocus: focused, disabled: disabled, tabIndex: disabled ? -1 : 0, placeholder: placeholder, size: size, readOnly: !searchable && !allowCustomValue, "data-button-right": arrow || clearable, style: isOpen ? { borderColor: theme.palette.primary.main } : {} }), renderActionButton()] }), isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, { ref: (ref) => {
385
385
  containerRef.current = ref;
386
386
  refs.setFloating(ref);
387
387
  }, style: floatingStyles, className: "mfFloating", children: _jsxs(StyledContent, Object.assign({ className: "mfFloatingContent", style: {
@@ -34,7 +34,7 @@ export const StyledInputContainer = styled.div `
34
34
 
35
35
  &[data-disabled="true"] {
36
36
  opacity: 0.5;
37
- pointer-events: none;
37
+ cursor: not-allowed;
38
38
 
39
39
  > * {
40
40
  pointer-events: none;
@@ -11,6 +11,8 @@ type GetSearchStateFn = (key: string) => string;
11
11
  type SetSearchStateFn = (key: string, value: string) => void;
12
12
  type GetFilterStateFn = (key: string) => Query | undefined;
13
13
  type SetFilterStateFn = (key: string, value?: Query | null) => void;
14
+ type GetExpandedKeysFn = (key: string) => string[] | undefined;
15
+ type SetExpandedKeysFn = (key: string, value: string[]) => void;
14
16
  declare const StateStorage: {
15
17
  getTableState: GetTableStateFn;
16
18
  getColumnState: GetColumnStateFn;
@@ -23,5 +25,7 @@ declare const StateStorage: {
23
25
  setSearchState: SetSearchStateFn;
24
26
  getFilterState: GetFilterStateFn;
25
27
  setFilterState: SetFilterStateFn;
28
+ getExpandedKeys: GetExpandedKeysFn;
29
+ setExpandedKeys: SetExpandedKeysFn;
26
30
  };
27
31
  export default StateStorage;
@@ -82,6 +82,17 @@ const setFilterState = (key, value) => {
82
82
  const newState = Object.assign(Object.assign({}, previousState), { filterState: value });
83
83
  set(key, newState);
84
84
  };
85
+ const getExpandedKeys = (key) => {
86
+ const data = getTableState(key);
87
+ return data.expandedKeys;
88
+ };
89
+ const setExpandedKeys = (key, value) => {
90
+ // get previous state
91
+ const previousState = getTableState(key);
92
+ // merge previous state with new state
93
+ const newState = Object.assign(Object.assign({}, previousState), { expandedKeys: value });
94
+ set(key, newState);
95
+ };
85
96
  const StateStorage = {
86
97
  getTableState,
87
98
  getColumnState,
@@ -94,5 +105,7 @@ const StateStorage = {
94
105
  setSearchState,
95
106
  getFilterState,
96
107
  setFilterState,
108
+ getExpandedKeys,
109
+ setExpandedKeys,
97
110
  };
98
111
  export default StateStorage;
@@ -22,6 +22,7 @@ import TableMenu from "./TableMenu";
22
22
  import useTable from "./useTable";
23
23
  import LoadingIndicator from "./LoadingIndicator";
24
24
  import styled from "styled-components";
25
+ import TableDefaults from "./TableDefaults";
25
26
  const StyledTableContainer = styled.div `
26
27
  display: flex;
27
28
  flex-direction: column;
@@ -40,8 +41,20 @@ const TableContent = ({ children, }) => {
40
41
  }, ref: tableElement, "data-compact": compactState, children: [_jsx(LoadingIndicator, { visible: loading }), _jsx(TableHeader, { headerRowElm: headerRowElm }), visibleColumnCount === 0 && _jsx("div", { children: "No columns visible" }), virtualized === true ? (_jsx(VirtualizedRows, { tableDimensions: tableDimensions, targetElm: targetElm, listElm: listElm, rowHeight: rowHeight, headerRowHeight: headerRowHeight })) : (_jsx(StaticRows, { targetElm: targetElm, listElm: listElm }))] })] }));
41
42
  };
42
43
  export const Table = (_a) => {
43
- var { data, columnProps, children } = _a, props = __rest(_a, ["data", "columnProps", "children"]) // pass through props straight to table context
44
+ var { data, columnProps, children, treeOptions } = _a, props = __rest(_a, ["data", "columnProps", "children", "treeOptions"]) // pass through props straight to table context
44
45
  ;
46
+ const resolvedTreeOptions = useMemo(() => {
47
+ var _a, _b, _c, _d, _e, _f;
48
+ return ({
49
+ enabled: (_a = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.enabled) !== null && _a !== void 0 ? _a : false,
50
+ mode: (_b = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.mode) !== null && _b !== void 0 ? _b : TableDefaults.tree.defaultMode,
51
+ childrenField: (_c = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.childrenField) !== null && _c !== void 0 ? _c : TableDefaults.tree.defaultChildrenField,
52
+ parentIdField: (_d = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.parentIdField) !== null && _d !== void 0 ? _d : TableDefaults.tree.defaultParentIdField,
53
+ idField: treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.idField,
54
+ indentPx: (_e = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.indentPx) !== null && _e !== void 0 ? _e : TableDefaults.tree.indentPx,
55
+ autoExpandOnMatch: (_f = treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.autoExpandOnMatch) !== null && _f !== void 0 ? _f : true,
56
+ });
57
+ }, [treeOptions]);
45
58
  const tableElement = useRef(null);
46
59
  const targetElm = useRef(null);
47
60
  const listElm = useRef(null);
@@ -113,18 +126,153 @@ export const Table = (_a) => {
113
126
  // Uses a WeakMap so entries are automatically garbage collected
114
127
  // when their corresponding row objects are no longer referenced.
115
128
  const uuidCache = useRef(new WeakMap());
116
- // Augment each row with a stable __key (UUID) and __index.
117
- // The WeakMap ensures that the same row object always receives
118
- // the same UUID across re-renders, preventing unnecessary DOM
119
- // reconciliation. useMemo preserves referential stability of
120
- // the output array when the data reference hasn't changed.
121
- const __data = useMemo(() => data === null || data === void 0 ? void 0 : data.map((d, i) => {
122
- let key = uuidCache.current.get(d);
129
+ const getOrAssignKey = (row) => {
130
+ let key = uuidCache.current.get(row);
123
131
  if (!key) {
124
132
  key = shortUUID.uuid();
125
- uuidCache.current.set(d, key);
133
+ uuidCache.current.set(row, key);
134
+ }
135
+ return key;
136
+ };
137
+ // Augment each row with stable metadata. When tree mode is disabled,
138
+ // produces a flat 1:1 mapping with __key and __index. When enabled,
139
+ // walks the input (nested or flat) into a flat depth-first array
140
+ // and adds tree metadata (__level, __parentKey, __hasChildren, __childKeys).
141
+ const { rows: __data, treeMeta } = useMemo(() => {
142
+ const parentKeyMap = new Map();
143
+ const childrenKeyMap = new Map();
144
+ const expandableKeys = [];
145
+ if (!data) {
146
+ return {
147
+ rows: [],
148
+ treeMeta: { parentKeyMap, childrenKeyMap, expandableKeys },
149
+ };
150
+ }
151
+ if (!resolvedTreeOptions.enabled) {
152
+ const rows = data.map((d, i) => (Object.assign(Object.assign({}, d), { __key: getOrAssignKey(d), __index: i })));
153
+ return {
154
+ rows,
155
+ treeMeta: { parentKeyMap, childrenKeyMap, expandableKeys },
156
+ };
126
157
  }
127
- return Object.assign(Object.assign({}, d), { __key: key, __index: i });
128
- }), [data]);
129
- return (_jsx(TableProvider, Object.assign({ columns: columnProps, data: __data, tableElement: tableElement, headerRowElm: headerRowElm, tableDimensions: tableDimensions, targetElm: targetElm, listElm: listElm }, props, { children: _jsx(TableContent, { children: children }) })));
158
+ const rows = [];
159
+ let counter = 0;
160
+ if (resolvedTreeOptions.mode === "nested") {
161
+ const childrenField = resolvedTreeOptions.childrenField;
162
+ const walk = (node, level, parentKey) => {
163
+ const key = getOrAssignKey(node);
164
+ const rawChildren = node === null || node === void 0 ? void 0 : node[childrenField];
165
+ const children = Array.isArray(rawChildren) ? rawChildren : [];
166
+ const childKeys = children.map((c) => getOrAssignKey(c));
167
+ // Strip the nested children field from the augmented copy to avoid
168
+ // memory bloat and prevent accidental iteration of the raw tree.
169
+ const _a = node, _b = childrenField, _stripped = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
170
+ const augmented = Object.assign(Object.assign({}, rest), { __key: key, __index: counter++, __level: level, __parentKey: parentKey, __hasChildren: children.length > 0, __childKeys: childKeys });
171
+ rows.push(augmented);
172
+ parentKeyMap.set(key, parentKey);
173
+ if (children.length > 0) {
174
+ childrenKeyMap.set(key, childKeys);
175
+ expandableKeys.push(key);
176
+ }
177
+ for (const child of children) {
178
+ walk(child, level + 1, key);
179
+ }
180
+ };
181
+ const rootKeys = [];
182
+ for (const root of data) {
183
+ rootKeys.push(getOrAssignKey(root));
184
+ walk(root, 0, undefined);
185
+ }
186
+ childrenKeyMap.set(undefined, rootKeys);
187
+ }
188
+ else {
189
+ // flat mode
190
+ const idField = resolvedTreeOptions.idField || props.keyField;
191
+ if (!idField) {
192
+ throw new Error('Table treeOptions.mode="flat" requires either treeOptions.idField or keyField to be set so the table can resolve parent-child relationships against a stable id.');
193
+ }
194
+ const parentIdField = resolvedTreeOptions.parentIdField;
195
+ // Pass 1: assign __key, build idToKey
196
+ const idToKey = new Map();
197
+ const idToRow = new Map();
198
+ for (const d of data) {
199
+ const id = d === null || d === void 0 ? void 0 : d[idField];
200
+ if (id === undefined || id === null)
201
+ continue;
202
+ idToKey.set(id, getOrAssignKey(d));
203
+ idToRow.set(id, d);
204
+ }
205
+ // Pass 2: build parent->children id map. Roots = parentId null/undefined
206
+ // OR parentId points at a non-existent row.
207
+ const parentToChildIds = new Map();
208
+ const rootIds = [];
209
+ let warnedOrphan = false;
210
+ for (const d of data) {
211
+ const id = d === null || d === void 0 ? void 0 : d[idField];
212
+ if (id === undefined || id === null)
213
+ continue;
214
+ const parentId = d === null || d === void 0 ? void 0 : d[parentIdField];
215
+ if (parentId === undefined || parentId === null) {
216
+ rootIds.push(id);
217
+ continue;
218
+ }
219
+ if (!idToRow.has(parentId)) {
220
+ if (!warnedOrphan) {
221
+ // eslint-disable-next-line no-console
222
+ console.warn(`Table tree (flat mode): row id "${id}" references missing parentId "${parentId}"; treating as root.`);
223
+ warnedOrphan = true;
224
+ }
225
+ rootIds.push(id);
226
+ continue;
227
+ }
228
+ if (!parentToChildIds.has(parentId))
229
+ parentToChildIds.set(parentId, []);
230
+ parentToChildIds.get(parentId).push(id);
231
+ }
232
+ // Pass 3: DFS from roots with cycle detection
233
+ const visiting = new Set();
234
+ let warnedCycle = false;
235
+ const walkFlat = (id, level, parentKey) => {
236
+ if (visiting.has(id)) {
237
+ if (!warnedCycle) {
238
+ // eslint-disable-next-line no-console
239
+ console.warn(`Table tree (flat mode): cycle detected at row id "${id}"; truncating subtree.`);
240
+ warnedCycle = true;
241
+ }
242
+ return;
243
+ }
244
+ visiting.add(id);
245
+ const node = idToRow.get(id);
246
+ const key = idToKey.get(id);
247
+ const childIds = parentToChildIds.get(id) || [];
248
+ const childKeys = childIds
249
+ .map((cid) => idToKey.get(cid))
250
+ .filter((k) => !!k);
251
+ const augmented = Object.assign(Object.assign({}, node), { __key: key, __index: counter++, __level: level, __parentKey: parentKey, __hasChildren: childIds.length > 0, __childKeys: childKeys });
252
+ rows.push(augmented);
253
+ parentKeyMap.set(key, parentKey);
254
+ if (childIds.length > 0) {
255
+ childrenKeyMap.set(key, childKeys);
256
+ expandableKeys.push(key);
257
+ }
258
+ for (const cid of childIds) {
259
+ walkFlat(cid, level + 1, key);
260
+ }
261
+ visiting.delete(id);
262
+ };
263
+ const rootKeys = [];
264
+ for (const id of rootIds) {
265
+ const k = idToKey.get(id);
266
+ if (k)
267
+ rootKeys.push(k);
268
+ walkFlat(id, 0, undefined);
269
+ }
270
+ childrenKeyMap.set(undefined, rootKeys);
271
+ }
272
+ return {
273
+ rows,
274
+ treeMeta: { parentKeyMap, childrenKeyMap, expandableKeys },
275
+ };
276
+ }, [data, resolvedTreeOptions, props.keyField]);
277
+ return (_jsx(TableProvider, Object.assign({ columns: columnProps, data: __data, tableElement: tableElement, headerRowElm: headerRowElm, tableDimensions: tableDimensions, targetElm: targetElm, listElm: listElm, treeOptions: resolvedTreeOptions, treeMeta: treeMeta }, props, { children: _jsx(TableContent, { children: children }) })));
130
278
  };
@@ -6,6 +6,16 @@ export declare const THR: import("styled-components/dist/types").IStyledComponen
6
6
  export declare const TD: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, TDProps>> & string;
7
7
  export declare const TH: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof TDProps> & TDProps, never>> & string;
8
8
  export declare const InnerCellContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
9
+ export declare const TreeCellWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
10
+ export declare const TreeIndentSpacer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
11
+ $level: number;
12
+ $indentPx: number;
13
+ }>> & string;
14
+ export declare const TreeChevronButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, {
15
+ $expanded: boolean;
16
+ }>> & string;
17
+ export declare const TreeChevronPlaceholder: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
18
+ export declare const TreeCellContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
9
19
  export declare const TableViewPort: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
10
20
  export declare const TableListElement: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
11
21
  export declare const TableWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
@@ -115,6 +115,55 @@ export const InnerCellContent = styled.div `
115
115
  overflow: hidden;
116
116
  text-overflow: ellipsis;
117
117
  `;
118
+ export const TreeCellWrapper = styled.div `
119
+ display: flex;
120
+ flex-direction: row;
121
+ align-items: center;
122
+ flex: 1 1 auto;
123
+ min-width: 0;
124
+ gap: 4px;
125
+ `;
126
+ export const TreeIndentSpacer = styled.div `
127
+ flex: 0 0 auto;
128
+ width: ${({ $level, $indentPx }) => $level * $indentPx}px;
129
+ `;
130
+ export const TreeChevronButton = styled.button `
131
+ flex: 0 0 auto;
132
+ width: 16px;
133
+ height: 16px;
134
+ padding: 0;
135
+ margin: 0;
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ background: transparent;
140
+ border: none;
141
+ cursor: pointer;
142
+ color: ${({ theme }) => theme.palette.text.secondary};
143
+ transition: transform 120ms ease;
144
+ transform: rotate(${({ $expanded }) => ($expanded ? "90deg" : "0deg")});
145
+
146
+ &:hover {
147
+ color: ${({ theme }) => theme.palette.text.primary};
148
+ }
149
+
150
+ &:focus-visible {
151
+ outline: 1px solid ${({ theme }) => theme.palette.primary.main};
152
+ outline-offset: 1px;
153
+ }
154
+ `;
155
+ export const TreeChevronPlaceholder = styled.div `
156
+ flex: 0 0 auto;
157
+ width: 16px;
158
+ height: 16px;
159
+ `;
160
+ export const TreeCellContent = styled.div `
161
+ flex: 1 1 auto;
162
+ min-width: 0;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ white-space: nowrap;
166
+ `;
118
167
  export const TableViewPort = styled.div `
119
168
  display: flex;
120
169
  flex-direction: column;
@@ -21,5 +21,12 @@ declare const TableDefaults: {
21
21
  minWidth: number;
22
22
  maxWidth: number;
23
23
  };
24
+ tree: {
25
+ indentPx: number;
26
+ chevronWidth: number;
27
+ defaultMode: "nested";
28
+ defaultChildrenField: string;
29
+ defaultParentIdField: string;
30
+ };
24
31
  };
25
32
  export default TableDefaults;
@@ -21,5 +21,12 @@ const TableDefaults = {
21
21
  minWidth: 35,
22
22
  maxWidth: 35,
23
23
  },
24
+ tree: {
25
+ indentPx: 16,
26
+ chevronWidth: 16,
27
+ defaultMode: "nested",
28
+ defaultChildrenField: "children",
29
+ defaultParentIdField: "parentId",
30
+ },
24
31
  };
25
32
  export default TableDefaults;
@@ -28,6 +28,27 @@ import { SelectionStatus } from "./enums";
28
28
  import moment from "moment";
29
29
  import { useControlled } from "../Utilities";
30
30
  import { exportTableToExcel } from "./Utils";
31
+ const RESERVED_FIELDS = [
32
+ "__key",
33
+ "__index",
34
+ "__level",
35
+ "__parentKey",
36
+ "__hasChildren",
37
+ "__childKeys",
38
+ ];
39
+ const EMPTY_TREE_META = {
40
+ parentKeyMap: new Map(),
41
+ childrenKeyMap: new Map(),
42
+ expandableKeys: [],
43
+ };
44
+ const DEFAULT_TREE_OPTIONS = {
45
+ enabled: false,
46
+ mode: "nested",
47
+ childrenField: "children",
48
+ parentIdField: "parentId",
49
+ indentPx: 16,
50
+ autoExpandOnMatch: true,
51
+ };
31
52
  const calculateSelectionTotal = (selectionState, totalRecords, dataLength = 0) => {
32
53
  if (!selectionState) {
33
54
  return 0;
@@ -47,11 +68,21 @@ const calculateSelectionTotal = (selectionState, totalRecords, dataLength = 0) =
47
68
  };
48
69
  export const TableContext = createContext(null);
49
70
  const TableProvider = (_a) => {
50
- var { children, columns, data, keyField, tableInstanceRef, stateStorage, tableMenuOptions, manualSorting, manualFiltering, manualSearch, manualExport, height, maxHeight, minHeight, focusedRowId, enableColumnResize, enableSorting, compact, totalRecords, onColumnStateChange, onColumnReorder, onColumnHeaderClick, onSort, onRowUpdated, tableElement, headerRowElm, tableDimensions, targetElm, listElm, defaultSelectionState, selectionState, onSelectionChange, defaultFilterState, filterState, onFilterChange } = _a, props = __rest(_a, ["children", "columns", "data", "keyField", "tableInstanceRef", "stateStorage", "tableMenuOptions", "manualSorting", "manualFiltering", "manualSearch", "manualExport", "height", "maxHeight", "minHeight", "focusedRowId", "enableColumnResize", "enableSorting", "compact", "totalRecords", "onColumnStateChange", "onColumnReorder", "onColumnHeaderClick", "onSort", "onRowUpdated", "tableElement", "headerRowElm", "tableDimensions", "targetElm", "listElm", "defaultSelectionState", "selectionState", "onSelectionChange", "defaultFilterState", "filterState", "onFilterChange"]);
71
+ var { children, columns, data, keyField, tableInstanceRef, stateStorage, tableMenuOptions, manualSorting, manualFiltering, manualSearch, manualExport, height, maxHeight, minHeight, focusedRowId, enableColumnResize, enableSorting, compact, totalRecords, onColumnStateChange, onColumnReorder, onColumnHeaderClick, onSort, onRowUpdated, tableElement, headerRowElm, tableDimensions, targetElm, listElm, defaultSelectionState, selectionState, onSelectionChange, defaultFilterState, filterState, onFilterChange, treeOptions, treeMeta, defaultExpandedKeys, expandedKeys, onExpandedChange } = _a, props = __rest(_a, ["children", "columns", "data", "keyField", "tableInstanceRef", "stateStorage", "tableMenuOptions", "manualSorting", "manualFiltering", "manualSearch", "manualExport", "height", "maxHeight", "minHeight", "focusedRowId", "enableColumnResize", "enableSorting", "compact", "totalRecords", "onColumnStateChange", "onColumnReorder", "onColumnHeaderClick", "onSort", "onRowUpdated", "tableElement", "headerRowElm", "tableDimensions", "targetElm", "listElm", "defaultSelectionState", "selectionState", "onSelectionChange", "defaultFilterState", "filterState", "onFilterChange", "treeOptions", "treeMeta", "defaultExpandedKeys", "expandedKeys", "onExpandedChange"]);
72
+ const _treeOptions = treeOptions || DEFAULT_TREE_OPTIONS;
73
+ const _treeMeta = treeMeta || EMPTY_TREE_META;
74
+ if (_treeOptions.enabled) {
75
+ if (RESERVED_FIELDS.includes(_treeOptions.childrenField)) {
76
+ throw new Error(`treeOptions.childrenField cannot be a reserved internal field: "${_treeOptions.childrenField}".`);
77
+ }
78
+ if (RESERVED_FIELDS.includes(_treeOptions.parentIdField)) {
79
+ throw new Error(`treeOptions.parentIdField cannot be a reserved internal field: "${_treeOptions.parentIdField}".`);
80
+ }
81
+ }
51
82
  const _columns = useMemo(() => columns
52
83
  .map((child, index) => {
53
- if (child.dataField === "__key") {
54
- throw new Error("dataField cannot be __key");
84
+ if (RESERVED_FIELDS.includes(child.dataField)) {
85
+ throw new Error(`dataField cannot be a reserved internal field: "${child.dataField}". Reserved: ${RESERVED_FIELDS.join(", ")}`);
55
86
  }
56
87
  // check for duplicate dataFields
57
88
  const dataFieldCount = columns.filter((col) => col.dataField === child.dataField).length;
@@ -70,7 +101,7 @@ const TableProvider = (_a) => {
70
101
  ? StateStorage.getTableState(stateStorage.key)
71
102
  : undefined;
72
103
  }, [stateStorage === null || stateStorage === void 0 ? void 0 : stateStorage.key]);
73
- const { columnState: savedColumnState, selectionState: savedSelectionState, sortState: savedSortState, searchState: savedSearchState, filterState: savedFilterState, } = savedTableState || {};
104
+ const { columnState: savedColumnState, selectionState: savedSelectionState, sortState: savedSortState, searchState: savedSearchState, filterState: savedFilterState, expandedKeys: savedExpandedKeys, } = savedTableState || {};
74
105
  const [compactState, setCompactState] = useState(compact || false);
75
106
  const [columnState, setColumnState] = useState(syncColumnState(_columns, savedColumnState));
76
107
  const [search, setSearch] = useState(savedSearchState || "");
@@ -80,6 +111,8 @@ const TableProvider = (_a) => {
80
111
  rules: [],
81
112
  });
82
113
  const [sortState, setSortState] = useState(savedSortState);
114
+ const [_expandedKeys, _setExpandedKeys] = useControlled(expandedKeys, defaultExpandedKeys || savedExpandedKeys || []);
115
+ const expandedKeysSet = useMemo(() => new Set(_expandedKeys), [_expandedKeys]);
83
116
  const [_selectionState, _setSelectionState] = useControlled(selectionState, defaultSelectionState || {
84
117
  selectedRowKeys: (savedSelectionState === null || savedSelectionState === void 0 ? void 0 : savedSelectionState.selectedRowKeys) || [],
85
118
  excludedRowKeys: (savedSelectionState === null || savedSelectionState === void 0 ? void 0 : savedSelectionState.excludedRowKeys) || [],
@@ -227,58 +260,99 @@ const TableProvider = (_a) => {
227
260
  }
228
261
  }
229
262
  };
230
- const sortData = (sortState) => {
231
- // sort data
232
- return data.sort((a, b) => {
233
- if (sortState) {
234
- const aValue = a[sortState.dataField];
235
- const bValue = b[sortState.dataField];
236
- const aIsEmpty = aValue === null || aValue === undefined;
237
- const bIsEmpty = bValue === null || bValue === undefined;
238
- // Treat empty values as the smallest values.
239
- if (aIsEmpty && bIsEmpty) {
240
- return 0;
241
- }
242
- if (aIsEmpty || bIsEmpty) {
243
- if (sortState.dir === "asc") {
244
- return aIsEmpty ? -1 : 1;
245
- }
246
- if (sortState.dir === "desc") {
247
- return aIsEmpty ? 1 : -1;
248
- }
249
- }
263
+ const compareRows = (a, b, sortState) => {
264
+ if (sortState) {
265
+ const aValue = a[sortState.dataField];
266
+ const bValue = b[sortState.dataField];
267
+ const aIsEmpty = aValue === null || aValue === undefined;
268
+ const bIsEmpty = bValue === null || bValue === undefined;
269
+ // Treat empty values as the smallest values.
270
+ if (aIsEmpty && bIsEmpty) {
271
+ return 0;
272
+ }
273
+ if (aIsEmpty || bIsEmpty) {
250
274
  if (sortState.dir === "asc") {
251
- if (aValue < bValue) {
252
- return -1;
253
- }
254
- if (aValue > bValue) {
255
- return 1;
256
- }
257
- return 0;
275
+ return aIsEmpty ? -1 : 1;
258
276
  }
259
- else if (sortState.dir === "desc") {
260
- if (aValue > bValue) {
261
- return -1;
262
- }
263
- if (aValue < bValue) {
264
- return 1;
265
- }
266
- return 0;
277
+ if (sortState.dir === "desc") {
278
+ return aIsEmpty ? 1 : -1;
267
279
  }
268
280
  }
269
- // sort by __index
270
- if (a.__index < b.__index) {
271
- return -1;
281
+ if (sortState.dir === "asc") {
282
+ if (aValue < bValue)
283
+ return -1;
284
+ if (aValue > bValue)
285
+ return 1;
286
+ return 0;
272
287
  }
273
- if (a.__index > b.__index) {
274
- return 1;
288
+ else if (sortState.dir === "desc") {
289
+ if (aValue > bValue)
290
+ return -1;
291
+ if (aValue < bValue)
292
+ return 1;
293
+ return 0;
275
294
  }
276
- return 0;
277
- });
295
+ }
296
+ // sort by __index
297
+ if (a.__index < b.__index)
298
+ return -1;
299
+ if (a.__index > b.__index)
300
+ return 1;
301
+ return 0;
302
+ };
303
+ const sortData = (sortState) => {
304
+ if (!_treeOptions.enabled) {
305
+ // Flat data — sort in-place (preserves existing behavior).
306
+ return data.sort((a, b) => compareRows(a, b, sortState));
307
+ }
308
+ // Tree-aware sort: sort siblings only, then re-DFS to rebuild the
309
+ // flat order. Cross-parent sorting is never allowed in tree mode.
310
+ const byKey = new Map();
311
+ const childrenByParent = new Map();
312
+ for (const row of data) {
313
+ byKey.set(row.__key, row);
314
+ const pk = row.__parentKey;
315
+ if (!childrenByParent.has(pk))
316
+ childrenByParent.set(pk, []);
317
+ childrenByParent.get(pk).push(row);
318
+ }
319
+ for (const [, siblings] of childrenByParent) {
320
+ siblings.sort((a, b) => compareRows(a, b, sortState));
321
+ }
322
+ const result = [];
323
+ const walk = (parentKey) => {
324
+ const siblings = childrenByParent.get(parentKey);
325
+ if (!siblings)
326
+ return;
327
+ for (const row of siblings) {
328
+ result.push(row);
329
+ if (row.__hasChildren)
330
+ walk(row.__key);
331
+ }
332
+ };
333
+ walk(undefined);
334
+ return result;
335
+ };
336
+ const collectAncestors = (rows) => {
337
+ const ancestorSet = new Set();
338
+ const byKey = new Map();
339
+ for (const r of data)
340
+ byKey.set(r.__key, r);
341
+ for (const row of rows) {
342
+ let parentKey = row.__parentKey;
343
+ while (parentKey) {
344
+ if (ancestorSet.has(parentKey))
345
+ break;
346
+ ancestorSet.add(parentKey);
347
+ const parent = byKey.get(parentKey);
348
+ parentKey = parent === null || parent === void 0 ? void 0 : parent.__parentKey;
349
+ }
350
+ }
351
+ return ancestorSet;
278
352
  };
279
- const searchData = (searchText) => {
353
+ const searchData = (rows, searchText) => {
280
354
  const columnKeys = columnState.map((col) => col.dataField);
281
- return data.filter((row) => {
355
+ const matched = rows.filter((row) => {
282
356
  return columnKeys.some((key) => {
283
357
  if (typeof row[key] === "string") {
284
358
  return row[key].toLowerCase().includes(searchText.toLowerCase());
@@ -286,6 +360,16 @@ const TableProvider = (_a) => {
286
360
  return false;
287
361
  });
288
362
  });
363
+ if (!_treeOptions.enabled) {
364
+ return { rows: matched, ancestorKeys: new Set() };
365
+ }
366
+ const ancestorKeys = collectAncestors(matched);
367
+ const visibleSet = new Set(matched.map((r) => r.__key));
368
+ ancestorKeys.forEach((k) => visibleSet.add(k));
369
+ return {
370
+ rows: rows.filter((r) => visibleSet.has(r.__key)),
371
+ ancestorKeys,
372
+ };
289
373
  };
290
374
  const rowMatchesRule = (row, rule) => {
291
375
  var _a, _b, _c, _d;
@@ -385,16 +469,26 @@ const TableProvider = (_a) => {
385
469
  return true;
386
470
  }
387
471
  };
388
- const filterData = (filter) => {
389
- if (!data)
390
- return [];
472
+ const filterData = (rows, filter) => {
473
+ if (!rows)
474
+ return { rows: [], ancestorKeys: new Set() };
391
475
  const { combinator, rules } = filter;
392
- if (!combinator || !rules)
393
- return data;
394
- if (combinator === "or") {
395
- return data.filter((row) => rules.some((rule) => rowMatchesRule(row, rule)));
476
+ if (!combinator || !rules || rules.length === 0) {
477
+ return { rows, ancestorKeys: new Set() };
396
478
  }
397
- return data.filter((row) => rules.every((rule) => rowMatchesRule(row, rule)));
479
+ const matched = combinator === "or"
480
+ ? rows.filter((row) => rules.some((rule) => rowMatchesRule(row, rule)))
481
+ : rows.filter((row) => rules.every((rule) => rowMatchesRule(row, rule)));
482
+ if (!_treeOptions.enabled) {
483
+ return { rows: matched, ancestorKeys: new Set() };
484
+ }
485
+ const ancestorKeys = collectAncestors(matched);
486
+ const visibleSet = new Set(matched.map((r) => r.__key));
487
+ ancestorKeys.forEach((k) => visibleSet.add(k));
488
+ return {
489
+ rows: rows.filter((r) => visibleSet.has(r.__key)),
490
+ ancestorKeys,
491
+ };
398
492
  };
399
493
  const toggleColumnVisibility = (dataField) => {
400
494
  const newColumnState = columnState.map((col) => {
@@ -423,6 +517,47 @@ const TableProvider = (_a) => {
423
517
  const key = !!keyField ? row[keyField] : row.__key;
424
518
  return String(key);
425
519
  };
520
+ const firstVisibleDataField = useMemo(() => { var _a; return (_a = columnState.find((c) => c.visible !== false)) === null || _a === void 0 ? void 0 : _a.dataField; }, [columnState]);
521
+ const persistExpandedKeys = (next) => {
522
+ if ((stateStorage === null || stateStorage === void 0 ? void 0 : stateStorage.enabled) && (stateStorage === null || stateStorage === void 0 ? void 0 : stateStorage.type) === "localStorage") {
523
+ StateStorage.setExpandedKeys(stateStorage.key, next);
524
+ }
525
+ };
526
+ const updateExpandedKeys = (next) => {
527
+ _setExpandedKeys(next);
528
+ onExpandedChange === null || onExpandedChange === void 0 ? void 0 : onExpandedChange(next);
529
+ persistExpandedKeys(next);
530
+ };
531
+ const expandRow = (row) => {
532
+ var _a;
533
+ const key = (_a = row === null || row === void 0 ? void 0 : row.__key) !== null && _a !== void 0 ? _a : getRowKey(row);
534
+ if (expandedKeysSet.has(key))
535
+ return;
536
+ updateExpandedKeys([..._expandedKeys, key]);
537
+ };
538
+ const collapseRow = (row) => {
539
+ var _a;
540
+ const key = (_a = row === null || row === void 0 ? void 0 : row.__key) !== null && _a !== void 0 ? _a : getRowKey(row);
541
+ if (!expandedKeysSet.has(key))
542
+ return;
543
+ updateExpandedKeys(_expandedKeys.filter((k) => k !== key));
544
+ };
545
+ const toggleRowExpanded = (row) => {
546
+ var _a;
547
+ const key = (_a = row === null || row === void 0 ? void 0 : row.__key) !== null && _a !== void 0 ? _a : getRowKey(row);
548
+ if (expandedKeysSet.has(key)) {
549
+ updateExpandedKeys(_expandedKeys.filter((k) => k !== key));
550
+ }
551
+ else {
552
+ updateExpandedKeys([..._expandedKeys, key]);
553
+ }
554
+ };
555
+ const expandAllRows = () => {
556
+ updateExpandedKeys([..._treeMeta.expandableKeys]);
557
+ };
558
+ const collapseAllRows = () => {
559
+ updateExpandedKeys([]);
560
+ };
426
561
  const selectRow = (row) => {
427
562
  const key = getRowKey(row);
428
563
  const newSelectionState = {
@@ -560,6 +695,61 @@ const TableProvider = (_a) => {
560
695
  const key = getRowKey(row);
561
696
  return focusedRowId === key;
562
697
  };
698
+ const { _data, effectiveExpandedKeys } = useMemo(() => {
699
+ let processedData = data || [];
700
+ if (manualSorting !== true) {
701
+ processedData = sortData(sortState);
702
+ }
703
+ const autoExpandedSet = new Set();
704
+ if (manualFiltering !== true && _filterState) {
705
+ const result = filterData(processedData, _filterState);
706
+ processedData = result.rows;
707
+ if (_treeOptions.enabled && _treeOptions.autoExpandOnMatch) {
708
+ result.ancestorKeys.forEach((k) => autoExpandedSet.add(k));
709
+ }
710
+ }
711
+ if (manualSearch !== true && search) {
712
+ const result = searchData(processedData, search);
713
+ processedData = result.rows;
714
+ if (_treeOptions.enabled && _treeOptions.autoExpandOnMatch) {
715
+ result.ancestorKeys.forEach((k) => autoExpandedSet.add(k));
716
+ }
717
+ }
718
+ // Compute the effective expanded set: user expansion ∪ auto-expanded ancestors.
719
+ // Never mutates user's _expandedKeys.
720
+ const effective = new Set(expandedKeysSet);
721
+ autoExpandedSet.forEach((k) => effective.add(k));
722
+ // Final pass: drop rows whose ancestors are not in the effective set.
723
+ if (_treeOptions.enabled) {
724
+ const byKey = new Map();
725
+ for (const r of processedData)
726
+ byKey.set(r.__key, r);
727
+ processedData = processedData.filter((row) => {
728
+ let pk = row.__parentKey;
729
+ while (pk) {
730
+ if (!effective.has(pk))
731
+ return false;
732
+ const parent = byKey.get(pk);
733
+ pk = parent === null || parent === void 0 ? void 0 : parent.__parentKey;
734
+ }
735
+ return true;
736
+ });
737
+ }
738
+ return { _data: processedData, effectiveExpandedKeys: effective };
739
+ }, [
740
+ data,
741
+ columnState,
742
+ search,
743
+ sortState,
744
+ _filterState,
745
+ expandedKeysSet,
746
+ _treeOptions,
747
+ ]);
748
+ const isRowExpanded = (row) => {
749
+ var _a;
750
+ const key = (_a = row === null || row === void 0 ? void 0 : row.__key) !== null && _a !== void 0 ? _a : getRowKey(row);
751
+ return effectiveExpandedKeys.has(key);
752
+ };
563
753
  if (tableInstanceRef) {
564
754
  tableInstanceRef.current = {
565
755
  columnState,
@@ -576,6 +766,13 @@ const TableProvider = (_a) => {
576
766
  clearSelections,
577
767
  runSearch,
578
768
  clearSearch,
769
+ expandRow,
770
+ collapseRow,
771
+ toggleRowExpanded,
772
+ isRowExpanded,
773
+ expandAllRows,
774
+ collapseAllRows,
775
+ getExpandedRowKeys: () => [..._expandedKeys],
579
776
  getTableState: () => {
580
777
  return {
581
778
  columnState,
@@ -587,24 +784,12 @@ const TableProvider = (_a) => {
587
784
  },
588
785
  sortState,
589
786
  searchState: search,
590
- _filterState,
787
+ filterState: _filterState,
788
+ expandedKeys: [..._expandedKeys],
591
789
  };
592
790
  },
593
791
  };
594
792
  }
595
- const _data = useMemo(() => {
596
- let processedData = data; // create a new array to avoid mutating the original data
597
- if (manualSorting !== true) {
598
- processedData = sortData(sortState);
599
- }
600
- if (manualFiltering !== true && _filterState) {
601
- processedData = filterData(_filterState);
602
- }
603
- if (manualSearch !== true && search) {
604
- processedData = searchData(search);
605
- }
606
- return processedData;
607
- }, [data, columnState, search, sortState, _filterState]);
608
793
  return (_jsx(TableContext.Provider, { value: Object.assign({ columnState,
609
794
  setColumnState,
610
795
  sortState, searchState: search, totalRecords,
@@ -631,6 +816,13 @@ const TableProvider = (_a) => {
631
816
  stateStorage, tableHeight: height, tableMaxHeight: maxHeight, tableMinHeight: minHeight, compact, tableElement: tableElement, headerRowElm: headerRowElm, tableDimensions: tableDimensions, targetElm: targetElm, listElm: listElm, enableColumnResize,
632
817
  onSelectionChange,
633
818
  onColumnStateChange,
634
- onColumnReorder, onRowUpdated: onRowUpdated || (() => { }), tableMenuOptions, data: _data }, props), children: children }));
819
+ onColumnReorder, onRowUpdated: onRowUpdated || (() => { }), tableMenuOptions, data: _data, treeOptions: _treeOptions, firstVisibleDataField, expandedKeys: _expandedKeys, effectiveExpandedKeys,
820
+ isRowExpanded,
821
+ toggleRowExpanded,
822
+ expandRow,
823
+ collapseRow,
824
+ expandAllRows,
825
+ collapseAllRows,
826
+ onExpandedChange }, props), children: children }));
635
827
  };
636
828
  export default TableProvider;
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Maximize2Icon } from "lucide-react";
2
+ import { ChevronRightIcon, Maximize2Icon } from "lucide-react";
3
3
  import ColumnResizer from "./ColumnResizer";
4
- import { InnerCellContent, TD, TR } from "./TableComponents";
4
+ import { InnerCellContent, TD, TR, TreeCellContent, TreeCellWrapper, TreeChevronButton, TreeChevronPlaceholder, TreeIndentSpacer, } from "./TableComponents";
5
5
  import useTable from "./useTable";
6
6
  import ActionCell from "./ActionCell";
7
7
  import ActionButton from "./ActionButton";
8
8
  import CheckBox from "../CheckBox";
9
9
  import { LoadingCellIndicator } from "./LoadingCellIndicator";
10
10
  const TableRow = ({ rowData, loading, rowStyle }) => {
11
- const { columnState, enableActionButton, onActionButtonClick, actionButtonIcon: Icon, enableSelection, selectRow, deselectRow, isRowSelected, isRowFocused, onRowUpdated, } = useTable();
11
+ const { columnState, enableActionButton, onActionButtonClick, actionButtonIcon: Icon, enableSelection, selectRow, deselectRow, isRowSelected, isRowFocused, onRowUpdated, treeOptions, firstVisibleDataField, isRowExpanded, toggleRowExpanded, } = useTable();
12
12
  const selected = isRowSelected(rowData);
13
13
  const focused = isRowFocused(rowData);
14
14
  const handleSelectionChange = (e) => {
15
15
  e === true ? selectRow(rowData) : deselectRow(rowData);
16
16
  };
17
17
  return (_jsxs(TR, { className: "mfui-tr", style: rowStyle, "data-key": rowData.__key, "data-selected": selected, "data-focused": focused, children: [enableSelection && (_jsx(ActionCell, { className: `mfui-td column-select`, children: _jsx(InnerCellContent, { className: "mfui inner-cell-content row-action", children: _jsx(CheckBox, { className: `mfui-checkbox`, value: selected, onChange: (e) => handleSelectionChange(e) }) }) })), enableActionButton && (_jsx(ActionCell, { className: `mfui-td column-action`, children: _jsx(InnerCellContent, { className: "mfui inner-cell-content row-action", children: _jsx(ActionButton, { variant: "subtle", onClick: () => onActionButtonClick === null || onActionButtonClick === void 0 ? void 0 : onActionButtonClick(rowData), children: Icon ? _jsx(Icon, { size: 14 }) : _jsx(Maximize2Icon, { size: 14 }) }) }) })), columnState.map((column, index) => {
18
+ var _a;
18
19
  if (column.visible === false)
19
20
  return null;
20
21
  if (loading) {
@@ -24,16 +25,20 @@ const TableRow = ({ rowData, loading, rowStyle }) => {
24
25
  flex: column.width ? "0 0 auto" : "1",
25
26
  }, children: _jsx(LoadingCellIndicator, {}) }, index));
26
27
  }
28
+ const cellBody = column.render
29
+ ? column.render({ rowData, onRowUpdated })
30
+ : rowData[column.dataField];
31
+ const isTreeColumn = (treeOptions === null || treeOptions === void 0 ? void 0 : treeOptions.enabled) === true &&
32
+ column.dataField === firstVisibleDataField;
33
+ const expanded = isTreeColumn ? isRowExpanded(rowData) : false;
27
34
  return (_jsxs(TD, { className: `mfui-td column-${column.columnId}`, "data-field": column.dataField, style: {
28
35
  width: column.width,
29
36
  minWidth: column.minWidth,
30
37
  flex: column.width ? "0 0 auto" : "1",
31
- }, children: [(column === null || column === void 0 ? void 0 : column.enableResize) === true && _jsx(ColumnResizer, { column: column }), _jsx(InnerCellContent, { className: "mfui inner-cell-content", children: column.render
32
- ? column.render({
33
- rowData,
34
- onRowUpdated,
35
- })
36
- : rowData[column.dataField] })] }, index));
38
+ }, children: [(column === null || column === void 0 ? void 0 : column.enableResize) === true && _jsx(ColumnResizer, { column: column }), _jsx(InnerCellContent, { className: "mfui inner-cell-content", children: isTreeColumn ? (_jsxs(TreeCellWrapper, { children: [_jsx(TreeIndentSpacer, { "$level": (_a = rowData.__level) !== null && _a !== void 0 ? _a : 0, "$indentPx": treeOptions.indentPx }), rowData.__hasChildren ? (_jsx(TreeChevronButton, { type: "button", "$expanded": expanded, onClick: (e) => {
39
+ e.stopPropagation();
40
+ toggleRowExpanded(rowData);
41
+ }, "aria-label": expanded ? "Collapse row" : "Expand row", "aria-expanded": expanded, children: _jsx(ChevronRightIcon, { size: 12 }) })) : (_jsx(TreeChevronPlaceholder, {})), _jsx(TreeCellContent, { children: cellBody })] })) : (cellBody) })] }, index));
37
42
  })] }));
38
43
  };
39
44
  export default TableRow;
@@ -10,6 +10,42 @@ export type StateStorage = {
10
10
  type: "localStorage";
11
11
  key: string;
12
12
  };
13
+ export type TreeMode = "nested" | "flat";
14
+ export type TreeOptions = {
15
+ /** Enable tree-row behavior. Default: false */
16
+ enabled?: boolean;
17
+ /** Input shape. Default: "nested" */
18
+ mode?: TreeMode;
19
+ /** For mode="nested": field name that holds child array. Default: "children" */
20
+ childrenField?: string;
21
+ /** For mode="flat": field name that holds parent id. Default: "parentId" */
22
+ parentIdField?: string;
23
+ /** For mode="flat": field name that holds the row's stable id. Falls back to keyField. */
24
+ idField?: string;
25
+ /** Pixel width of one indentation level. Default: 16 */
26
+ indentPx?: number;
27
+ /** If a descendant matches search/filter, auto-expand its ancestors for the effective view. Default: true */
28
+ autoExpandOnMatch?: boolean;
29
+ };
30
+ export type ResolvedTreeOptions = {
31
+ enabled: boolean;
32
+ mode: TreeMode;
33
+ childrenField: string;
34
+ parentIdField: string;
35
+ idField?: string;
36
+ indentPx: number;
37
+ autoExpandOnMatch: boolean;
38
+ };
39
+ export type ExpandedKeysState = string[];
40
+ export type OnExpandedChangeFn = (keys: string[]) => void;
41
+ export type TreeMeta = {
42
+ /** Map of row key -> parent row key (undefined for roots) */
43
+ parentKeyMap: Map<string, string | undefined>;
44
+ /** Map of parent key -> ordered list of direct child keys */
45
+ childrenKeyMap: Map<string | undefined, string[]>;
46
+ /** All row keys that have children (eligible for expansion) */
47
+ expandableKeys: string[];
48
+ };
13
49
  export type RenderOptions = {
14
50
  rowData: any;
15
51
  onRowUpdated?: (context: {
@@ -55,6 +91,7 @@ export type TableState = {
55
91
  sortState?: SortState;
56
92
  filterState?: Query;
57
93
  searchState?: string;
94
+ expandedKeys?: ExpandedKeysState;
58
95
  };
59
96
  export interface onResizeFinishedProps {
60
97
  column: ColumnState;
@@ -102,6 +139,17 @@ export type TableContextType = {
102
139
  data: any[];
103
140
  totalRecords?: number;
104
141
  keyField?: string;
142
+ treeOptions: ResolvedTreeOptions;
143
+ firstVisibleDataField?: string;
144
+ expandedKeys: string[];
145
+ effectiveExpandedKeys: Set<string>;
146
+ isRowExpanded: (row: any) => boolean;
147
+ toggleRowExpanded: (row: any) => void;
148
+ expandRow: (row: any) => void;
149
+ collapseRow: (row: any) => void;
150
+ expandAllRows: () => void;
151
+ collapseAllRows: () => void;
152
+ onExpandedChange?: OnExpandedChangeFn;
105
153
  tableHeight?: number;
106
154
  tableMaxHeight?: number;
107
155
  tableMinHeight?: number;
@@ -213,6 +261,11 @@ export interface TableProviderProps {
213
261
  defaultFilterState?: Query;
214
262
  filterState?: Query;
215
263
  onFilterChange?: (e: Query) => void;
264
+ treeOptions?: ResolvedTreeOptions;
265
+ defaultExpandedKeys?: ExpandedKeysState;
266
+ expandedKeys?: ExpandedKeysState;
267
+ onExpandedChange?: OnExpandedChangeFn;
268
+ treeMeta?: TreeMeta;
216
269
  }
217
270
  export interface TableRowProps {
218
271
  rowData: {
@@ -334,6 +387,10 @@ export interface TableProps {
334
387
  defaultFilterState?: Query;
335
388
  filterState?: Query;
336
389
  onFilterChange?: (e: Query) => void;
390
+ treeOptions?: TreeOptions;
391
+ defaultExpandedKeys?: ExpandedKeysState;
392
+ expandedKeys?: ExpandedKeysState;
393
+ onExpandedChange?: OnExpandedChangeFn;
337
394
  }
338
395
  export type TableInstance = {
339
396
  columnState: ColumnState[];
@@ -354,4 +411,11 @@ export type TableInstance = {
354
411
  clearSearch: () => void;
355
412
  clearSelections: () => void;
356
413
  getTableState: () => TableState;
414
+ expandRow: (row: any) => void;
415
+ collapseRow: (row: any) => void;
416
+ toggleRowExpanded: (row: any) => void;
417
+ isRowExpanded: (row: any) => boolean;
418
+ expandAllRows: () => void;
419
+ collapseAllRows: () => void;
420
+ getExpandedRowKeys: () => string[];
357
421
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monolith-forensics/monolith-ui",
3
- "version": "1.5.0",
3
+ "version": "1.5.2-dev.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "author": "Matt Danner (Monolith Forensics LLC)",