@azrico/reactgrid 1.5.2 → 1.6.1

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/CHANGELOG.md CHANGED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### 1.7.0 (2026-10-14)
6
+
7
+ * Add `advancedFilterLabel` to rename the quick-search label (default "Search").
8
+ * Add `advancedFilterPlaceholder` for the quick-search input. A string is the native placeholder; any other node (custom HTML) is shown over the empty input.
9
+
10
+ ### 1.6.0 (2026-10-14)
11
+
12
+ * Pass `filterValue`, `sortInfo` and `groupBy` to a `dataSource` function next to `skip`, `limit`, `cached_data` and `last_cache_info`, so header filters can drive a server-side search.
13
+ * With `gridOptions.remoteFilter` / `remoteSort`, a filter or sort change invalidates the data cache and calls `dataSource` again. Local filtering and sorting still reuse the cached rows.
14
+ * Export the typed `AzDataSourceParams` and `AzDataSourceFunc` for the `dataSource` and `details_dataSource` props.
15
+ * Re-export `SelectFilter`, `NumberFilter`, `DateFilter` and `BoolFilter` for `column.filterEditor`.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export * from './src/views/DataGrid';
2
2
  export * from './src/types/types';
3
3
  export { get_row_id, set_row_id, get_col_name, get_col_header } from './src/func/func_grid';
4
+ export { default as SelectFilter } from '@inovua/reactdatagrid-enterprise/SelectFilter';
5
+ export { default as NumberFilter } from '@inovua/reactdatagrid-enterprise/NumberFilter';
6
+ export { default as DateFilter } from '@inovua/reactdatagrid-enterprise/DateFilter';
7
+ export { default as BoolFilter } from '@inovua/reactdatagrid-enterprise/BoolFilter';
4
8
  import { AzGrid } from './src/views/DataGrid';
5
9
  export default AzGrid;
package/dist/index.js CHANGED
@@ -7,8 +7,36 @@ var _exportNames = {
7
7
  get_row_id: true,
8
8
  set_row_id: true,
9
9
  get_col_name: true,
10
- get_col_header: true
10
+ get_col_header: true,
11
+ SelectFilter: true,
12
+ NumberFilter: true,
13
+ DateFilter: true,
14
+ BoolFilter: true
11
15
  };
16
+ Object.defineProperty(exports, "BoolFilter", {
17
+ enumerable: true,
18
+ get: function () {
19
+ return _BoolFilter.default;
20
+ }
21
+ });
22
+ Object.defineProperty(exports, "DateFilter", {
23
+ enumerable: true,
24
+ get: function () {
25
+ return _DateFilter.default;
26
+ }
27
+ });
28
+ Object.defineProperty(exports, "NumberFilter", {
29
+ enumerable: true,
30
+ get: function () {
31
+ return _NumberFilter.default;
32
+ }
33
+ });
34
+ Object.defineProperty(exports, "SelectFilter", {
35
+ enumerable: true,
36
+ get: function () {
37
+ return _SelectFilter.default;
38
+ }
39
+ });
12
40
  exports.default = void 0;
13
41
  Object.defineProperty(exports, "get_col_header", {
14
42
  enumerable: true,
@@ -59,4 +87,10 @@ Object.keys(_types).forEach(function (key) {
59
87
  });
60
88
  });
61
89
  var _func_grid = require("./src/func/func_grid");
90
+ var _SelectFilter = _interopRequireDefault(require("@inovua/reactdatagrid-enterprise/SelectFilter"));
91
+ var _NumberFilter = _interopRequireDefault(require("@inovua/reactdatagrid-enterprise/NumberFilter"));
92
+ var _DateFilter = _interopRequireDefault(require("@inovua/reactdatagrid-enterprise/DateFilter"));
93
+ var _BoolFilter = _interopRequireDefault(require("@inovua/reactdatagrid-enterprise/BoolFilter"));
94
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
95
+ // filter editors for column.filterEditor, so consumers don't import @inovua directly
62
96
  var _default = exports.default = _DataGrid.AzGrid;
@@ -175,6 +175,8 @@ function ExampleGrid() {
175
175
  editable: true,
176
176
  onEditComplete: onEditComplete,
177
177
  advancedFilterFields: ['name'],
178
+ advancedFilterLabel: "Search Name",
179
+ advancedFilterPlaceholder: _react.default.createElement("i", null, "Type a name"),
178
180
  // leftColumns={leftColumns}
179
181
  gridOptions: {
180
182
  defaultFilterValue: [{
@@ -3,7 +3,10 @@ export type MakeWriteable<Type> = {
3
3
  -readonly [Key in keyof Type]: Type[Key];
4
4
  };
5
5
  export type ModifyFields<T, R> = Omit<T, keyof R> & R;
6
- export declare function ApplyDataSource(datasource: any, props: any): Promise<any>;
6
+ export declare function ApplyDataSource(datasource: any, props: any, remote?: {
7
+ remoteFilter?: boolean;
8
+ remoteSort?: boolean;
9
+ }): Promise<any>;
7
10
  /**
8
11
  * merges together all different props for columns
9
12
  * to build a single final column data array
@@ -25,13 +25,17 @@ const defaultColumnOptions = {
25
25
  lockable: false,
26
26
  editable: false
27
27
  };
28
- async function ApplyDataSource(datasource, props) {
28
+ async function ApplyDataSource(datasource, props, remote = {}) {
29
29
  let result_data = undefined;
30
30
  let isUsingCache = false;
31
+ //filter/sort are only part of the key when they are sent to a remote dataSource,
32
+ //local filtering/sorting keeps reusing the cached rows
31
33
  const cacheInfo = {
32
34
  ds: datasource,
33
35
  skip: props.skip,
34
- limit: props.limit
36
+ limit: props.limit,
37
+ filterValue: remote.remoteFilter ? props.filterValue : undefined,
38
+ sortInfo: remote.remoteSort ? props.sortInfo : undefined
35
39
  };
36
40
  //check if we can used cached data
37
41
  //if the datasource is changed since last render, we should invalidate cache
@@ -284,7 +288,10 @@ function get_default_key() {
284
288
  }
285
289
  //check if two cache objects are the same
286
290
  function validateCache(current, old, looseCheck = false) {
287
- if (!old || !(0, _object.object_equals)(current, old)) return false;
291
+ //deep compare skip/limit/filterValue/sortInfo, array order matters (sort priority)
292
+ if (!old || !(0, _object.object_equals)(current, old, {
293
+ strictArrayCheck: true
294
+ })) return false;
288
295
  if (looseCheck) return true;
289
296
  if (current.ds != old.ds) return false;
290
297
  return true;
@@ -8,14 +8,25 @@ var _react = _interopRequireDefault(require("react"));
8
8
  var _func_grid = require("../func/func_grid");
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
10
  function useDataSource(ds, props) {
11
+ var _props$gridOptions, _props$gridOptions2;
11
12
  const cached_data = _react.default.useRef();
12
13
  const last_cache_info = _react.default.useRef();
14
+ // filter/sort only change the result when the grid sends them to the dataSource
15
+ const remoteFilter = !!(props !== null && props !== void 0 && (_props$gridOptions = props.gridOptions) !== null && _props$gridOptions !== void 0 && _props$gridOptions.remoteFilter);
16
+ const remoteSort = !!(props !== null && props !== void 0 && (_props$gridOptions2 = props.gridOptions) !== null && _props$gridOptions2 !== void 0 && _props$gridOptions2.remoteSort);
13
17
  return _react.default.useCallback(async dt => {
14
- return await (0, _func_grid.ApplyDataSource)(ds, {
18
+ const params = {
15
19
  skip: dt === null || dt === void 0 ? void 0 : dt.skip,
16
20
  limit: dt === null || dt === void 0 ? void 0 : dt.limit,
21
+ filterValue: dt === null || dt === void 0 ? void 0 : dt.filterValue,
22
+ sortInfo: dt === null || dt === void 0 ? void 0 : dt.sortInfo,
23
+ groupBy: dt === null || dt === void 0 ? void 0 : dt.groupBy,
17
24
  cached_data: cached_data,
18
25
  last_cache_info: last_cache_info
26
+ };
27
+ return await (0, _func_grid.ApplyDataSource)(ds, params, {
28
+ remoteFilter,
29
+ remoteSort
19
30
  });
20
- }, [ds]);
31
+ }, [ds, remoteFilter, remoteSort]);
21
32
  }
@@ -79,14 +79,39 @@ function useGridSearch(props) {
79
79
  searchId.current++;
80
80
  set_searchResult(null);
81
81
  }, [search, props.dataSource]);
82
- const element = _react.default.createElement(_react.default.Fragment, null, _react.default.createElement("form", {
83
- className: 'azgrid-searcharea'
84
- }, _react.default.createElement("label", null, "Search", _react.default.createElement("input", {
82
+ const {
83
+ advancedFilterLabel,
84
+ advancedFilterPlaceholder
85
+ } = props;
86
+ const textPlaceholder = typeof advancedFilterPlaceholder === 'string' || typeof advancedFilterPlaceholder === 'number' ? String(advancedFilterPlaceholder) : undefined;
87
+ // custom HTML cannot go in the native placeholder attribute, so it is shown over the empty input
88
+ const nodePlaceholder = textPlaceholder === undefined && advancedFilterPlaceholder !== undefined && advancedFilterPlaceholder !== null && typeof advancedFilterPlaceholder !== 'boolean' ? advancedFilterPlaceholder : undefined;
89
+ const input = _react.default.createElement("input", {
85
90
  type: "text",
86
91
  name: "name",
87
92
  value: search,
93
+ placeholder: textPlaceholder,
88
94
  onChange: e => set_search(e.target.value)
89
- }))));
95
+ });
96
+ const element = _react.default.createElement(_react.default.Fragment, null, _react.default.createElement("form", {
97
+ className: 'azgrid-searcharea'
98
+ }, _react.default.createElement("label", null, advancedFilterLabel !== null && advancedFilterLabel !== void 0 ? advancedFilterLabel : 'Search', nodePlaceholder === undefined ? input : _react.default.createElement("span", {
99
+ style: {
100
+ position: 'relative',
101
+ display: 'inline-block'
102
+ }
103
+ }, input, search ? null : _react.default.createElement("span", {
104
+ style: {
105
+ position: 'absolute',
106
+ left: 8,
107
+ top: 0,
108
+ bottom: 0,
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ pointerEvents: 'none',
112
+ opacity: 0.6
113
+ }
114
+ }, nodePlaceholder)))));
90
115
  return {
91
116
  filteredData: searchResult !== null && searchResult !== void 0 ? searchResult : trackedDataSource,
92
117
  element
@@ -1,7 +1,7 @@
1
1
  import { ModifyFields } from '../func/func_grid';
2
2
  import { ConvertableToMenuItem } from '@azrico/controls/context-menu';
3
3
  import React from 'react';
4
- import { TypeCellSelection, TypeColumn, TypeDataGridProps, TypeDataSource, TypeEditInfo, TypeFilterTypes, TypeFilterValue, TypeRowDetailsInfo, TypeRowProps, TypeRowSelection, TypeComputedProps } from '@inovua/reactdatagrid-community/types';
4
+ import { TypeCellSelection, TypeColumn, TypeDataGridProps, TypeDataSource, TypeEditInfo, TypeFilterTypes, TypeFilterValue, TypeRowDetailsInfo, TypeRowProps, TypeRowSelection, TypeComputedProps, TypeSortInfo, TypeGroupBy } from '@inovua/reactdatagrid-community/types';
5
5
  import { TypeOnSelectionChangeArg } from '@inovua/reactdatagrid-community/types/TypeDataGridProps';
6
6
  export type ColumnDefinition = TypeColumn;
7
7
  export type CellDefinition = any;
@@ -44,9 +44,21 @@ export type AzGridOptions = Partial<TypeDataGridProps> & {
44
44
  [key: string]: any;
45
45
  };
46
46
  };
47
+ /** arguments passed to a dataSource function */
48
+ export type AzDataSourceParams = {
49
+ skip?: number;
50
+ limit?: number;
51
+ /** current header filters, filter.value is an array of selected ids for a multi-select SelectFilter */
52
+ filterValue?: TypeFilterValue;
53
+ sortInfo?: TypeSortInfo;
54
+ groupBy?: TypeGroupBy;
55
+ cached_data: React.MutableRefObject<any>;
56
+ last_cache_info: React.MutableRefObject<any>;
57
+ };
58
+ export type AzDataSourceFunc = (args: AzDataSourceParams) => any[] | Promise<any[] | any>;
47
59
  export type AzGridProps = {
48
- dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
49
- details_dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
60
+ dataSource?: any[] | AzDataSourceFunc;
61
+ details_dataSource?: any[] | AzDataSourceFunc;
50
62
  details_columns?: LooseColumnDefinition[];
51
63
  details_filter?: FilterValues;
52
64
  licenseKey?: string;
@@ -76,6 +88,10 @@ export type AzGridProps = {
76
88
  limit?: number;
77
89
  editable?: boolean | ((editValue: any, cellProps: any) => boolean);
78
90
  advancedFilterFields?: string[];
91
+ /** Label of the quick-search box shown with `advancedFilterFields`. Default "Search". */
92
+ advancedFilterLabel?: React.ReactNode;
93
+ /** Placeholder of the quick-search input. A string is used as the native placeholder, any other node (custom HTML) is shown over the empty input. */
94
+ advancedFilterPlaceholder?: React.ReactNode;
79
95
  loading?: boolean;
80
96
  theme?: 'light' | 'dark' | (string & {});
81
97
  /** @deprecated Use theme="dark" or theme="light" instead. */
package/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from './src/views/DataGrid';
2
2
  export * from './src/types/types';
3
3
  export { get_row_id, set_row_id, get_col_name, get_col_header } from './src/func/func_grid';
4
+ // filter editors for column.filterEditor, so consumers don't import @inovua directly
5
+ export { default as SelectFilter } from '@inovua/reactdatagrid-enterprise/SelectFilter';
6
+ export { default as NumberFilter } from '@inovua/reactdatagrid-enterprise/NumberFilter';
7
+ export { default as DateFilter } from '@inovua/reactdatagrid-enterprise/DateFilter';
8
+ export { default as BoolFilter } from '@inovua/reactdatagrid-enterprise/BoolFilter';
4
9
 
5
10
  import { AzGrid } from './src/views/DataGrid';
6
11
  export default AzGrid;
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "@azrico/reactgrid",
3
- "version": "1.5.2",
4
- "main": "dist/index.js",
5
- "description": "React Datagrid",
6
- "author": "Azrideus <MehrdadTavangar@gmail.com>",
7
- "homepage": ".",
8
- "scripts": {
9
- "start": "react-scripts start",
10
- "build": "tsc && yarn cp_themes && babel dist --out-dir dist",
11
- "test": "rem",
12
- "check:theme": "node config/check-theme.cjs",
13
- "theme:generate": "node config/generate-theme.cjs",
14
- "cp_themes": "copyfiles ./src/themes/* ./dist/"
15
- },
16
- "config-overrides-path": "../../config/config-overrides.js",
17
- "devDependencies": {
18
- "@babel/cli": "^7.13.14",
19
- "@babel/core": "^7.13.15",
20
- "@babel/eslint-parser": "^7.12.17",
21
- "@babel/node": "^7.13.13",
22
- "@babel/plugin-proposal-class-properties": "^7.12.13",
23
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
24
- "@babel/preset-env": "^7.13.15",
25
- "@babel/preset-typescript": "^7.14.5",
26
- "@mui/material": "*",
27
- "@types/react": "^18.0.25",
28
- "@types/react-dom": "^18.0.8",
29
- "react-dom": "^18.2.0",
30
- "react-scripts": "^5.0.1",
31
- "typescript": "^4.8.4"
32
- },
33
- "dependencies": {
34
- "@azrico/controls": "1.6.1",
35
- "@azrico/object": "^1.4.97",
36
- "@emotion/react": "^11.9.3",
37
- "@emotion/styled": "^11.9.3",
38
- "@inovua/reactdatagrid-enterprise": "^5.10.2",
39
- "clsx": "^2.0.0",
40
- "lodash.debounce": "^4.0.8",
41
- "react": "^18.2.0",
42
- "react-use-size": "^3.0.1"
43
- },
44
- "peerDependencies": {
45
- "@azrico/controls": ">=1.6.1",
46
- "@azrico/object": ">=1.4.97",
47
- "@emotion/react": ">=11.9.3",
48
- "@emotion/styled": ">=11.9.3",
49
- "@inovua/reactdatagrid-enterprise": ">=5.10.2",
50
- "@mui/material": ">=5.0.0 <8.0.0",
51
- "clsx": ">=1.2.1",
52
- "react": ">=18.2.0",
53
- "react-use-size": ">=3.0.1"
54
- },
55
- "browserslist": {
56
- "production": [
57
- ">0.2%",
58
- "not dead",
59
- "not op_mini all"
60
- ],
61
- "development": [
62
- "last 1 chrome version",
63
- "last 1 firefox version",
64
- "last 1 safari version"
65
- ]
66
- },
67
- "packageManager": "yarn@4.10.2"
1
+ {
2
+ "name": "@azrico/reactgrid",
3
+ "version": "1.6.1",
4
+ "main": "dist/index.js",
5
+ "description": "React Datagrid",
6
+ "author": "Azrideus <MehrdadTavangar@gmail.com>",
7
+ "homepage": ".",
8
+ "scripts": {
9
+ "start": "react-scripts start",
10
+ "build": "tsc && yarn cp_themes && babel dist --out-dir dist",
11
+ "test": "rem",
12
+ "check:theme": "node config/check-theme.cjs",
13
+ "theme:generate": "node config/generate-theme.cjs",
14
+ "cp_themes": "copyfiles ./src/themes/* ./dist/"
15
+ },
16
+ "config-overrides-path": "../../config/config-overrides.js",
17
+ "devDependencies": {
18
+ "@babel/cli": "^7.13.14",
19
+ "@babel/core": "^7.13.15",
20
+ "@babel/eslint-parser": "^7.12.17",
21
+ "@babel/node": "^7.13.13",
22
+ "@babel/plugin-proposal-class-properties": "^7.12.13",
23
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
24
+ "@babel/preset-env": "^7.13.15",
25
+ "@babel/preset-typescript": "^7.14.5",
26
+ "@mui/material": "*",
27
+ "@types/react": "^18.0.25",
28
+ "@types/react-dom": "^18.0.8",
29
+ "react-dom": "^18.2.0",
30
+ "react-scripts": "^5.0.1",
31
+ "typescript": "^4.8.4"
32
+ },
33
+ "dependencies": {
34
+ "@azrico/controls": "1.6.1",
35
+ "@azrico/object": "^1.4.97",
36
+ "@emotion/react": "^11.9.3",
37
+ "@emotion/styled": "^11.9.3",
38
+ "@inovua/reactdatagrid-enterprise": "^5.10.2",
39
+ "clsx": "^2.0.0",
40
+ "lodash.debounce": "^4.0.8",
41
+ "react": "^18.2.0",
42
+ "react-use-size": "^3.0.1"
43
+ },
44
+ "peerDependencies": {
45
+ "@azrico/controls": ">=1.6.1",
46
+ "@azrico/object": ">=1.4.97",
47
+ "@emotion/react": ">=11.9.3",
48
+ "@emotion/styled": ">=11.9.3",
49
+ "@inovua/reactdatagrid-enterprise": ">=5.10.2",
50
+ "@mui/material": ">=5.0.0 <8.0.0",
51
+ "clsx": ">=1.2.1",
52
+ "react": ">=18.2.0",
53
+ "react-use-size": ">=3.0.1"
54
+ },
55
+ "browserslist": {
56
+ "production": [
57
+ ">0.2%",
58
+ "not dead",
59
+ "not op_mini all"
60
+ ],
61
+ "development": [
62
+ "last 1 chrome version",
63
+ "last 1 firefox version",
64
+ "last 1 safari version"
65
+ ]
66
+ },
67
+ "packageManager": "yarn@4.10.2"
68
68
  }
package/src/example.tsx CHANGED
@@ -1,184 +1,186 @@
1
- export * from './views/DataGrid';
2
-
3
- import AzGrid from './views/DataGrid';
4
- import React from 'react';
5
- import './themes/azgrid-theme.css';
6
- import './themes/example-palettes.css';
7
-
8
- const columns = [
9
- {
10
- name: 'name',
11
- title: 'Name',
12
- defaultFlex: 1,
13
- },
14
- {
15
- name: 'job',
16
- title: 'Job',
17
- type: 'combo',
18
- items: ['None', 'Engineer', 'Doctor'],
19
- defaultFlex: 1,
20
- },
21
- {
22
- name: 'age',
23
- title: 'Age',
24
- type: 'number',
25
- defaultFlex: 1,
26
- ariaLabel: (value: any) => `${value} years`,
27
- },
28
- ];
29
-
30
- const editableColumns = ['job', 'age'];
31
-
32
- function getData(delta: any, max = 200) {
33
- const fd: any[] = [];
34
- for (let index = 0; index < max; index++) {
35
- const currentObject = {
36
- __table_id: String(index),
37
- name: 'user-' + index,
38
- age: 5 + index,
39
- job: 'None',
40
- };
41
- if (delta[index] && typeof delta[index] === 'object') {
42
- for (const key in delta[index]) {
43
- currentObject[key] = delta[index][key];
44
- }
45
- }
46
-
47
- const childrows: any[] = [];
48
- if (index % 5 == 0) {
49
- const sub_selfid = 's' + index;
50
- childrows.push({
51
- __table_id: sub_selfid,
52
- __parent__table_id: String(index),
53
- name: 'sub-' + index,
54
- age: 20,
55
- });
56
- }
57
-
58
- currentObject['__children'] = childrows;
59
- fd.push(currentObject);
60
- }
61
- return fd;
62
- }
63
-
64
- export function ExampleGrid() {
65
- const [ds_reset, set_ds_reset] = React.useState(0);
66
- const [delta, set_delta] = React.useState({});
67
- const [delayed_reload, set_delayed_reload] = React.useState(false);
68
-
69
- const dataSource = React.useMemo(() => getData(delta), []);
70
- // const dataSource = React.useCallback(
71
- // async (dt) => {
72
- // //console.log('Start loading');
73
- // return await new Promise((resolve) => {
74
- // setTimeout(() => {
75
- // resolve(getData(delta));
76
- // }, 1000);
77
- // });
78
- // },
79
- // [ds_reset, delta]
80
- // );
81
- // const details_dataSource = React.useCallback(
82
- // async (dt) => {
83
- // //console.log('Start loading');
84
- // return await new Promise((resolve) => {
85
- // setTimeout(() => {
86
- // resolve(getData(delta, 5));
87
- // }, 1000);
88
- // });
89
- // },
90
- // [ds_reset, delta]
91
- // );
92
-
93
- function onEditComplete(params) {
94
- const { value, columnId, rowId, ...rest } = params;
95
- let new_rd = delta[rowId];
96
- if (!new_rd) new_rd = {};
97
- new_rd[columnId] = value;
98
- set_delta((d) => {
99
- return { ...d, [rowId]: new_rd };
100
- });
101
- }
102
-
103
- const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
104
- console.log('dbl', rowdata?.data || rowdata);
105
- alert('dbl: ' + index);
106
- }, []);
107
- const onRowClick = React.useCallback((index: number, rowdata: any) => {
108
- console.log('click:', rowdata?.data || rowdata);
109
- }, []);
110
-
111
- React.useEffect(() => {
112
- setTimeout(() => {
113
- set_delayed_reload(true);
114
- }, 2000);
115
- }, []);
116
- const customColumns = React.useMemo(() => {
117
- if (!delayed_reload) return [];
118
- return [{ name: 'name', render: (r) => r.value + '^' }];
119
- }, [delayed_reload]);
120
-
121
- const contextMenuItems = React.useCallback((product: any) => {
122
- return [
123
- {
124
- type: 'text',
125
- content: product.name,
126
- },
127
- ];
128
- }, []);
129
-
130
- return (
131
- <div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
132
- {['light', 'dark', 'deco'].map((theme) => (
133
- <section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
134
- <h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
135
- <AzGrid
136
- theme={theme}
137
- ariaLabel={`Users (${theme})`}
138
- delayedSize
139
- // details_dataSource={details_dataSource}
140
- dataSource={dataSource}
141
- details_columns={columns}
142
- onRowClick={onRowClick}
143
- onRowDoubleClick={onRowDoubleClick}
144
- allowChildRows
145
- // onSelectionChange={onSelectionChange}
146
- // selected={selected}
147
- contextMenuItems={contextMenuItems as any}
148
- customColumns={customColumns}
149
- columns={columns}
150
- editableColumns={editableColumns}
151
- editable={true}
152
- onEditComplete={onEditComplete}
153
- advancedFilterFields={['name']}
154
- // leftColumns={leftColumns}
155
- gridOptions={
156
- {
157
- defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
158
- pagination: 'local',
159
- defaultLimit: 10,
160
- pageSizes: [10, 25, 50],
161
- checkboxColumn: true,
162
- multiSelect: true,
163
- // consumer onRenderRow runs after the internal one
164
- onRenderRow: (rowProps: any) => {
165
- rowProps['data-example-row'] = 'true';
166
- },
167
- // defaultFilterValue: filterValue,
168
- // remoteFilter: false,
169
- // multiSelect: true,
170
- // pagination: 'local',
171
- // defaultLimit: 100,
172
- // limit: 100,
173
- // pageSizes: [100],
174
- // rowIndexColumn: true,
175
- // checkboxColumn: true,
176
- }
177
- }
178
- />
179
- </section>
180
- ))}
181
- </div>
182
- );
183
- }
184
- export default ExampleGrid;
1
+ export * from './views/DataGrid';
2
+
3
+ import AzGrid from './views/DataGrid';
4
+ import React from 'react';
5
+ import './themes/azgrid-theme.css';
6
+ import './themes/example-palettes.css';
7
+
8
+ const columns = [
9
+ {
10
+ name: 'name',
11
+ title: 'Name',
12
+ defaultFlex: 1,
13
+ },
14
+ {
15
+ name: 'job',
16
+ title: 'Job',
17
+ type: 'combo',
18
+ items: ['None', 'Engineer', 'Doctor'],
19
+ defaultFlex: 1,
20
+ },
21
+ {
22
+ name: 'age',
23
+ title: 'Age',
24
+ type: 'number',
25
+ defaultFlex: 1,
26
+ ariaLabel: (value: any) => `${value} years`,
27
+ },
28
+ ];
29
+
30
+ const editableColumns = ['job', 'age'];
31
+
32
+ function getData(delta: any, max = 200) {
33
+ const fd: any[] = [];
34
+ for (let index = 0; index < max; index++) {
35
+ const currentObject = {
36
+ __table_id: String(index),
37
+ name: 'user-' + index,
38
+ age: 5 + index,
39
+ job: 'None',
40
+ };
41
+ if (delta[index] && typeof delta[index] === 'object') {
42
+ for (const key in delta[index]) {
43
+ currentObject[key] = delta[index][key];
44
+ }
45
+ }
46
+
47
+ const childrows: any[] = [];
48
+ if (index % 5 == 0) {
49
+ const sub_selfid = 's' + index;
50
+ childrows.push({
51
+ __table_id: sub_selfid,
52
+ __parent__table_id: String(index),
53
+ name: 'sub-' + index,
54
+ age: 20,
55
+ });
56
+ }
57
+
58
+ currentObject['__children'] = childrows;
59
+ fd.push(currentObject);
60
+ }
61
+ return fd;
62
+ }
63
+
64
+ export function ExampleGrid() {
65
+ const [ds_reset, set_ds_reset] = React.useState(0);
66
+ const [delta, set_delta] = React.useState({});
67
+ const [delayed_reload, set_delayed_reload] = React.useState(false);
68
+
69
+ const dataSource = React.useMemo(() => getData(delta), []);
70
+ // const dataSource = React.useCallback(
71
+ // async (dt) => {
72
+ // //console.log('Start loading');
73
+ // return await new Promise((resolve) => {
74
+ // setTimeout(() => {
75
+ // resolve(getData(delta));
76
+ // }, 1000);
77
+ // });
78
+ // },
79
+ // [ds_reset, delta]
80
+ // );
81
+ // const details_dataSource = React.useCallback(
82
+ // async (dt) => {
83
+ // //console.log('Start loading');
84
+ // return await new Promise((resolve) => {
85
+ // setTimeout(() => {
86
+ // resolve(getData(delta, 5));
87
+ // }, 1000);
88
+ // });
89
+ // },
90
+ // [ds_reset, delta]
91
+ // );
92
+
93
+ function onEditComplete(params) {
94
+ const { value, columnId, rowId, ...rest } = params;
95
+ let new_rd = delta[rowId];
96
+ if (!new_rd) new_rd = {};
97
+ new_rd[columnId] = value;
98
+ set_delta((d) => {
99
+ return { ...d, [rowId]: new_rd };
100
+ });
101
+ }
102
+
103
+ const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
104
+ console.log('dbl', rowdata?.data || rowdata);
105
+ alert('dbl: ' + index);
106
+ }, []);
107
+ const onRowClick = React.useCallback((index: number, rowdata: any) => {
108
+ console.log('click:', rowdata?.data || rowdata);
109
+ }, []);
110
+
111
+ React.useEffect(() => {
112
+ setTimeout(() => {
113
+ set_delayed_reload(true);
114
+ }, 2000);
115
+ }, []);
116
+ const customColumns = React.useMemo(() => {
117
+ if (!delayed_reload) return [];
118
+ return [{ name: 'name', render: (r) => r.value + '^' }];
119
+ }, [delayed_reload]);
120
+
121
+ const contextMenuItems = React.useCallback((product: any) => {
122
+ return [
123
+ {
124
+ type: 'text',
125
+ content: product.name,
126
+ },
127
+ ];
128
+ }, []);
129
+
130
+ return (
131
+ <div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
132
+ {['light', 'dark', 'deco'].map((theme) => (
133
+ <section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
134
+ <h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
135
+ <AzGrid
136
+ theme={theme}
137
+ ariaLabel={`Users (${theme})`}
138
+ delayedSize
139
+ // details_dataSource={details_dataSource}
140
+ dataSource={dataSource}
141
+ details_columns={columns}
142
+ onRowClick={onRowClick}
143
+ onRowDoubleClick={onRowDoubleClick}
144
+ allowChildRows
145
+ // onSelectionChange={onSelectionChange}
146
+ // selected={selected}
147
+ contextMenuItems={contextMenuItems as any}
148
+ customColumns={customColumns}
149
+ columns={columns}
150
+ editableColumns={editableColumns}
151
+ editable={true}
152
+ onEditComplete={onEditComplete}
153
+ advancedFilterFields={['name']}
154
+ advancedFilterLabel="Search Name"
155
+ advancedFilterPlaceholder={<i>Type a name</i>}
156
+ // leftColumns={leftColumns}
157
+ gridOptions={
158
+ {
159
+ defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
160
+ pagination: 'local',
161
+ defaultLimit: 10,
162
+ pageSizes: [10, 25, 50],
163
+ checkboxColumn: true,
164
+ multiSelect: true,
165
+ // consumer onRenderRow runs after the internal one
166
+ onRenderRow: (rowProps: any) => {
167
+ rowProps['data-example-row'] = 'true';
168
+ },
169
+ // defaultFilterValue: filterValue,
170
+ // remoteFilter: false,
171
+ // multiSelect: true,
172
+ // pagination: 'local',
173
+ // defaultLimit: 100,
174
+ // limit: 100,
175
+ // pageSizes: [100],
176
+ // rowIndexColumn: true,
177
+ // checkboxColumn: true,
178
+ }
179
+ }
180
+ />
181
+ </section>
182
+ ))}
183
+ </div>
184
+ );
185
+ }
186
+ export default ExampleGrid;
@@ -17,12 +17,24 @@ const defaultColumnOptions: ColumnDefinition = {
17
17
  editable: false,
18
18
  };
19
19
 
20
- export async function ApplyDataSource(datasource, props) {
20
+ export async function ApplyDataSource(
21
+ datasource,
22
+ props,
23
+ remote: { remoteFilter?: boolean; remoteSort?: boolean } = {}
24
+ ) {
21
25
  let result_data: any = undefined;
22
26
 
23
27
  let isUsingCache = false;
24
28
 
25
- const cacheInfo = { ds: datasource, skip: props.skip, limit: props.limit };
29
+ //filter/sort are only part of the key when they are sent to a remote dataSource,
30
+ //local filtering/sorting keeps reusing the cached rows
31
+ const cacheInfo = {
32
+ ds: datasource,
33
+ skip: props.skip,
34
+ limit: props.limit,
35
+ filterValue: remote.remoteFilter ? props.filterValue : undefined,
36
+ sortInfo: remote.remoteSort ? props.sortInfo : undefined,
37
+ };
26
38
  //check if we can used cached data
27
39
  //if the datasource is changed since last render, we should invalidate cache
28
40
  if (!validateCache(props.last_cache_info.current, cacheInfo, props.looseCacheCheck)) {
@@ -289,7 +301,8 @@ export function get_default_key() {
289
301
 
290
302
  //check if two cache objects are the same
291
303
  export function validateCache(current: any, old: any, looseCheck = false) {
292
- if (!old || !object_equals(current, old)) return false;
304
+ //deep compare skip/limit/filterValue/sortInfo, array order matters (sort priority)
305
+ if (!old || !object_equals(current, old, { strictArrayCheck: true })) return false;
293
306
 
294
307
  if (looseCheck) return true;
295
308
 
@@ -1,25 +1,32 @@
1
- import React from 'react';
2
- import { AzGridProps, DataGridWrapperProps, GridType } from '../types/types';
3
- import {
4
- applyColumnData,
5
- ApplyDataSource,
6
- find_realData,
7
- get_row_id,
8
- } from '../func/func_grid';
9
- import { func_selection } from '../func/func_child';
10
-
11
- export function useDataSource(ds: any, props: AzGridProps) {
12
- const cached_data = React.useRef<any>();
13
- const last_cache_info = React.useRef<any>();
14
- return React.useCallback(
15
- async (dt) => {
16
- return await ApplyDataSource(ds, {
17
- skip: dt?.skip,
18
- limit: dt?.limit,
19
- cached_data: cached_data,
20
- last_cache_info: last_cache_info,
21
- });
22
- },
23
- [ds]
24
- );
25
- }
1
+ import React from 'react';
2
+ import { AzDataSourceParams, AzGridProps, DataGridWrapperProps, GridType } from '../types/types';
3
+ import {
4
+ applyColumnData,
5
+ ApplyDataSource,
6
+ find_realData,
7
+ get_row_id,
8
+ } from '../func/func_grid';
9
+ import { func_selection } from '../func/func_child';
10
+
11
+ export function useDataSource(ds: any, props: AzGridProps) {
12
+ const cached_data = React.useRef<any>();
13
+ const last_cache_info = React.useRef<any>();
14
+ // filter/sort only change the result when the grid sends them to the dataSource
15
+ const remoteFilter = !!props?.gridOptions?.remoteFilter;
16
+ const remoteSort = !!props?.gridOptions?.remoteSort;
17
+ return React.useCallback(
18
+ async (dt) => {
19
+ const params: AzDataSourceParams = {
20
+ skip: dt?.skip,
21
+ limit: dt?.limit,
22
+ filterValue: dt?.filterValue,
23
+ sortInfo: dt?.sortInfo,
24
+ groupBy: dt?.groupBy,
25
+ cached_data: cached_data,
26
+ last_cache_info: last_cache_info,
27
+ };
28
+ return await ApplyDataSource(ds, params, { remoteFilter, remoteSort });
29
+ },
30
+ [ds, remoteFilter, remoteSort]
31
+ );
32
+ }
@@ -1,106 +1,146 @@
1
- import React from 'react';
2
- import { DataGridWrapperProps } from '../types/types';
3
- import debounce from 'lodash.debounce';
4
- import { array_intersection, fn_eval } from '@azrico/object';
5
-
6
- const SEARCH_DELAY = 300;
7
-
8
- function to_rows(result: any): any[] {
9
- return result && typeof result === 'object' ? result.data || result : result;
10
- }
11
- //false when the result is only a page of the data (remote pagination)
12
- function is_complete(result: any) {
13
- if (!result || typeof result !== 'object' || Array.isArray(result)) return true;
14
- const loaded = Array.isArray(result.data) ? result.data.length : 0;
15
- return !(result.count > loaded);
16
- }
17
-
18
- export function useGridSearch(props: DataGridWrapperProps) {
19
- const [search, set_search] = React.useState('');
20
- // null shows the datasource itself, an array shows search results
21
- const [searchResult, set_searchResult] = React.useState<any[] | null>(null);
22
- const latestProps = React.useRef(props);
23
- latestProps.current = props;
24
- // last result the grid loaded, reused by search instead of loading the data again
25
- const loaded = React.useRef<{ ds: any; result: any } | null>(null);
26
- const searchId = React.useRef(0);
27
-
28
- const trackedDataSource = React.useCallback(
29
- async (args: any) => {
30
- const result = await fn_eval(props.dataSource, args);
31
- loaded.current = { ds: props.dataSource, result };
32
- return result;
33
- },
34
- [props.dataSource]
35
- );
36
-
37
- const get_search_rows = React.useCallback(async () => {
38
- const ds = latestProps.current.dataSource;
39
- const last = loaded.current;
40
- if (last && last.ds === ds && is_complete(last.result)) return to_rows(last.result);
41
- return to_rows(await fn_eval(ds));
42
- }, []);
43
-
44
- const filter_rows = React.useCallback((rows: any[], text: string) => {
45
- const { gridRef, advancedFilterFields } = latestProps.current;
46
- const visibleColumns = gridRef.current?.visibleColumns;
47
- const columns_to_search: string[] = visibleColumns
48
- ? array_intersection(
49
- advancedFilterFields || [],
50
- visibleColumns.map((r) => r.id)
51
- )
52
- : advancedFilterFields || [];
53
- const lowerSearchText = text.toLowerCase().trim();
54
- if (!lowerSearchText || columns_to_search.length === 0) return rows;
55
- return rows.filter((p) => {
56
- for (let index = 0; index < columns_to_search.length; index++) {
57
- const col = columns_to_search[index];
58
- const v = (p[col] + '').toLowerCase(); //value of column
59
- if (v && v.indexOf(lowerSearchText) != -1) {
60
- //if one column is matching then the row is a match
61
- return true;
62
- }
63
- }
64
- return false;
65
- });
66
- }, []);
67
-
68
- const run_search = React.useMemo(
69
- () =>
70
- debounce(async (text: string) => {
71
- const id = ++searchId.current;
72
- const rows = await get_search_rows();
73
- if (id !== searchId.current || !Array.isArray(rows)) return; //a newer search started
74
- set_searchResult(filter_rows(rows, text));
75
- }, SEARCH_DELAY),
76
- []
77
- );
78
- React.useEffect(() => () => run_search.cancel(), [run_search]);
79
-
80
- React.useEffect(() => {
81
- if (search) {
82
- run_search(search);
83
- return;
84
- }
85
- run_search.cancel();
86
- searchId.current++;
87
- set_searchResult(null);
88
- }, [search, props.dataSource]);
89
-
90
- const element = (
91
- <>
92
- <form className={'azgrid-searcharea'}>
93
- <label>
94
- Search
95
- <input
96
- type="text"
97
- name="name"
98
- value={search}
99
- onChange={(e) => set_search(e.target.value)}
100
- />
101
- </label>
102
- </form>
103
- </>
104
- );
105
- return { filteredData: searchResult ?? trackedDataSource, element };
106
- }
1
+ import React from 'react';
2
+ import { DataGridWrapperProps } from '../types/types';
3
+ import debounce from 'lodash.debounce';
4
+ import { array_intersection, fn_eval } from '@azrico/object';
5
+
6
+ const SEARCH_DELAY = 300;
7
+
8
+ function to_rows(result: any): any[] {
9
+ return result && typeof result === 'object' ? result.data || result : result;
10
+ }
11
+ //false when the result is only a page of the data (remote pagination)
12
+ function is_complete(result: any) {
13
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return true;
14
+ const loaded = Array.isArray(result.data) ? result.data.length : 0;
15
+ return !(result.count > loaded);
16
+ }
17
+
18
+ export function useGridSearch(props: DataGridWrapperProps) {
19
+ const [search, set_search] = React.useState('');
20
+ // null shows the datasource itself, an array shows search results
21
+ const [searchResult, set_searchResult] = React.useState<any[] | null>(null);
22
+ const latestProps = React.useRef(props);
23
+ latestProps.current = props;
24
+ // last result the grid loaded, reused by search instead of loading the data again
25
+ const loaded = React.useRef<{ ds: any; result: any } | null>(null);
26
+ const searchId = React.useRef(0);
27
+
28
+ const trackedDataSource = React.useCallback(
29
+ async (args: any) => {
30
+ const result = await fn_eval(props.dataSource, args);
31
+ loaded.current = { ds: props.dataSource, result };
32
+ return result;
33
+ },
34
+ [props.dataSource]
35
+ );
36
+
37
+ const get_search_rows = React.useCallback(async () => {
38
+ const ds = latestProps.current.dataSource;
39
+ const last = loaded.current;
40
+ if (last && last.ds === ds && is_complete(last.result)) return to_rows(last.result);
41
+ return to_rows(await fn_eval(ds));
42
+ }, []);
43
+
44
+ const filter_rows = React.useCallback((rows: any[], text: string) => {
45
+ const { gridRef, advancedFilterFields } = latestProps.current;
46
+ const visibleColumns = gridRef.current?.visibleColumns;
47
+ const columns_to_search: string[] = visibleColumns
48
+ ? array_intersection(
49
+ advancedFilterFields || [],
50
+ visibleColumns.map((r) => r.id)
51
+ )
52
+ : advancedFilterFields || [];
53
+ const lowerSearchText = text.toLowerCase().trim();
54
+ if (!lowerSearchText || columns_to_search.length === 0) return rows;
55
+ return rows.filter((p) => {
56
+ for (let index = 0; index < columns_to_search.length; index++) {
57
+ const col = columns_to_search[index];
58
+ const v = (p[col] + '').toLowerCase(); //value of column
59
+ if (v && v.indexOf(lowerSearchText) != -1) {
60
+ //if one column is matching then the row is a match
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ });
66
+ }, []);
67
+
68
+ const run_search = React.useMemo(
69
+ () =>
70
+ debounce(async (text: string) => {
71
+ const id = ++searchId.current;
72
+ const rows = await get_search_rows();
73
+ if (id !== searchId.current || !Array.isArray(rows)) return; //a newer search started
74
+ set_searchResult(filter_rows(rows, text));
75
+ }, SEARCH_DELAY),
76
+ []
77
+ );
78
+ React.useEffect(() => () => run_search.cancel(), [run_search]);
79
+
80
+ React.useEffect(() => {
81
+ if (search) {
82
+ run_search(search);
83
+ return;
84
+ }
85
+ run_search.cancel();
86
+ searchId.current++;
87
+ set_searchResult(null);
88
+ }, [search, props.dataSource]);
89
+
90
+ const { advancedFilterLabel, advancedFilterPlaceholder } = props;
91
+ const textPlaceholder =
92
+ typeof advancedFilterPlaceholder === 'string' || typeof advancedFilterPlaceholder === 'number'
93
+ ? String(advancedFilterPlaceholder)
94
+ : undefined;
95
+ // custom HTML cannot go in the native placeholder attribute, so it is shown over the empty input
96
+ const nodePlaceholder =
97
+ textPlaceholder === undefined &&
98
+ advancedFilterPlaceholder !== undefined &&
99
+ advancedFilterPlaceholder !== null &&
100
+ typeof advancedFilterPlaceholder !== 'boolean'
101
+ ? advancedFilterPlaceholder
102
+ : undefined;
103
+ const input = (
104
+ <input
105
+ type="text"
106
+ name="name"
107
+ value={search}
108
+ placeholder={textPlaceholder}
109
+ onChange={(e) => set_search(e.target.value)}
110
+ />
111
+ );
112
+
113
+ const element = (
114
+ <>
115
+ <form className={'azgrid-searcharea'}>
116
+ <label>
117
+ {advancedFilterLabel ?? 'Search'}
118
+ {nodePlaceholder === undefined ? (
119
+ input
120
+ ) : (
121
+ <span style={{ position: 'relative', display: 'inline-block' }}>
122
+ {input}
123
+ {search ? null : (
124
+ <span
125
+ style={{
126
+ position: 'absolute',
127
+ left: 8,
128
+ top: 0,
129
+ bottom: 0,
130
+ display: 'flex',
131
+ alignItems: 'center',
132
+ pointerEvents: 'none',
133
+ opacity: 0.6,
134
+ }}
135
+ >
136
+ {nodePlaceholder}
137
+ </span>
138
+ )}
139
+ </span>
140
+ )}
141
+ </label>
142
+ </form>
143
+ </>
144
+ );
145
+ return { filteredData: searchResult ?? trackedDataSource, element };
146
+ }
@@ -13,6 +13,8 @@ import {
13
13
  TypeRowProps,
14
14
  TypeRowSelection,
15
15
  TypeComputedProps,
16
+ TypeSortInfo,
17
+ TypeGroupBy,
16
18
  } from '@inovua/reactdatagrid-community/types';
17
19
 
18
20
  import { TypeOnSelectionChangeArg } from '@inovua/reactdatagrid-community/types/TypeDataGridProps';
@@ -58,10 +60,22 @@ export type AzGridOptions = Partial<TypeDataGridProps> & {
58
60
  /** extra DOM props for the header row */
59
61
  headerProps?: { [key: string]: any };
60
62
  };
63
+ /** arguments passed to a dataSource function */
64
+ export type AzDataSourceParams = {
65
+ skip?: number;
66
+ limit?: number;
67
+ /** current header filters, filter.value is an array of selected ids for a multi-select SelectFilter */
68
+ filterValue?: TypeFilterValue;
69
+ sortInfo?: TypeSortInfo;
70
+ groupBy?: TypeGroupBy;
71
+ cached_data: React.MutableRefObject<any>;
72
+ last_cache_info: React.MutableRefObject<any>;
73
+ };
74
+ export type AzDataSourceFunc = (args: AzDataSourceParams) => any[] | Promise<any[] | any>;
61
75
  /* -------------------------------------------------------------------------- */
62
76
  export type AzGridProps = {
63
- dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
64
- details_dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>); //details is rendered under each row
77
+ dataSource?: any[] | AzDataSourceFunc;
78
+ details_dataSource?: any[] | AzDataSourceFunc; //details is rendered under each row
65
79
  details_columns?: LooseColumnDefinition[];
66
80
  details_filter?: FilterValues;
67
81
 
@@ -102,6 +116,10 @@ export type AzGridProps = {
102
116
  //
103
117
  editable?: boolean | ((editValue: any, cellProps: any) => boolean);
104
118
  advancedFilterFields?: string[];
119
+ /** Label of the quick-search box shown with `advancedFilterFields`. Default "Search". */
120
+ advancedFilterLabel?: React.ReactNode;
121
+ /** Placeholder of the quick-search input. A string is used as the native placeholder, any other node (custom HTML) is shown over the empty input. */
122
+ advancedFilterPlaceholder?: React.ReactNode;
105
123
  //
106
124
 
107
125
  loading?: boolean;