diff --git a/packages/app-utils/src/storage/LocalWorkspaceStorage.ts b/packages/app-utils/src/storage/LocalWorkspaceStorage.ts index 3a42ee1ae..1c0bb8246 100644 --- a/packages/app-utils/src/storage/LocalWorkspaceStorage.ts +++ b/packages/app-utils/src/storage/LocalWorkspaceStorage.ts @@ -56,6 +56,7 @@ export class LocalWorkspaceStorage implements WorkspaceStorage { truncateNumbersWithPound: false, showEmptyStrings: true, showNullStrings: true, + showExtraGroupColumn: true, defaultNotebookSettings: { isMinimapEnabled: false, }, @@ -103,6 +104,10 @@ export class LocalWorkspaceStorage implements WorkspaceStorage { serverConfigValues, 'showNullStrings' ), + showExtraGroupColumn: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'showExtraGroupColumn' + ), defaultNotebookSettings: serverConfigValues?.get('isMinimapEnabled') !== undefined ? { diff --git a/packages/code-studio/src/settings/FormattingSectionContent.tsx b/packages/code-studio/src/settings/FormattingSectionContent.tsx index a01823b48..0f2bcfcd2 100644 --- a/packages/code-studio/src/settings/FormattingSectionContent.tsx +++ b/packages/code-studio/src/settings/FormattingSectionContent.tsx @@ -28,6 +28,7 @@ import { getTruncateNumbersWithPound, getShowEmptyStrings, getShowNullStrings, + getShowExtraGroupColumn, updateSettings as updateSettingsAction, RootState, WorkspaceSettings, @@ -56,6 +57,7 @@ interface FormattingSectionContentProps { truncateNumbersWithPound: boolean; showEmptyStrings: boolean; showNullStrings: boolean; + showExtraGroupColumn: boolean; updateSettings: (settings: Partial) => void; defaultDecimalFormatOptions: FormatOption; defaultIntegerFormatOptions: FormatOption; @@ -72,6 +74,7 @@ interface FormattingSectionContentState { truncateNumbersWithPound: boolean; showEmptyStrings: boolean; showNullStrings: boolean; + showExtraGroupColumn: boolean; timestampAtMenuOpen: Date; } @@ -113,6 +116,8 @@ export class FormattingSectionContent extends PureComponent< this.handleShowEmptyStringsChange.bind(this); this.handleShowNullStringsChange = this.handleShowNullStringsChange.bind(this); + this.handleShowExtraGroupColumnChange = + this.handleShowExtraGroupColumnChange.bind(this); const { defaultDateTimeFormat, @@ -124,6 +129,7 @@ export class FormattingSectionContent extends PureComponent< truncateNumbersWithPound, showEmptyStrings, showNullStrings, + showExtraGroupColumn, } = props; this.containerRef = React.createRef(); @@ -139,6 +145,7 @@ export class FormattingSectionContent extends PureComponent< truncateNumbersWithPound, showEmptyStrings, showNullStrings, + showExtraGroupColumn, timestampAtMenuOpen: new Date(), }; } @@ -330,6 +337,15 @@ export class FormattingSectionContent extends PureComponent< this.queueUpdate(update); } + handleShowExtraGroupColumnChange(): void { + const { showExtraGroupColumn } = this.state; + const update = { + showExtraGroupColumn: !showExtraGroupColumn, + }; + this.setState(update); + this.queueUpdate(update); + } + commitChanges(): void { const { updateSettings } = this.props; const updates = this.pendingUpdates.reduce( @@ -356,6 +372,7 @@ export class FormattingSectionContent extends PureComponent< truncateNumbersWithPound, showEmptyStrings, showNullStrings, + showExtraGroupColumn, } = this.state; const { @@ -596,6 +613,23 @@ export class FormattingSectionContent extends PureComponent< + +
+ +
+ + Show extra "group" column + +
+
); @@ -614,6 +648,7 @@ const mapStateToProps = ( truncateNumbersWithPound: getTruncateNumbersWithPound(state), showEmptyStrings: getShowEmptyStrings(state), showNullStrings: getShowNullStrings(state), + showExtraGroupColumn: getShowExtraGroupColumn(state), timeZone: getTimeZone(state), defaults: getDefaultSettings(state), }); diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index c0b9e98dc..a9113c3e3 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -20,6 +20,12 @@ module.exports = { 'react/jsx-uses-react': 'error', 'react/jsx-uses-vars': 'error', 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], + 'react-hooks/exhaustive-deps': [ + 'warn', + { + additionalHooks: '(useOnChange)', + }, + ], 'react/react-in-jsx-scope': 'off', 'react/sort-comp': [ 2, diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 989b8ac6c..fb9b1ba1d 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -523,6 +523,7 @@ class IrisGrid extends Component { truncateNumbersWithPound: false, showEmptyStrings: true, showNullStrings: true, + showExtraGroupColumn: true, formatter: EMPTY_ARRAY, decimalFormatOptions: PropTypes.shape({ defaultFormatString: PropTypes.string, @@ -637,6 +638,7 @@ class IrisGrid extends Component { this.truncateNumbersWithPound = false; this.showEmptyStrings = true; this.showNullStrings = true; + this.showExtraGroupColumn = true; // When the loading scrim started/when it should extend to the end of the screen. this.tableSaver = null; @@ -1023,6 +1025,8 @@ class IrisGrid extends Component { showNullStrings: boolean; + showExtraGroupColumn: boolean; + // When the loading scrim started/when it should extend to the end of the screen. loadingScrimStartTime?: number; @@ -1870,6 +1874,7 @@ class IrisGrid extends Component { const showEmptyStrings = settings?.showEmptyStrings ?? true; const showNullStrings = settings?.showNullStrings ?? true; + const showExtraGroupColumn = settings?.showExtraGroupColumn ?? true; const isColumnFormatChanged = !deepEqual( this.globalColumnFormats, @@ -1892,6 +1897,8 @@ class IrisGrid extends Component { const isShowEmptyStringsChanged = this.showEmptyStrings !== showEmptyStrings; const isShowNullStringsChanged = this.showNullStrings !== showNullStrings; + const isShowExtraGroupColumnChanged = + this.showExtraGroupColumn !== showExtraGroupColumn; if ( isColumnFormatChanged || @@ -1900,7 +1907,8 @@ class IrisGrid extends Component { isIntegerFormattingChanged || isTruncateNumbersChanged || isShowEmptyStringsChanged || - isShowNullStringsChanged + isShowNullStringsChanged || + isShowExtraGroupColumnChanged ) { this.globalColumnFormats = globalColumnFormats; this.dateTimeFormatterOptions = dateTimeFormatterOptions; @@ -1909,6 +1917,7 @@ class IrisGrid extends Component { this.truncateNumbersWithPound = truncateNumbersWithPound; this.showEmptyStrings = showEmptyStrings; this.showNullStrings = showNullStrings; + this.showExtraGroupColumn = showExtraGroupColumn; this.updateFormatter({}, forceUpdate); if (isDateFormattingChanged && forceUpdate) { @@ -4810,7 +4819,6 @@ class IrisGrid extends Component { {isVisible && ( { frozenColumns={frozenColumns} columnHeaderGroups={columnHeaderGroups} partitionConfig={partitionConfig} + showExtraGroupColumn={this.showExtraGroupColumn} /> )} {!isMenuShown && ( diff --git a/packages/iris-grid/src/IrisGridModelUpdater.tsx b/packages/iris-grid/src/IrisGridModelUpdater.tsx index 2be2e65a5..844934046 100644 --- a/packages/iris-grid/src/IrisGridModelUpdater.tsx +++ b/packages/iris-grid/src/IrisGridModelUpdater.tsx @@ -1,10 +1,11 @@ /* eslint-disable react/require-default-props */ /* eslint-disable no-param-reassign */ -import React, { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import type { dh } from '@deephaven/jsapi-types'; import { ModelIndex, MoveOperation } from '@deephaven/grid'; import { Formatter, ReverseType, TableUtils } from '@deephaven/jsapi-utils'; import { EMPTY_ARRAY, EMPTY_MAP } from '@deephaven/utils'; +import { useOnChange } from '@deephaven/react-hooks'; import IrisGridUtils from './IrisGridUtils'; import { ColumnName, UITotalsTableConfig, PendingDataMap } from './CommonTypes'; import IrisGridModel from './IrisGridModel'; @@ -13,12 +14,12 @@ import { PartitionConfig, isPartitionedGridModel, } from './PartitionedGridModel'; +import { isIrisGridTreeTableModel } from './IrisGridTreeTableModel'; const COLUMN_BUFFER_PAGES = 1; interface IrisGridModelUpdaterProps { model: IrisGridModel; - modelColumns: readonly dh.Column[]; top: number; bottom: number; left: number | null; @@ -40,164 +41,170 @@ interface IrisGridModelUpdaterProps { pendingRowCount?: number; pendingDataMap?: PendingDataMap; partitionConfig?: PartitionConfig; + showExtraGroupColumn?: boolean; } /** * React component to keep IrisGridModel in sync */ -const IrisGridModelUpdater = React.memo( - ({ - model, - modelColumns, - top, - bottom, - left, - right, - filter, - formatter, - reverseType = TableUtils.REVERSE_TYPE.NONE, - sorts, - customColumns, - movedColumns, - hiddenColumns, - alwaysFetchColumns, - rollupConfig = null, - totalsConfig = null, - selectDistinctColumns = EMPTY_ARRAY, - pendingRowCount = 0, - pendingDataMap = EMPTY_MAP, - frozenColumns, - formatColumns, - columnHeaderGroups, - partitionConfig, - }: IrisGridModelUpdaterProps) => { - const columns = useMemo( - () => - IrisGridUtils.getModelViewportColumns( - modelColumns, - left, - right, - movedColumns, - hiddenColumns, - alwaysFetchColumns, - COLUMN_BUFFER_PAGES - ), - [ - modelColumns, +function IrisGridModelUpdater({ + model, + top, + bottom, + left, + right, + filter, + formatter, + reverseType = TableUtils.REVERSE_TYPE.NONE, + sorts, + customColumns, + movedColumns, + hiddenColumns, + alwaysFetchColumns, + rollupConfig = null, + totalsConfig = null, + selectDistinctColumns = EMPTY_ARRAY, + pendingRowCount = 0, + pendingDataMap = EMPTY_MAP, + frozenColumns, + formatColumns, + columnHeaderGroups, + partitionConfig, + showExtraGroupColumn, +}: IrisGridModelUpdaterProps): JSX.Element | null { + const { isTotalsAvailable, isRollupAvailable } = model; + // Check for showExtraGroupColumn before memoizing columns, since updating it will change the columns + useOnChange(() => { + if (isIrisGridTreeTableModel(model) && showExtraGroupColumn != null) { + model.showExtraGroupColumn = showExtraGroupColumn; + } + }, [model, showExtraGroupColumn]); + + const columns = useMemo( + () => + IrisGridUtils.getModelViewportColumns( + model.columns, left, right, movedColumns, hiddenColumns, alwaysFetchColumns, - ] - ); - - useEffect( - function updateFilter() { - model.filter = filter; - }, - [model, filter] - ); - useEffect( - function updateSorts() { - const sortsForModel = [...sorts]; - if (reverseType !== TableUtils.REVERSE_TYPE.NONE) { - sortsForModel.push(model.dh.Table.reverse()); - } - model.sort = sortsForModel; - }, - [model, sorts, reverseType] - ); - useEffect( - function updateFormatter() { - model.formatter = formatter; - }, - [model, formatter] - ); - useEffect( - function updateCustomColumns() { - if (model.isCustomColumnsAvailable) { - model.customColumns = customColumns; - } - }, - [model, customColumns] - ); - useEffect( - function updateFormatColumns() { - if (model.isFormatColumnsAvailable) { - model.formatColumns = formatColumns; - } - }, - [model, formatColumns] - ); - useEffect( - function updateViewport() { - model.setViewport(top, bottom, columns); - }, - [model, top, bottom, columns] - ); - useEffect( - function updateRollupCOnfig() { - if (model.isRollupAvailable) { - model.rollupConfig = rollupConfig; - } - }, - [model, model.isRollupAvailable, rollupConfig] - ); - useEffect( - function updateSelectDistinctColumns() { - if (model.isSelectDistinctAvailable) { - model.selectDistinctColumns = selectDistinctColumns; - } - }, - [model, selectDistinctColumns] - ); - useEffect( - function updateTotalsConfig() { - if (model.isTotalsAvailable) { - model.totalsConfig = totalsConfig; - } - }, - [model, model.isTotalsAvailable, totalsConfig] - ); - useEffect( - function updatePendingRowCount() { - model.pendingRowCount = pendingRowCount; - }, - [model, pendingRowCount] - ); - useEffect( - function updatePendingDataMap() { - model.pendingDataMap = pendingDataMap; - }, - [model, pendingDataMap] - ); - useEffect( - function updateFrozenColumns() { - if (frozenColumns) { - model.updateFrozenColumns(frozenColumns); - } - }, - [model, frozenColumns] - ); - useEffect( - function updateColumnHeaderGroups() { - model.columnHeaderGroups = columnHeaderGroups; - }, - [model, columnHeaderGroups] - ); - useEffect( - function updatePartitionConfig() { - if (partitionConfig && isPartitionedGridModel(model)) { - model.partitionConfig = partitionConfig; - } - }, - [model, partitionConfig] - ); + COLUMN_BUFFER_PAGES + ), + [ + model.columns, + left, + right, + movedColumns, + hiddenColumns, + alwaysFetchColumns, + ] + ); + useOnChange( + function updateFilter() { + model.filter = filter; + }, + [model, filter] + ); + useOnChange( + function updateSorts() { + const sortsForModel = [...sorts]; + if (reverseType !== TableUtils.REVERSE_TYPE.NONE) { + sortsForModel.push(model.dh.Table.reverse()); + } + model.sort = sortsForModel; + }, + [model, sorts, reverseType] + ); + useOnChange( + function updateFormatter() { + model.formatter = formatter; + }, + [model, formatter] + ); + useOnChange( + function updateCustomColumns() { + if (model.isCustomColumnsAvailable) { + model.customColumns = customColumns; + } + }, + [model, customColumns] + ); + useOnChange( + function updateFormatColumns() { + if (model.isFormatColumnsAvailable) { + model.formatColumns = formatColumns; + } + }, + [model, formatColumns] + ); + useOnChange( + function updateViewport() { + model.setViewport(top, bottom, columns); + }, + [model, top, bottom, columns] + ); + useOnChange( + function updateRollupCOnfig() { + if (isRollupAvailable) { + model.rollupConfig = rollupConfig; + } + }, + [model, isRollupAvailable, rollupConfig] + ); + useOnChange( + function updateSelectDistinctColumns() { + if (model.isSelectDistinctAvailable) { + model.selectDistinctColumns = selectDistinctColumns; + } + }, + [model, selectDistinctColumns] + ); + useOnChange( + function updateTotalsConfig() { + if (isTotalsAvailable) { + model.totalsConfig = totalsConfig; + } + }, + [model, isTotalsAvailable, totalsConfig] + ); + useOnChange( + function updatePendingRowCount() { + model.pendingRowCount = pendingRowCount; + }, + [model, pendingRowCount] + ); + useOnChange( + function updatePendingDataMap() { + model.pendingDataMap = pendingDataMap; + }, + [model, pendingDataMap] + ); + useOnChange( + function updateFrozenColumns() { + if (frozenColumns) { + model.updateFrozenColumns(frozenColumns); + } + }, + [model, frozenColumns] + ); + useOnChange( + function updateColumnHeaderGroups() { + model.columnHeaderGroups = columnHeaderGroups; + }, + [model, columnHeaderGroups] + ); + useOnChange( + function updatePartitionConfig() { + if (partitionConfig && isPartitionedGridModel(model)) { + model.partitionConfig = partitionConfig; + } + }, + [model, partitionConfig] + ); - return null; - } -); + return null; +} IrisGridModelUpdater.displayName = 'IrisGridModelUpdater'; diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.test.ts b/packages/iris-grid/src/IrisGridTreeTableModel.test.ts index af7f11de4..9228a2316 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.test.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.test.ts @@ -6,9 +6,15 @@ const irisGridTestUtils = new IrisGridTestUtils(dh); describe('IrisGridTreeTableModel virtual columns', () => { const expectedVirtualColumn = expect.objectContaining({ - name: '__DH_UI_GROUP__', + constituentType: 'string', + description: 'Key column', displayName: 'Group', + index: -1, + isPartitionColumn: false, isProxy: true, + isSortable: false, + name: '__DH_UI_GROUP__', + type: 'string', }); const columns = irisGridTestUtils.makeColumns(); diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.ts b/packages/iris-grid/src/IrisGridTreeTableModel.ts index a49bece9b..c1a8f3a9d 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.ts @@ -9,13 +9,46 @@ import { import type { dh as DhType } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; import { Formatter, TableUtils } from '@deephaven/jsapi-utils'; -import { assertNotNull } from '@deephaven/utils'; +import { assertNotNull, EventShimCustomEvent } from '@deephaven/utils'; import { UIRow, ColumnName } from './CommonTypes'; import IrisGridTableModelTemplate from './IrisGridTableModelTemplate'; -import { DisplayColumn } from './IrisGridModel'; +import IrisGridModel, { DisplayColumn } from './IrisGridModel'; const log = Log.module('IrisGridTreeTableModel'); +const VirtualGroupColumn = Object.freeze({ + name: '__DH_UI_GROUP__', + displayName: 'Group', + type: TableUtils.dataType.STRING, + constituentType: TableUtils.dataType.STRING, + isPartitionColumn: false, + isSortable: false, + isProxy: true, + description: 'Key column', + index: -1, + filter: () => { + throw new Error('Filter not implemented for virtual column'); + }, + sort: () => { + throw new Error('Sort not implemented virtual column'); + }, + formatColor: () => { + throw new Error('Color not implemented for virtual column'); + }, + get: () => { + throw new Error('get not implemented for virtual column'); + }, + getFormat: () => { + throw new Error('getFormat not implemented for virtual column'); + }, + formatNumber: () => { + throw new Error('formatNumber not implemented for virtual column'); + }, + formatDate: () => { + throw new Error('formatDate not implemented for virtual column'); + }, +}); + export interface UITreeRow extends UIRow { isExpanded: boolean; hasChildren: boolean; @@ -30,6 +63,12 @@ function isLayoutTreeTable(table: DhType.TreeTable): table is LayoutTreeTable { return (table as LayoutTreeTable).layoutHints !== undefined; } +export function isIrisGridTreeTableModel( + tableModel: IrisGridModel +): tableModel is IrisGridTreeTableModel { + return (tableModel as IrisGridTreeTableModel).showExtraGroupColumn != null; +} + class IrisGridTreeTableModel extends IrisGridTableModelTemplate< DhType.TreeTable, UITreeRow @@ -37,6 +76,8 @@ class IrisGridTreeTableModel extends IrisGridTableModelTemplate< /** We keep a virtual column at the front that tracks the "group" that is expanded */ private virtualColumns: DisplayColumn[]; + private showExtraGroupCol = true; + constructor( dh: typeof DhType, table: DhType.TreeTable, @@ -44,49 +85,37 @@ class IrisGridTreeTableModel extends IrisGridTableModelTemplate< inputTable: DhType.InputTable | null = null ) { super(dh, table, formatter, inputTable); + this.virtualColumns = - table.groupedColumns.length > 1 - ? [ - { - name: '__DH_UI_GROUP__', - displayName: 'Group', - type: TableUtils.dataType.STRING, - constituentType: TableUtils.dataType.STRING, - isPartitionColumn: false, - isSortable: false, - isProxy: true, - description: 'Key column', - index: -1, - filter: () => { - throw new Error('Filter not implemented for virtual column'); - }, - sort: () => { - throw new Error('Sort not implemented virtual column'); - }, - formatColor: () => { - throw new Error('Color not implemented for virtual column'); - }, - get: () => { - throw new Error('get not implemented for virtual column'); - }, - getFormat: () => { - throw new Error('getFormat not implemented for virtual column'); - }, - formatNumber: () => { - throw new Error( - 'formatNumber not implemented for virtual column' - ); - }, - formatDate: () => { - throw new Error( - 'formatDate not implemented for virtual column' - ); - }, - }, - ] + this.showExtraGroupColumn && table.groupedColumns.length > 1 + ? [VirtualGroupColumn] : []; } + get showExtraGroupColumn(): boolean { + return this.showExtraGroupCol; + } + + set showExtraGroupColumn(showExtraGroupCol: boolean) { + if (this.showExtraGroupCol === showExtraGroupCol) { + return; + } + this.showExtraGroupCol = showExtraGroupCol; + this.updateVirtualColumns(); + } + + updateVirtualColumns(): void { + this.virtualColumns = + this.showExtraGroupColumn && this.table.groupedColumns.length > 1 + ? [VirtualGroupColumn] + : []; + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { + detail: this.columns, + }) + ); + } + applyBufferedViewport( viewportTop: number, viewportBottom: number, diff --git a/packages/jsapi-utils/src/Settings.ts b/packages/jsapi-utils/src/Settings.ts index 264615892..40f324021 100644 --- a/packages/jsapi-utils/src/Settings.ts +++ b/packages/jsapi-utils/src/Settings.ts @@ -21,6 +21,7 @@ export interface NumberFormatSettings { truncateNumbersWithPound?: boolean; showEmptyStrings?: boolean; showNullStrings?: boolean; + showExtraGroupColumn?: boolean; } export interface Settings diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index de94dbe05..0cd5de5f8 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -7,6 +7,7 @@ export * from './useCheckOverflow'; export * from './useContentRect'; export { default as useContextOrThrow } from './useContextOrThrow'; export * from './useDebouncedCallback'; +export * from './useOnChange'; export * from './useThrottledCallback'; export * from './useDelay'; export * from './useDependentState'; diff --git a/packages/react-hooks/src/useOnChange.test.ts b/packages/react-hooks/src/useOnChange.test.ts new file mode 100644 index 000000000..af81ab307 --- /dev/null +++ b/packages/react-hooks/src/useOnChange.test.ts @@ -0,0 +1,65 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useOnChange from './useOnChange'; +import usePrevious from './usePrevious'; + +// Mock usePrevious to control its return value +jest.mock('./usePrevious'); + +describe('useOnChange', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls the callback when dependencies change', () => { + const callback = jest.fn(); + let deps = [1, 2]; + + (usePrevious as jest.Mock).mockReturnValueOnce(undefined); + + const { rerender } = renderHook(() => useOnChange(callback, deps)); + + // Initial render, callback should be called + expect(callback).toHaveBeenCalledTimes(1); + + // Change dependencies + deps = [2, 3]; + (usePrevious as jest.Mock).mockReturnValueOnce([1, 2]); + + rerender(); + + // Callback should be called again + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('does not call the callback when dependencies do not change', () => { + const callback = jest.fn(); + const deps = [1, 2]; + + (usePrevious as jest.Mock).mockReturnValueOnce(undefined); + + const { rerender } = renderHook(() => useOnChange(callback, deps)); + + // Initial render, callback should be called + expect(callback).toHaveBeenCalledTimes(1); + + // Rerender with the same dependencies + (usePrevious as jest.Mock).mockReturnValueOnce([1, 2]); + + rerender(); + + // Callback should not be called again + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('calls the callback immediately on the first render', () => { + const callback = jest.fn(); + const deps = [1, 2]; + + (usePrevious as jest.Mock).mockReturnValueOnce(undefined); + + renderHook(() => useOnChange(callback, deps)); + + // Callback should be called immediately on the first render + expect(callback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/react-hooks/src/useOnChange.ts b/packages/react-hooks/src/useOnChange.ts new file mode 100644 index 000000000..54040f322 --- /dev/null +++ b/packages/react-hooks/src/useOnChange.ts @@ -0,0 +1,17 @@ +import { DependencyList } from 'react'; +import usePrevious from './usePrevious'; + +/** + * Custom hook that triggers a callback function immediately when any of the dependencies change. + * + * @param callback - The function to be called when the dependencies change. + * @param deps - The list of dependencies to watch for changes. + */ +export function useOnChange(callback: () => void, deps: DependencyList): void { + const prevDeps = usePrevious(deps); + if (prevDeps === undefined || !deps.every((dep, i) => dep === prevDeps[i])) { + callback(); + } +} + +export default useOnChange; diff --git a/packages/redux/src/selectors.ts b/packages/redux/src/selectors.ts index f4557a660..22dedc320 100644 --- a/packages/redux/src/selectors.ts +++ b/packages/redux/src/selectors.ts @@ -129,6 +129,11 @@ export const getShowNullStrings = ( store: State ): Settings['showNullStrings'] => getSettings(store).showNullStrings; +export const getShowExtraGroupColumn = ( + store: State +): Settings['showExtraGroupColumn'] => + getSettings(store).showExtraGroupColumn; + export const getDisableMoveConfirmation = ( store: State ): Settings['disableMoveConfirmation'] => diff --git a/packages/redux/src/store.ts b/packages/redux/src/store.ts index 6f7654b2d..f3c9e57ce 100644 --- a/packages/redux/src/store.ts +++ b/packages/redux/src/store.ts @@ -49,6 +49,7 @@ export interface WorkspaceSettings { truncateNumbersWithPound: boolean; showEmptyStrings: boolean; showNullStrings: boolean; + showExtraGroupColumn: boolean; disableMoveConfirmation: boolean; shortcutOverrides?: { windows?: { [id: string]: ValidKeyState };