@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
300 lines (299 loc) • 12.4 kB
JavaScript
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { useAdaptable } from '../AdaptableContext';
import usePrevious from '../../components/utils/usePrevious';
import { DataChangeHistoryModuleId } from '../../Utilities/Constants/ModuleConstants';
import { buildActionColumnButton } from './buildActionColumnButton';
import { AdaptableAgGrid } from '../../agGrid/AdaptableAgGrid';
export const DataChangeHistoryGrid = (props) => {
const { changeHistoryLog, adaptableContainerId, agGridContainerId, onUndoChange, onClearRow } = props;
const mainAdaptableInstance = useAdaptable();
const [_adaptableApi, setAdaptableApi] = useState(null);
const adaptableApiRef = useRef(null);
adaptableApiRef.current = _adaptableApi;
const undoChangeEnabled = mainAdaptableInstance.api.entitlementApi.isModuleFullEntitlement(DataChangeHistoryModuleId);
const previousChangeHistoryLog = usePrevious(changeHistoryLog, changeHistoryLog);
// initialize Adaptable grid
useEffect(() => {
const initializeAdaptableGrid = async () => {
const gridOptions = buildGridOptions(mainAdaptableInstance, changeHistoryLog);
const adaptableOptions = buildAdaptableOptions(mainAdaptableInstance, adaptableContainerId, agGridContainerId, undoChangeEnabled, onUndoChange, onClearRow);
const modules = mainAdaptableInstance.getAgGridRegisteredModules();
const dataChangeHistoryAdaptableApi = await AdaptableAgGrid._initInternal({
variant: 'vanilla',
adaptableOptions,
gridOptions,
modules,
});
setAdaptableApi(dataChangeHistoryAdaptableApi);
};
initializeAdaptableGrid();
return () => {
requestAnimationFrame(() => {
adaptableApiRef.current?.internalApi
.getAdaptableInstance()
.destroy({ unmount: true, destroyAgGrid: true });
});
};
}, []);
// update row data
useEffect(() => {
const adaptableApi = adaptableApiRef.current;
if (!adaptableApi) {
// initial render
return;
}
const addedChangeKeys = Object.keys(changeHistoryLog).filter((newChangeKey) => {
const previousChangeWithSameKey = previousChangeHistoryLog[newChangeKey];
if (!previousChangeWithSameKey) {
return true;
}
const addedChange = changeHistoryLog[newChangeKey];
return addedChange.changedAt !== previousChangeWithSameKey.changedAt;
});
const removedChangeKeys = Object.keys(previousChangeHistoryLog).filter((previousChangeKey) => {
const newChangeWithSameKey = changeHistoryLog[previousChangeKey];
if (!newChangeWithSameKey) {
return true;
}
const previousChange = previousChangeHistoryLog[previousChangeKey];
return previousChange.changedAt !== newChangeWithSameKey.changedAt;
});
const addedChangeHistoryLogs = filterChangeHistoryLog(changeHistoryLog, addedChangeKeys);
const removedChangeHistoryLogs = filterChangeHistoryLog(previousChangeHistoryLog, removedChangeKeys);
// here we access directly the AG Grid API, but this is OK because:
// 1. this method (gridAPi.applyTransaction() is NOT accessible otherwise and making it public opens other cans of worms
// 2. this UI will be reimplemented with Infinite Table anyway
const addedRows = mapChangeHistoryRowData(addedChangeHistoryLogs, mainAdaptableInstance);
const removedRows = mapChangeHistoryRowData(removedChangeHistoryLogs, mainAdaptableInstance);
adaptableApi.agGridApi.applyTransactionAsync({
add: addedRows,
remove: removedRows,
});
}, [changeHistoryLog]);
return React.createElement(React.Fragment, null);
};
const buildAdaptableOptions = (mainAdaptableInstance, adaptableContainerId, agGridContainerId, undoChangeEnabled, onUndoChange, onClearRow) => {
const mainAdaptableOptions = mainAdaptableInstance.adaptableOptions;
const mainAdaptableTheme = mainAdaptableInstance.api.themeApi.getCurrentTheme();
const undoRowNode = (rowNode) => {
const rowData = rowNode?.data;
const changeKey = rowData?.['changeKey'];
changeKey && onUndoChange(changeKey);
};
const clearRow = (rowNode) => {
const rowData = rowNode?.data;
const changeKey = rowData?.['changeKey'];
changeKey && onClearRow(changeKey);
};
const actionColumnButton = buildActionColumnButton(mainAdaptableInstance.adaptableOptions?.dataChangeHistoryOptions, mainAdaptableInstance.api, undoRowNode, clearRow);
let actionColumnOptions = undefined;
if (actionColumnButton) {
actionColumnOptions = {
actionColumns: [
{
columnId: 'undoActionColumn',
friendlyName: ' ',
rowScope: {
ExcludeGroupRows: true,
ExcludeSummaryRows: true,
},
actionColumnSettings: { suppressMenu: true, suppressMovable: true },
actionColumnButton,
},
],
};
}
const options = {
primaryKey: 'primaryKey',
licenseKey: mainAdaptableOptions.licenseKey,
userName: `${mainAdaptableOptions.userName}`,
adaptableId: `${mainAdaptableOptions.adaptableId}::DataChangeHistory`,
containerOptions: {
adaptableContainer: adaptableContainerId,
agGridContainer: agGridContainerId,
},
entitlementOptions: {
defaultAccessLevel: 'Hidden',
},
actionColumnOptions: actionColumnOptions,
initialState: {
Layout: {
Revision: Date.now(),
CurrentLayout: 'DataChangeHistoryLayout',
Layouts: [
{
Name: 'DataChangeHistoryLayout',
TableColumns: [
'changeInfo.primaryKeyValue',
'changedColumnLabel',
'changeInfo.oldValue',
'changeInfo.newValue',
'changeInfo.changedAt',
'changeTriggerLabel',
'undoActionColumn',
],
ColumnSorts: [
{
ColumnId: 'changeInfo.changedAt',
SortOrder: 'Desc',
},
],
ColumnWidths: {
undoActionColumn: 110,
},
ColumnPinning: {
undoActionColumn: 'right',
},
AutoSizeColumns: true,
},
],
},
FormatColumn: {
Revision: Date.now(),
FormatColumns: [
{
Scope: {
ColumnIds: ['changeInfo.changedAt'],
},
DisplayFormat: {
Formatter: 'DateFormatter',
Options: {
Pattern: `${mainAdaptableOptions.userInterfaceOptions?.dateInputOptions?.dateFormat} HH:mm:ss`,
},
},
},
],
},
Theme: {
Revision: Date.now(),
CurrentTheme: mainAdaptableTheme,
},
},
};
if (!undoChangeEnabled) {
const layoutColumns = options.initialState.Layout.Layouts[0].TableColumns;
if (layoutColumns.includes('undoActionColumn')) {
// remove action column from layout
options.initialState.Layout.Layouts[0].TableColumns = layoutColumns.filter((columnId) => columnId !== 'undoActionColumn');
}
}
return options;
};
const buildGridOptions = (mainAdaptableInstance, changeHistoryLog) => {
const mainPrimaryKeyColumnHeader = mainAdaptableInstance.api.columnApi.getPrimaryKeyColumn()?.friendlyName ?? 'Row ID';
const options = {
loading: false,
overlayNoRowsTemplate: '<div style="font-size: var(--ab-font-size-2);color: var(--ab-color-text-on-primary);">No Data Changes</div>',
defaultColDef: {
floatingFilter: true,
filter: true,
sortable: true,
resizable: true,
enableRowGroup: true,
editable: false,
enablePivot: false,
enableValue: false,
lockPinned: true,
// default set to true to pass this to the action column
suppressAutoSize: true,
menuTabs: ['generalMenuTab', 'filterMenuTab'],
},
autoGroupColumnDef: {
sortable: true,
},
columnDefs: [
{
field: 'primaryKey',
cellDataType: 'text',
hide: true,
},
{
headerName: `Row (${mainPrimaryKeyColumnHeader})`,
field: 'changeInfo.primaryKeyValue',
cellDataType: 'text',
suppressAutoSize: false,
flex: 3,
initialFlex: 3,
},
{
headerName: 'Column',
field: 'changedColumnLabel',
cellDataType: 'text',
suppressAutoSize: false,
flex: 3,
initialFlex: 3,
},
{
headerName: 'Previous',
field: 'changeInfo.oldValue',
cellDataType: 'text',
suppressAutoSize: false,
flex: 3,
initialFlex: 3,
},
{
headerName: 'New',
field: 'changeInfo.newValue',
cellDataType: 'text',
suppressAutoSize: false,
flex: 3,
initialFlex: 3,
},
{
headerName: 'Changed',
field: 'changeInfo.changedAt',
cellDataType: 'date',
suppressAutoSize: false,
flex: 3,
initialFlex: 3,
},
{
headerName: 'Source',
field: 'changeTriggerLabel',
cellDataType: 'text',
suppressAutoSize: false,
flex: 2,
initialFlex: 2,
},
],
rowData: mapChangeHistoryRowData(changeHistoryLog, mainAdaptableInstance),
cellSelection: true,
suppressColumnVirtualisation: false,
sideBar: false,
rowSelection: {
mode: 'multiRow',
},
skipHeaderOnAutoSize: true,
};
return options;
};
const mapChangeHistoryRowData = (changeHistoryLog = {}, mainAdaptableInstance) => {
return Object.entries(changeHistoryLog).map(([changeKey, changeInfo]) => {
return {
primaryKey: `${changeKey}::${changeInfo.changedAt}`,
changeKey,
changeTriggerLabel: getChangeTriggerLabel(changeInfo),
changedColumnLabel: getColumnHeaderLabel(changeInfo.column.columnId, mainAdaptableInstance),
changeInfo,
};
});
};
// memoize headers to avoid unnecessary main grid lookups
const headerMap = new Map();
const getColumnHeaderLabel = (columnId, mainAdaptableInstance) => {
if (!headerMap.has(columnId)) {
headerMap.set(columnId, mainAdaptableInstance.api.columnApi.getColumnWithColumnId(columnId)?.friendlyName ?? columnId);
}
return headerMap.get(columnId);
};
const getChangeTriggerLabel = (changeInfo) => {
return changeInfo.trigger === 'tick' ? 'Ticking' : 'User Edit';
};
const filterChangeHistoryLog = (changeObject, filterValues) => {
const result = {};
filterValues.forEach((allowedKey) => {
result[allowedKey] = changeObject[allowedKey];
});
return result;
};