@linzjs/step-ag-grid 7.6.1 → 7.7.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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@linzjs/step-ag-grid",
3
3
  "repository": "github:linz/step-ag-grid.git",
4
4
  "license": "MIT",
5
- "version": "7.6.1",
5
+ "version": "7.7.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -34,7 +34,7 @@
34
34
  "react-dom": ">=17"
35
35
  },
36
36
  "dependencies": {
37
- "@linzjs/lui": "^17.23.2",
37
+ "@linzjs/lui": ">=17",
38
38
  "ag-grid-community": ">=27",
39
39
  "ag-grid-react": ">=27",
40
40
  "debounce-promise": "^3.1.2",
@@ -52,7 +52,7 @@
52
52
  "bundle": "rollup -c",
53
53
  "stylelint": "stylelint src/**/*.scss src/**/*.css --fix",
54
54
  "css": "sass ./src/styles/index.scss:dist/index.css --no-source-map",
55
- "test": "react-scripts test",
55
+ "test": "jest",
56
56
  "eject": "react-scripts eject",
57
57
  "lint": "eslint ./src --ext .js,.ts,.tsx --fix --cache --ignore-path .gitignore",
58
58
  "storybook": "start-storybook -p 6006",
@@ -120,12 +120,16 @@
120
120
  "eslint-plugin-react": "^7.31.10",
121
121
  "eslint-plugin-react-hooks": "^4.6.0",
122
122
  "eslint-plugin-testing-library": "^5.9.1",
123
+ "jest": "^29.3.1",
124
+ "jest-environment-jsdom": "^29.3.1",
125
+ "jest-expect-message": "^1.1.3",
123
126
  "mkdirp": "^1.0.4",
124
127
  "npm-run-all": "^4.1.5",
125
128
  "postcss": "^8.4.18",
126
129
  "postcss-loader": "^7.0.1",
127
130
  "postcss-scss": "^4.0.5",
128
131
  "prettier": "^2.7.1",
132
+ "react-app-polyfill": "^3.0.0",
129
133
  "react-scripts": "^5.0.1",
130
134
  "rollup": "^3.2.5",
131
135
  "rollup-plugin-copy": "^3.4.0",
@@ -68,7 +68,7 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
68
68
  sortable: !!(props?.field || props?.valueGetter),
69
69
  resizable: true,
70
70
  ...(custom?.editor && {
71
- cellClassRules: GridCellMultiSelectClassRules,
71
+ cellClassRules: custom.multiEdit ? GridCellMultiSelectClassRules : undefined,
72
72
  editable: props.editable ?? true,
73
73
  cellEditor: GenericCellEditorComponentWrapper(custom),
74
74
  }),
@@ -251,7 +251,10 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
251
251
  async () => {
252
252
  // Need to refresh to get spinners to work on all rows
253
253
  gridApi.refreshCells({ rowNodes: props.selectedRows as RowNode[], force: true });
254
- ok = await fnUpdate(selectedRows);
254
+ ok = await fnUpdate(selectedRows).catch((ex) => {
255
+ console.error("Exception during modifyUpdating", ex);
256
+ return false;
257
+ });
255
258
  },
256
259
  );
257
260
 
@@ -31,9 +31,7 @@ export const GridPopoverContextProvider = ({ props, children }: GridPopoverConte
31
31
  async (saveFn: (selectedRows: any[]) => Promise<boolean>, tabDirection: 1 | 0 | -1): Promise<boolean> => {
32
32
  if (hasSaved.current) return true;
33
33
  hasSaved.current = true;
34
- const r = saving ? false : await updatingCells({ selectedRows, field }, saveFn, setSaving, tabDirection);
35
- //if (r) stopEditing();
36
- return r;
34
+ return saving ? false : await updatingCells({ selectedRows, field }, saveFn, setSaving, tabDirection);
37
35
  },
38
36
  [field, saving, selectedRows, updatingCells],
39
37
  );
@@ -75,8 +75,6 @@
75
75
  }
76
76
 
77
77
  .ag-cell.ag-cell-inline-editing {
78
- padding-left: 9px;
79
- padding-right: 9px;
80
78
  bottom: 0;
81
79
  }
82
80
 
@@ -84,9 +82,11 @@
84
82
  word-break: break-word;
85
83
  }
86
84
 
87
- .ag-cell-popup-editing,
88
- .ag-selected-for-edit,
85
+ .ag-cell.ag-cell-popup-editing,
86
+ .ag-cell.ag-selected-for-edit,
89
87
  .ag-cell-inline-editing {
88
+ padding-left: 9px;
89
+ padding-right: 9px;
90
90
  background: rgb(72 160 244 / 20%);
91
91
  // These are important, it needs to override ag-grid to work
92
92
  border: 3px solid #48a0f4 !important;
@@ -1,9 +1,16 @@
1
- import { useRef } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
 
3
3
  export const useDeferredPromise = <T>() => {
4
4
  const promiseResolve = useRef<((value: T | PromiseLike<T>) => void) | undefined>();
5
5
  const promiseReject = useRef<() => void>();
6
6
 
7
+ // End promise on unload
8
+ useEffect(() => {
9
+ return () => {
10
+ promiseReject.current && promiseReject.current();
11
+ };
12
+ }, []);
13
+
7
14
  return {
8
15
  invoke: () =>
9
16
  new Promise<T>((resolve, reject) => {
@@ -0,0 +1,95 @@
1
+ import { TextInputValidator, TextInputValidatorProps } from "./textValidator";
2
+ import { GridBaseRow } from "../components/Grid";
3
+
4
+ describe("TextInputValidator", () => {
5
+ test("number format", () => {
6
+ const validate = [
7
+ {
8
+ numberFormat: {},
9
+ tests: [
10
+ ["x", "Must be a valid number"],
11
+ ["1.2e6", "Must be a valid number"],
12
+ ["", null],
13
+ ["1", null],
14
+ ["1.2", null],
15
+ [".2", null],
16
+ ["-1.2", null],
17
+ ["-.2", null],
18
+ ["-.2", null],
19
+ ],
20
+ },
21
+ {
22
+ numberFormat: { geMin: 0, leMax: 10 },
23
+ tests: [
24
+ ["x", "Must be a valid number"],
25
+ ["", null],
26
+ ["1", null],
27
+ ["0", null],
28
+ ["-1", "Must not be less than 0"],
29
+ ["10", null],
30
+ ["10.1", "Must not be greater than 10"],
31
+ ],
32
+ },
33
+ {
34
+ numberFormat: { gtMin: 0, ltMax: 10 },
35
+ tests: [
36
+ ["x", "Must be a valid number"],
37
+ ["", null],
38
+ ["1", null],
39
+ ["0", "Must be greater than 0"],
40
+ ["10", "Must be less than 10"],
41
+ ],
42
+ },
43
+ {
44
+ numberFormat: { precision: 2 },
45
+ tests: [
46
+ ["x", "Must be a valid number"],
47
+ ["", null],
48
+ ["1.22", null],
49
+ ["1.123", "Must have no more than 2 decimal places"],
50
+ ],
51
+ },
52
+ {
53
+ numberFormat: { precision: 0 },
54
+ tests: [
55
+ ["1", null],
56
+ ["1.1", "Must be a whole number"],
57
+ ],
58
+ },
59
+ {
60
+ required: true,
61
+ tests: [
62
+ ["xx", null],
63
+ ["", "Must not be empty"],
64
+ ],
65
+ },
66
+ ] as (TextInputValidatorProps<GridBaseRow> & { tests: [string, string | undefined][] })[];
67
+
68
+ validate.forEach((v, i) => {
69
+ for (const test of v.tests) {
70
+ expect(
71
+ TextInputValidator(v, test[0], { id: 0 }, {}),
72
+ `Test ${i}: "${test[0]}" should return "${test[1]}"`,
73
+ ).toBe(test[1]);
74
+ }
75
+ });
76
+ });
77
+
78
+ test("validator is called", () => {
79
+ const fn = jest.fn();
80
+ TextInputValidator({ invalid: fn }, "", { id: 0 }, {});
81
+ expect(fn).toHaveBeenCalled();
82
+ });
83
+
84
+ test("maxLength", () => {
85
+ expect(TextInputValidator({ maxLength: 2 }, "", { id: 0 }, {})).toBeNull();
86
+ expect(TextInputValidator({ maxLength: 2 }, "aa", { id: 0 }, {})).toBeNull();
87
+ expect(TextInputValidator({ maxLength: 2 }, "aaa", { id: 0 }, {})).toBe("Must be no longer than 2 characters");
88
+ });
89
+
90
+ test("maxBytes", () => {
91
+ expect(TextInputValidator({ maxBytes: 2 }, "", { id: 0 }, {})).toBeNull();
92
+ expect(TextInputValidator({ maxBytes: 2 }, "aa", { id: 0 }, {})).toBeNull();
93
+ expect(TextInputValidator({ maxBytes: 2 }, "a–", { id: 0 }, {})).toBe("Must be no longer than 2 bytes");
94
+ });
95
+ });
@@ -1,11 +1,18 @@
1
1
  import { GridBaseRow } from "../components/Grid";
2
- import { stringByteLengthIsInvalid } from "./util";
2
+ import { isFloat, stringByteLengthIsInvalid } from "./util";
3
3
 
4
4
  export interface TextInputValidatorProps<RowType extends GridBaseRow> {
5
5
  required?: boolean;
6
6
  maxLength?: number;
7
7
  maxBytes?: number;
8
8
  invalid?: (value: string, data: RowType, context: any) => JSX.Element | string | null;
9
+ numberFormat?: {
10
+ precision?: number;
11
+ gtMin?: number;
12
+ geMin?: number;
13
+ ltMax?: number;
14
+ leMax?: number;
15
+ };
9
16
  }
10
17
 
11
18
  export const TextInputValidator = <RowType extends GridBaseRow>(
@@ -18,17 +25,40 @@ export const TextInputValidator = <RowType extends GridBaseRow>(
18
25
 
19
26
  // This can happen because subcomponent is invoked without type safety
20
27
  if (typeof value !== "string") {
21
- console.error("Value is not a string", value);
22
- return null;
28
+ return "Value is not a string";
23
29
  }
24
- if (props.required && value.length === 0) {
25
- return `Some text is required`;
30
+ if (props.required && value == "") {
31
+ return "Must not be empty";
26
32
  }
27
33
  if (props.maxLength && value.length > props.maxLength) {
28
- return `Text must be no longer than ${props.maxLength} characters`;
34
+ return `Must be no longer than ${props.maxLength} characters`;
29
35
  }
30
36
  if (props.maxBytes && stringByteLengthIsInvalid(value, props.maxBytes)) {
31
- return `Text must be no longer than ${props.maxLength} bytes`;
37
+ return `Must be no longer than ${props.maxBytes} bytes`;
38
+ }
39
+ const nf = props.numberFormat;
40
+ if (nf) {
41
+ if (value != "" && !isFloat(value)) {
42
+ return `Must be a valid number`;
43
+ }
44
+ const number = parseFloat(value);
45
+ if (nf.gtMin != null && number <= nf.gtMin) {
46
+ return `Must be greater than ${nf.gtMin}`;
47
+ }
48
+ if (nf.geMin != null && number < nf.geMin) {
49
+ return `Must not be less than ${nf.geMin}`;
50
+ }
51
+ if (nf.ltMax != null && number >= nf.ltMax) {
52
+ return `Must be less than ${nf.ltMax}`;
53
+ }
54
+ if (nf.leMax != null && number > nf.leMax) {
55
+ return `Must not be greater than ${nf.leMax}`;
56
+ }
57
+ if (nf.precision != null && value != "") {
58
+ if (parseFloat(number.toPrecision(nf.precision + 1)) != number) {
59
+ return nf.precision == 0 ? `Must be a whole number` : `Must have no more than ${nf.precision} decimal places`;
60
+ }
61
+ }
32
62
  }
33
63
  if (props.invalid) {
34
64
  return props.invalid(value, data, context);
package/src/utils/util.ts CHANGED
@@ -8,7 +8,7 @@ export const wait = (timeoutMs: number) =>
8
8
  });
9
9
 
10
10
  export const isFloat = (value: string) => {
11
- const regexp = /^-?\d+(\.\d+)?$/;
11
+ const regexp = /^-?\d*(\.\d+)?$/;
12
12
  return regexp.test(value);
13
13
  };
14
14