@lowdefy/blocks-aggrid 5.6.0 → 6.0.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.
@@ -16,7 +16,7 @@
16
16
  import { type } from '@lowdefy/helpers';
17
17
  import { getCellRenderer } from './cellRenderers/index.js';
18
18
  import createEllipsisCell from './cellRenderers/EllipsisCell.js';
19
- function applyEllipsis(colDef, ellipsis) {
19
+ function applyEllipsis(colDef, ellipsis, makeEllipsisRenderer) {
20
20
  if (!type.isInt(ellipsis) || ellipsis < 1) return colDef;
21
21
  const clampClass = `lf-ellipsis-${Math.min(ellipsis, 6)}`;
22
22
  const existingClass = colDef.cellClass;
@@ -34,7 +34,7 @@ function applyEllipsis(colDef, ellipsis) {
34
34
  // ag-grid's internal cell wrappers. Skip if a cellRenderer is already set
35
35
  // (user or built-in cell.type takes precedence and can opt in via CSS).
36
36
  if (!colDef.cellRenderer) {
37
- next.cellRenderer = createEllipsisCell(ellipsis);
37
+ next.cellRenderer = makeEllipsisRenderer();
38
38
  }
39
39
  return next;
40
40
  }
@@ -66,41 +66,76 @@ function applyAlignment(colDef, cell) {
66
66
  headerClass
67
67
  };
68
68
  }
69
- function buildCellRenderer({ cell, methods, components }) {
70
- const Renderer = getCellRenderer(cell?.type);
71
- if (!Renderer) return undefined;
72
- // ag-grid calls the renderer as a React function component when returned directly.
73
- return function CellRendererAdapter(params) {
74
- return Renderer({
75
- ...params,
76
- cellConfig: cell,
77
- methods,
78
- components
79
- });
80
- };
69
+ // A cellRenderer is a React element type, so a new function is a different
70
+ // component: CellCtrl.refreshCellRenderer bails when `cellRendererClass !==
71
+ // componentClass`, and the React cell comp re-keys. Building the renderers fresh on
72
+ // every render therefore unmounted every cell whenever anything re-rendered the
73
+ // block, destroying whatever a cell was holding — an open popup, a focused input, a
74
+ // half-typed value in the selector / textInput / paragraphInput cells.
75
+ //
76
+ // So the adapter installed on the colDef is created once per column and kept, and
77
+ // the closure it calls is replaced in place. ag-grid keeps the cell; the cell
78
+ // renders the current config.
79
+ function stableRenderer(entry, slot, render) {
80
+ let stable = entry[slot];
81
+ if (!stable) {
82
+ const box = {
83
+ render
84
+ };
85
+ // ag-grid calls the renderer as a React function component when returned directly.
86
+ function CellRendererAdapter(params) {
87
+ return box.render(params);
88
+ }
89
+ stable = {
90
+ box,
91
+ Adapter: CellRendererAdapter
92
+ };
93
+ entry[slot] = stable;
94
+ }
95
+ stable.box.render = render;
96
+ return stable.Adapter;
97
+ }
98
+ // Keyed by colId or field so an adapter survives a column reorder, falling back to
99
+ // position for columns that declare neither. A key already taken in this pass gets a
100
+ // suffix, so two columns sharing a field do not share an adapter.
101
+ function colKey(col, index, prefix, seen) {
102
+ const id = type.isString(col.colId) ? col.colId : type.isString(col.field) ? col.field : `${index}`;
103
+ const base = `${prefix}${id}`;
104
+ let key = base;
105
+ let n = 1;
106
+ while(seen.has(key)){
107
+ key = `${base}#${n}`;
108
+ n += 1;
109
+ }
110
+ seen.add(key);
111
+ return key;
81
112
  }
82
- function recProcessColDefs(columnDefs, methods, components) {
83
- return columnDefs.map((col)=>{
113
+ function recProcessColDefs(columnDefs, methods, components, cache, seen, prefix) {
114
+ return columnDefs.map((col, index)=>{
115
+ const key = colKey(col, index, prefix, seen);
116
+ const entry = cache.get(key) ?? {};
117
+ cache.set(key, entry);
84
118
  const newColDef = {};
85
119
  if (type.isArray(col.children)) {
86
- newColDef.children = recProcessColDefs(col.children, methods, components);
120
+ newColDef.children = recProcessColDefs(col.children, methods, components, cache, seen, `${key}/`);
87
121
  }
88
122
  if (type.isObject(col.cell) && type.isString(col.cell.type)) {
89
- const renderer = buildCellRenderer({
90
- cell: col.cell,
91
- methods,
92
- components
93
- });
94
- if (renderer) {
95
- newColDef.cellRenderer = renderer;
123
+ const Renderer = getCellRenderer(col.cell.type);
124
+ if (Renderer) {
125
+ const cell = col.cell;
126
+ newColDef.cellRenderer = stableRenderer(entry, 'cell', (params)=>Renderer({
127
+ ...params,
128
+ cellConfig: cell,
129
+ methods,
130
+ components
131
+ }));
96
132
  }
97
133
  } else if (type.isFunction(col.cellRenderer)) {
98
- newColDef.cellRenderer = (params)=>{
99
- return renderHtml({
100
- html: col.cellRenderer(params),
134
+ const cellRenderer = col.cellRenderer;
135
+ newColDef.cellRenderer = stableRenderer(entry, 'cell', (params)=>renderHtml({
136
+ html: cellRenderer(params),
101
137
  methods
102
- });
103
- };
138
+ }));
104
139
  }
105
140
  const merged = {
106
141
  ...col,
@@ -110,10 +145,18 @@ function recProcessColDefs(columnDefs, methods, components) {
110
145
  delete merged.cell;
111
146
  delete merged.ellipsis;
112
147
  const aligned = applyAlignment(merged, col.cell);
113
- return applyEllipsis(aligned, col.ellipsis);
148
+ return applyEllipsis(aligned, col.ellipsis, ()=>stableRenderer(entry, 'ellipsis', createEllipsisCell(col.ellipsis)));
114
149
  });
115
150
  }
116
- function processColDefs(columnDefs = [], methods, components) {
117
- return recProcessColDefs(columnDefs, methods, components);
151
+ function processColDefs(columnDefs = [], methods, components, cache = new Map()) {
152
+ const seen = new Set();
153
+ const processed = recProcessColDefs(columnDefs, methods, components, cache, seen, '');
154
+ // Drop columns that are no longer defined, so a grid whose columns come and go does
155
+ // not accumulate adapters. A column that returns gets a fresh one, which is correct
156
+ // — its cells were unmounted with it.
157
+ for (const key of cache.keys()){
158
+ if (!seen.has(key)) cache.delete(key);
159
+ }
160
+ return processed;
118
161
  }
119
162
  export default processColDefs;
@@ -0,0 +1,65 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { useEffect, useRef } from 'react';
16
+ import { type } from '@lowdefy/helpers';
17
+ import processColDefs from './processColDefs.js';
18
+ // Fingerprint the authored columnDefs, functions included. JSON.stringify drops
19
+ // function values unless a replacer returns something for them, and a cellRenderer or
20
+ // valueGetter built by an operator is a new function on every evaluation — so identity
21
+ // is the only signal available that one of them changed.
22
+ function fingerprint(columnDefs) {
23
+ const functions = [];
24
+ const json = JSON.stringify(columnDefs, (_key, value)=>{
25
+ if (type.isFunction(value)) {
26
+ functions.push(value);
27
+ return `__fn__${functions.length - 1}`;
28
+ }
29
+ return value;
30
+ });
31
+ return {
32
+ json,
33
+ functions
34
+ };
35
+ }
36
+ function unchanged(previous, next) {
37
+ if (!previous) return false;
38
+ if (previous.json !== next.json) return false;
39
+ if (previous.functions.length !== next.functions.length) return false;
40
+ return previous.functions.every((fn, index)=>fn === next.functions[index]);
41
+ }
42
+ // Cell renderers keep their identity across renders (see processColDefs), which is
43
+ // what stops ag-grid destroying a cell mid-interaction — but it also means nothing
44
+ // replaces a cell when its column definition changes. ag-grid refreshes body cells on
45
+ // a data change; it does not listen for colDefChanged. So ask it: refreshCells
46
+ // re-renders the mounted cells in place with the new definition. It passes
47
+ // `newData: false`, so React keeps the instances, and with them whatever the cell
48
+ // was holding.
49
+ function useColDefs({ columnDefs, methods, components, gridRef }) {
50
+ const cache = useRef(new Map());
51
+ const previous = useRef();
52
+ const processed = processColDefs(columnDefs, methods, components, cache.current);
53
+ useEffect(()=>{
54
+ const next = fingerprint(columnDefs);
55
+ const first = previous.current === undefined;
56
+ const same = unchanged(previous.current, next);
57
+ previous.current = next;
58
+ if (first || same) return;
59
+ gridRef.current?.api?.refreshCells({
60
+ force: true
61
+ });
62
+ });
63
+ return processed;
64
+ }
65
+ export default useColDefs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-aggrid",
3
- "version": "5.6.0",
3
+ "version": "6.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "AgGrid Blocks for Lowdefy.",
6
6
  "homepage": "https://lowdefy.com",
@@ -44,25 +44,25 @@
44
44
  "dist/*"
45
45
  ],
46
46
  "dependencies": {
47
- "@lowdefy/block-utils": "5.6.0",
48
- "@lowdefy/blocks-antd": "5.6.0",
49
- "@lowdefy/helpers": "5.6.0",
47
+ "@lowdefy/block-utils": "6.0.0",
48
+ "@lowdefy/blocks-antd": "6.0.0",
49
+ "@lowdefy/helpers": "6.0.0",
50
50
  "ag-grid-community": "33.3.2",
51
51
  "ag-grid-react": "33.3.2"
52
52
  },
53
53
  "peerDependencies": {
54
- "@ant-design/icons": ">=6",
54
+ "@ant-design/icons": "6.1.0",
55
55
  "antd": ">=6",
56
- "dayjs": ">=1.11",
56
+ "dayjs": "1.11.20",
57
57
  "react": ">=18",
58
58
  "react-dom": ">=18"
59
59
  },
60
60
  "devDependencies": {
61
- "@lowdefy/block-dev-e2e": "5.6.0",
62
- "@lowdefy/e2e-utils": "5.6.0",
63
- "@playwright/test": "1.50.1",
64
- "@swc/cli": "0.8.0",
65
- "@swc/core": "1.15.18",
61
+ "@lowdefy/block-dev-e2e": "6.0.0",
62
+ "@lowdefy/e2e-utils": "6.0.0",
63
+ "@playwright/test": "1.59.1",
64
+ "@swc/cli": "0.8.1",
65
+ "@swc/core": "1.15.32",
66
66
  "copyfiles": "2.4.1"
67
67
  },
68
68
  "publishConfig": {
@@ -71,7 +71,7 @@
71
71
  "scripts": {
72
72
  "build": "swc src --out-dir dist --config-file ../../../../.swcrc --cli-config-file ../../../../.swc-cli.json && pnpm copyfiles",
73
73
  "clean": "rm -rf dist",
74
- "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\"",
74
+ "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.snap\"",
75
75
  "e2e": "playwright test --config e2e/playwright.config.js",
76
76
  "e2e:ui": "playwright test --config e2e/playwright.config.js --ui"
77
77
  }