Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion documentation/ag-grid-docs/src/utils/htaccess/cspRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ const SALESFORCE_FORM_ORIGIN: Record<CspEnv, string> = {
production: 'https://webto.salesforce.com',
};

// The ecommerce checkout renders the Realex/Global Payments Hosted Payment Page
// (rxp-hpp.js) in an iframe and POSTs the payment form to it — sandbox host in
// non-prod, live host in production (see globalPaymentsServiceUrl in the
// ag-grid-ecommerce frontend environments). Governs frame-src and form-action.
const REALEX_HPP_ORIGIN: Record<CspEnv, string> = {
dev: 'https://pay.sandbox.realexpayments.com',
staging: 'https://pay.sandbox.realexpayments.com',
production: 'https://pay.realexpayments.com',
};

// Dev-server-only extras (HMR + cross-port preview). Never emitted for staging
// or production.
const DEV_SCRIPT_SRC = ['https://localhost:4610', 'https://localhost:4611'];
Expand All @@ -61,6 +71,7 @@ export function getCspDirectives(options: CspOptions): CspDirectives {
const { env } = options;
const trialFormOrigin = options.trialFormOrigin ?? TRIAL_FORM_ORIGIN[env];
const salesforceFormOrigin = SALESFORCE_FORM_ORIGIN[env];
const realexHppOrigin = REALEX_HPP_ORIGIN[env];

const directives: CspDirectives = {
'default-src': [SELF],
Expand All @@ -69,13 +80,16 @@ export function getCspDirectives(options: CspOptions): CspDirectives {
AG_GRID_HOSTS,
'https://plausible.io',
'https://www.googletagmanager.com',
'https://www.google-analytics.com', // Universal Analytics analytics.js (GTM-injected after cookie consent)
'https://cdn.jsdelivr.net',
'https://cdnjs.cloudflare.com',
'https://js.zi-scripts.com', // ZoomInfo tag (injected via GTM)
'https://*.zoominfo.com', // ZoomInfo FormComplete
'https://www.google.com', // reCAPTCHA
'https://www.gstatic.com', // reCAPTCHA
'https://www.youtube.com', // YouTube iframe JS API (loads into the page)
'https://cdn.cookielaw.org', // OneTrust cookie-consent SDK (GTM-injected, prod-only)
'blob:', // ZoomInfo zi-tag.js bootstraps a blob: URL script
UNSAFE_INLINE,
UNSAFE_EVAL,
],
Expand Down Expand Up @@ -105,7 +119,7 @@ export function getCspDirectives(options: CspOptions): CspDirectives {
'https://plausible.io',
'https://*.algolia.net',
'https://*.algolianet.com',
'https://www.google-analytics.com',
'https://*.google-analytics.com', // GA4 incl. regional collect endpoints (region1/2.google-analytics.com)
'https://*.analytics.google.com',
'https://stats.g.doubleclick.net',
'https://flagcdn.com',
Expand All @@ -115,13 +129,18 @@ export function getCspDirectives(options: CspOptions): CspDirectives {
'https://js.zi-scripts.com', // ZoomInfo
'https://*.zoominfo.com', // ZoomInfo
'https://www.google.com', // reCAPTCHA (api2/clr XHR)
'https://cdn.cookielaw.org', // OneTrust config/JSON/asset XHR (GTM-injected, prod-only)
'https://*.onetrust.com', // OneTrust geolocation + consent-receipt endpoints
'https://www.googleapis.com', // Firebase Auth (ecommerce checkout): identitytoolkit REST
'https://securetoken.googleapis.com', // Firebase Auth ID-token refresh
trialFormOrigin,
],
'frame-src': [
SELF,
'https://www.googletagmanager.com',
'https://www.youtube.com',
'https://www.google.com', // reCAPTCHA challenge iframe
realexHppOrigin, // ecommerce checkout: Realex Hosted Payment Page iframe
],
'media-src': [SELF, 'data:', 'blob:', 'https:'],
'worker-src': [SELF, 'blob:'],
Expand All @@ -131,6 +150,7 @@ export function getCspDirectives(options: CspOptions): CspDirectives {
SELF,
trialFormOrigin,
salesforceFormOrigin,
realexHppOrigin, // ecommerce checkout: payment form POST to Realex HPP
'https://codesandbox.io', // example-runner "Open in CodeSandbox" form POST
'https://plnkr.co', // example-runner "Open in Plunker" form POST
],
Expand Down
13 changes: 9 additions & 4 deletions packages/ag-grid-community/src/columns/buildColumnTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface ColumnTreeBuild {
/** Cols keyed by `colId` / `userProvidedColDef` ref / `field`; for O(1) reuse. */
colsByKey: Map<string | ColDef, AgColumn>;
source: ColumnEventType;
/** True = user (re)set the definitions, so reused cols re-apply stateful attrs; see {@link AgColumn.reapplyColDef}. */
newColDefs: boolean;
buildToken: number;
/** Padding-wrapper cache for the editable (hierarchy/calc) path; `null` for pivot result trees
* (a one-shot build that never splices). */
Expand All @@ -48,6 +50,7 @@ export function _buildColumnTree(
existingColsByKey: Map<string | ColDef, AgColumn>,
existingColsById: { readonly [id: string]: AgColumn },
source: ColumnEventType,
newColDefs: boolean,
buildToken: number,
wrapperCache: ColWrapperCache | null
): ColumnTreeBuild {
Expand Down Expand Up @@ -160,7 +163,7 @@ export function _buildColumnTree(
if (isReusableUserCol(existing) && userDef && userDef.colId == null && userDef.field == null) {
allocatedKeys.add(id);
existing.buildToken = buildToken;
existing.reapplyColDef(def, source);
existing.reapplyColDef(def, source, newColDefs);
return existing;
}
continue;
Expand All @@ -177,7 +180,7 @@ export function _buildColumnTree(
const keyed = existingColsByKey.get(base);
if (keyed?.colId === base && isReusableUserCol(keyed)) {
keyed.buildToken = buildToken;
keyed.reapplyColDef(def, source);
keyed.reapplyColDef(def, source, newColDefs);
return keyed;
}
let count = 0;
Expand All @@ -201,7 +204,7 @@ export function _buildColumnTree(
}
if (existing !== undefined) {
existing.buildToken = buildToken;
existing.reapplyColDef(def, source);
existing.reapplyColDef(def, source, newColDefs);
return existing;
}
return _createUserColumn(beans, def, id, primaryColumns, buildToken);
Expand Down Expand Up @@ -230,7 +233,7 @@ export function _buildColumnTree(
}
if (column !== undefined) {
column.buildToken = buildToken;
column.reapplyColDef(def, source);
column.reapplyColDef(def, source, newColDefs);
} else {
const field = def.field;
column = colId == null && field == null ? buildAnonymousColumn(def) : buildKeyedColumn(def, colId, field);
Expand Down Expand Up @@ -275,6 +278,7 @@ export function _buildColumnTree(
groupsById: newGroupsById,
colsByKey: newColsByKey,
source,
newColDefs,
buildToken,
wrapperCache,
};
Expand Down Expand Up @@ -489,6 +493,7 @@ export function _buildColumnTree(
groupsById: newGroupsById,
colsByKey: newColsByKey,
source,
newColDefs,
buildToken,
wrapperCache,
};
Expand Down
17 changes: 9 additions & 8 deletions packages/ag-grid-community/src/columns/columnModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,15 @@ export class ColumnModel extends BeanStub implements NamedBean {

const builder = _buildColumnTree(
beans,
colDefs,
true,
this.colDefGroupsById,
this.colDefColsByKey,
this.colsById,
source,
this.nextBuildToken(),
this.hierarchyWrapperCache
/* defs */ colDefs,
/* primaryColumns */ true,
/* existingGroupsById */ this.colDefGroupsById,
/* existingColsByKey */ this.colDefColsByKey,
/* existingColsById */ this.colsById,
/* source */ source,
/* newColDefs */ newColDefs,
/* buildToken */ this.nextBuildToken(),
/* wrapperCache */ this.hierarchyWrapperCache
);
groupHierarchyColSvc?.contributeTo(builder);
calculatedColsSvc?.contributeTo(builder);
Expand Down
35 changes: 19 additions & 16 deletions packages/ag-grid-community/src/entities/agColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,24 +220,27 @@ export class AgColumn<TValue = any>
return true;
}

/** Re-apply `def` to `column`, keeping its colDef and runtime state in sync. */
public reapplyColDef(def: ColDef, source: ColumnEventType): void {
/** Re-apply `def` to a reused column. Stateful attrs are only (re)applied when the user authored the
* definitions (`newColDefs`); an internal rebuild (e.g. calc-col add) must leave live state intact. */
public reapplyColDef(def: ColDef, source: ColumnEventType, newColDefs: boolean): void {
const merged = _addColumnDefaultAndTypes(this.beans, def, this.colId);
this.setColDef(merged, def, source);
updateSomeColumnState(
this.beans,
this,
merged.hide,
merged.sort,
merged.sortIndex,
merged.pinned,
merged.flex,
source
);
// Width is owned by the flex layout while flexing, so only set it when not.
const colFlex = this.flex;
if (colFlex == null || colFlex <= 0) {
this.setActualWidth(merged.width ?? this.actualWidth, source);
if (newColDefs) {
updateSomeColumnState(
this.beans,
this,
merged.hide,
merged.sort,
merged.sortIndex,
merged.pinned,
merged.flex,
source
);
// Read `flex` after the state update so a flex→fixed switch applies before width.
const colFlex = this.flex;
if (colFlex == null || colFlex <= 0) {
this.setActualWidth(merged.width ?? this.actualWidth, source);
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/ag-grid-community/src/rendering/cell/cellCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,10 @@ export class CellCtrl extends BeanStub {
// non of {field, valueGetter, showRowGroup} is bad in the users application, however for this edge case, it's
// best always refresh and take the performance hit rather than never refresh and users complaining in support
// that cells are not updating.
const noValueProvided = field == null && valueGetter == null && showRowGroup == null;
// a calculated column has no field/valueGetter/showRowGroup but DOES have a value (its
// expression), so it must not count as value-less here — otherwise it force-refreshes every
// pass and flashes on changes to unrelated columns instead of only when its value changes.
const noValueProvided = field == null && valueGetter == null && showRowGroup == null && !column.isCalculatedCol;

const newData = params?.newData ?? false;
const forceRefresh = noValueProvided || (params && (params.force || newData));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,18 @@ export class CalculatedColumnForm extends Component {
private setupFormFields(): void {
const translate = this.getLocaleTextFunc();

this.eTitle.setLabel(translate('calculatedColumnTitle', 'Title')).setValue(this.draft.headerName, true);
this.eTitle
.setLabel(translate('calculatedColumnTitle', 'Title'))
.setLabelAlignment('top')
.setValue(this.draft.headerName, true);
this.eType
.setLabel(translate('calculatedColumnType', 'Type'))
.setLabelAlignment('top')
.addOptions(this.dataTypeOptions)
.setValue(this.draft.cellDataType, true);
this.eExpression
.setLabel(translate('calculatedColumnExpression', 'Expression'))
.setLabelAlignment('top')
.setInputPlaceholder(translate('calculatedColumnExpressionPlaceholder', 'Type here'))
.setRows(3)
.setValue(this.draft.calculatedExpression, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,9 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
// missing/removed anchor (or none) falls back to a plain append.
const source = build.source;
const buildToken = build.buildToken;
const newColDefs = build.newColDefs;
dynamicColumns.forEach((dc, colId) => {
const agCol = this.getOrCreateAgColumn(dc, colId, buildToken, source);
const agCol = this.getOrCreateAgColumn(dc, colId, buildToken, source, newColDefs);
agCol.buildToken = buildToken; // So the post-build sweep keeps the col alive.
const anchorId = dc.anchorColId;
if (anchorId != null && anchorId !== colId && staticColOverrides.get(anchorId) !== null) {
Expand Down Expand Up @@ -389,15 +390,16 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
dc: DynamicCalculatedColumn,
colId: string,
buildToken: number,
source: ColumnEventType
source: ColumnEventType,
newColDefs: boolean
): AgColumn {
const beans = this.beans;
const existing = dc.instance;
if (existing !== null) {
// Reuse the owned instance (always alive: contributed/stamped every refresh, nulled when
// parked/removed). Restamp + refresh colDef in case expression/cellDataType changed.
existing.buildToken = buildToken;
existing.reapplyColDef(dc.colDef, source);
existing.reapplyColDef(dc.colDef, source, newColDefs);
return existing;
}
const agCol = _createUserColumn(beans, dc.colDef, colId, true, buildToken);
Expand Down Expand Up @@ -527,9 +529,9 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
title: this.getLocaleTextFunc()('calculatedColumn', 'Calculated Column'),
component: form,
width: 300,
height: 320,
height: 345,
minWidth: 300,
minHeight: 280,
minHeight: 345,
centered: true,
movable: true,
resizable: true,
Expand Down
14 changes: 7 additions & 7 deletions packages/ag-grid-enterprise/src/formula/formulaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe
if (node.rowPinned != null && node.pinnedSibling) {
return;
}
// evicts this row's and its pinned sibling's own formulas so same-row calculated
// columns re-query their expression during the normal row refresh.
// Evict this row's (and its pinned sibling's) formulas so same-row calculated columns
// re-evaluate, then invalidate every cached value — an editable formula in another row
// may reference the changed cell. The refresh neither forces nor suppresses flash, so
// dependent cells flash on a genuine value change in step with the edited column.
this.dropRow(node);
if (this.active) {
this.bumpValueCacheAndRefresh();
}
this.bumpValueCacheAndRefresh(false);
};
const onNewColumnsLoaded = () => {
if (!this.isEvaluationActive()) {
Expand Down Expand Up @@ -374,9 +374,9 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe
* Bulk-invalidate every cached value via a version bump (ASTs preserved) and repaint.
* O(1); entries become stale on next read.
*/
private bumpValueCacheAndRefresh(): void {
private bumpValueCacheAndRefresh(forceRefresh: boolean = true): void {
this.valueCacheVersion++;
this.beans.rowRenderer.refreshCells(REFRESH_CELLS_PARAMS);
this.beans.rowRenderer.refreshCells(forceRefresh ? REFRESH_CELLS_PARAMS : undefined);
}

/**
Expand Down
18 changes: 10 additions & 8 deletions packages/ag-grid-enterprise/src/pivot/pivotResultColsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,16 @@ export class PivotResultColsService extends BeanStub implements NamedBean, IPivo
const buildToken = beans.colModel.nextBuildToken();
const balanced = _buildColumnTree(
beans,
colDefs,
false,
this.pivotGroupsById,
this.pivotColsByKey,
beans.colModel.colsById,
source,
buildToken,
null
/* defs */ colDefs,
/* primaryColumns */ false,
/* existingGroupsById */ this.pivotGroupsById,
/* existingColsByKey */ this.pivotColsByKey,
/* existingColsById */ beans.colModel.colsById,
/* source */ source,
// Generated cols: re-apply their stateful attrs each pivot rebuild (pre-live-state-flag behaviour).
/* newColDefs */ true,
/* buildToken */ buildToken,
/* wrapperCache */ null
);
// `previousTree` (not `currentPivotTree`) covers the clear/restore window where `currentPivotTree` is
// null but saved bean refs survive. Skip the sweep when the tree is missing or unchanged.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('column-model rewrite edge cases', () => {
afterEach(() => {
pivotGridsManager.reset();
gridsManager.reset();
vi.restoreAllMocks();
});

const defColIds = (defs: (ColDef | any)[] | undefined): string[] => (defs ?? []).map((d) => d.colId ?? d.field);
Expand Down Expand Up @@ -54,6 +55,8 @@ describe('column-model rewrite edge cases', () => {
// A pivot build must not reuse a user (primary) column whose colId collides with a generated pivot
// colId — reuse is scoped to the same build kind (primary vs pivot result), not just colKind 'user'.
test('pivot build does not reuse a user column that shares a generated pivot colId', async () => {
// The colliding colId is expected to raise warning #273 (colId suffixed) — capture and assert it.
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const api = pivotGridsManager.createGrid('g', {
columnDefs: [
{ field: 'country', rowGroup: true },
Expand All @@ -76,6 +79,9 @@ describe('column-model rewrite edge cases', () => {
const pivotResult = api.getPivotResultColumns() ?? [];
expect(pivotResult).not.toContain(userCol);
expect(pivotResult.some((c) => c.getColId() === 'pivot_b_x_total_1')).toBe(true);

// Verify the suffixing warning was actually raised (not silently swallowed).
expect(warnSpy.mock.calls.some((args) => String(args[0]).includes('warning #273'))).toBe(true);
});

// P2-2: colId is imperative for REUSE — a column with a colId is never reused by field. Changing a
Expand Down
Loading
Loading