From 52ba2cd125ff68f71c479d2d7c82f4b08d5b2ab6 Mon Sep 17 00:00:00 2001 From: Tony Zhou <75397821+Zhou-Ziheng@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:45:40 -0500 Subject: [PATCH] feat: Read settings from props/server config when available (#1558) Updated the follow values to use ServerConfig when possible: - defaultDateTimeFormat - timeZone - defaultDecimalFormatOptions - defaultIntegerFormatOptions - truncateNumbersWithPound - defaultNotebookSettings.isMinimapEnabled --- packages/code-studio/src/main/AppInit.tsx | 32 +- .../code-studio/src/main/AppMainContainer.tsx | 4 +- .../ColumnSpecificSectionContent.test.tsx | 4 +- .../settings/ColumnSpecificSectionContent.tsx | 89 +----- .../FormattingSectionContent.test.tsx | 36 ++- .../src/settings/FormattingSectionContent.tsx | 295 +++++++----------- .../src/settings/ShortcutsSectionContent.tsx | 19 +- .../src/storage/LocalWorkspaceStorage.ts | 142 +++++++-- .../src/styleguide/StyleGuideInit.tsx | 8 +- .../src/ChartPlugin.tsx | 9 +- .../src/panels/IrisGridPanel.tsx | 9 +- .../src/panels/NotebookPanel.tsx | 27 +- packages/redux/src/actionTypes.ts | 2 + packages/redux/src/actions.ts | 35 ++- .../src/reducers/defaultWorkspaceSettings.ts | 7 + packages/redux/src/reducers/index.ts | 2 + packages/redux/src/selectors.ts | 40 ++- packages/redux/src/store.ts | 20 +- 18 files changed, 374 insertions(+), 406 deletions(-) create mode 100644 packages/redux/src/reducers/defaultWorkspaceSettings.ts diff --git a/packages/code-studio/src/main/AppInit.tsx b/packages/code-studio/src/main/AppInit.tsx index 6b1d4cffcb..63cfbf76b6 100644 --- a/packages/code-studio/src/main/AppInit.tsx +++ b/packages/code-studio/src/main/AppInit.tsx @@ -20,9 +20,7 @@ import { useApi, useClient } from '@deephaven/jsapi-bootstrap'; import type { dh as DhType, IdeConnection } from '@deephaven/jsapi-types'; import { useConnection } from '@deephaven/jsapi-components'; import { - DecimalColumnFormatter, getSessionDetails, - IntegerColumnFormatter, loadSessionWrapper, SessionWrapper, } from '@deephaven/jsapi-utils'; @@ -39,12 +37,14 @@ import { setPlugins as setPluginsAction, setUser as setUserAction, setWorkspace as setWorkspaceAction, + setDefaultWorkspaceSettings as setDefaultWorkspaceSettingsAction, setWorkspaceStorage as setWorkspaceStorageAction, setServerConfigValues as setServerConfigValuesAction, User, - Workspace, WorkspaceStorage, ServerConfigValues, + WorkspaceSettings, + CustomizableWorkspace, } from '@deephaven/redux'; import { useServerConfig, useUser } from '@deephaven/app-utils'; import { type PluginModuleMap, usePlugins } from '@deephaven/plugin'; @@ -58,7 +58,7 @@ import GrpcFileStorage from '../storage/grpc/GrpcFileStorage'; const log = Log.module('AppInit'); interface AppInitProps { - workspace: Workspace; + workspace: CustomizableWorkspace; workspaceStorage: WorkspaceStorage; setActiveTool: (type: (typeof ToolType)[keyof typeof ToolType]) => void; @@ -74,7 +74,8 @@ interface AppInitProps { setDashboardSessionWrapper: (id: string, wrapper: SessionWrapper) => void; setPlugins: (map: PluginModuleMap) => void; setUser: (user: User) => void; - setWorkspace: (workspace: Workspace) => void; + setWorkspace: (workspace: CustomizableWorkspace) => void; + setDefaultWorkspaceSettings: (settings: WorkspaceSettings) => void; setWorkspaceStorage: (workspaceStorage: WorkspaceStorage) => void; setServerConfigValues: (config: ServerConfigValues) => void; } @@ -97,6 +98,7 @@ function AppInit(props: AppInitProps): JSX.Element { setUser, setWorkspace, setWorkspaceStorage, + setDefaultWorkspaceSettings, setServerConfigValues, } = props; @@ -149,21 +151,6 @@ function AppInit(props: AppInitProps): JSX.Element { // Fill in settings that have not yet been set const { settings } = data; - if (settings.defaultDecimalFormatOptions === undefined) { - settings.defaultDecimalFormatOptions = { - defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING, - }; - } - - if (settings.defaultIntegerFormatOptions === undefined) { - settings.defaultIntegerFormatOptions = { - defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING, - }; - } - - if (settings.truncateNumbersWithPound === undefined) { - settings.truncateNumbersWithPound = false; - } // Set any shortcuts that user has overridden on this platform const { shortcutOverrides = {} } = settings; @@ -195,6 +182,9 @@ function AppInit(props: AppInitProps): JSX.Element { setUser(user); setWorkspaceStorage(workspaceStorage); setWorkspace(loadedWorkspace); + setDefaultWorkspaceSettings( + LocalWorkspaceStorage.makeDefaultWorkspaceSettings(serverConfig) + ); } catch (e) { log.error(e); setError(e); @@ -217,6 +207,7 @@ function AppInit(props: AppInitProps): JSX.Element { setDashboardSessionWrapper, setUser, setWorkspace, + setDefaultWorkspaceSettings, setWorkspaceStorage, setServerConfigValues, user, @@ -281,6 +272,7 @@ const ConnectedAppInit = connect(mapStateToProps, { setPlugins: setPluginsAction, setUser: setUserAction, setWorkspace: setWorkspaceAction, + setDefaultWorkspaceSettings: setDefaultWorkspaceSettingsAction, setWorkspaceStorage: setWorkspaceStorageAction, setServerConfigValues: setServerConfigValuesAction, })(AppInit); diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 46f4f4ef48..156f102821 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -71,11 +71,11 @@ import { setActiveTool as setActiveToolAction, updateWorkspaceData as updateWorkspaceDataAction, getPlugins, - Workspace, WorkspaceData, RootState, User, ServerConfigValues, + CustomizableWorkspace, } from '@deephaven/redux'; import { bindAllMethods, PromiseUtils } from '@deephaven/utils'; import GoldenLayout from '@deephaven/golden-layout'; @@ -130,7 +130,7 @@ interface AppMainContainerProps { updateDashboardData: (id: string, data: Partial) => void; updateWorkspaceData: (workspaceData: Partial) => void; user: User; - workspace: Workspace; + workspace: CustomizableWorkspace; plugins: PluginModuleMap; serverConfigValues: ServerConfigValues; } diff --git a/packages/code-studio/src/settings/ColumnSpecificSectionContent.test.tsx b/packages/code-studio/src/settings/ColumnSpecificSectionContent.test.tsx index 58049414a5..4429282b0e 100644 --- a/packages/code-studio/src/settings/ColumnSpecificSectionContent.test.tsx +++ b/packages/code-studio/src/settings/ColumnSpecificSectionContent.test.tsx @@ -25,7 +25,7 @@ function renderContent({ timeZone = '', truncateNumbersWithPound = false, settings = {} as WorkspaceSettings, - saveSettings = jest.fn(), + updateSettings = jest.fn(), scrollTo = undefined, defaultDecimalFormatOptions = { defaultFormatString: DEFAULT_DECIMAL_STRING, @@ -45,7 +45,7 @@ function renderContent({ timeZone={timeZone} truncateNumbersWithPound={truncateNumbersWithPound} settings={settings} - saveSettings={saveSettings} + updateSettings={updateSettings} scrollTo={scrollTo} defaultDecimalFormatOptions={defaultDecimalFormatOptions} defaultIntegerFormatOptions={defaultIntegerFormatOptions} diff --git a/packages/code-studio/src/settings/ColumnSpecificSectionContent.tsx b/packages/code-studio/src/settings/ColumnSpecificSectionContent.tsx index b36bd6d6a9..6ec5fed1cb 100644 --- a/packages/code-studio/src/settings/ColumnSpecificSectionContent.tsx +++ b/packages/code-studio/src/settings/ColumnSpecificSectionContent.tsx @@ -22,16 +22,13 @@ import { import type { dh as DhType } from '@deephaven/jsapi-types'; import { getApi, - getDefaultDateTimeFormat, getDefaultDecimalFormatOptions, getDefaultIntegerFormatOptions, getFormatter, getTimeZone, getShowTimeZone, getShowTSeparator, - getTruncateNumbersWithPound, - getSettings, - saveSettings as saveSettingsAction, + updateSettings as updateSettingsAction, RootState, WorkspaceSettings, } from '@deephaven/redux'; @@ -53,13 +50,10 @@ import DateTimeOptions from './DateTimeOptions'; export interface ColumnSpecificSectionContentProps { dh: DhType; formatter: FormatterItem[]; - defaultDateTimeFormat: string; showTimeZone: boolean; showTSeparator: boolean; timeZone: string; - truncateNumbersWithPound: boolean; - settings: WorkspaceSettings; - saveSettings: (settings: WorkspaceSettings) => void; + updateSettings: (settings: Partial) => void; scrollTo: (x: number, y: number) => void; defaultDecimalFormatOptions: FormatOption; defaultIntegerFormatOptions: FormatOption; @@ -71,7 +65,6 @@ interface ColumnSpecificSectionContentState { showTimeZone: boolean; showTSeparator: boolean; timeZone: string; - truncateNumbersWithPound: boolean; timestampAtMenuOpen: Date; } @@ -81,19 +74,6 @@ export class ColumnSpecificSectionContent extends PureComponent< > { static defaultProps = { scrollTo: (): void => undefined, - defaults: { - defaultDateTimeFormat: - DateTimeColumnFormatter.DEFAULT_DATETIME_FORMAT_STRING, - defaultDecimalFormatOptions: { - defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING, - }, - defaultIntegerFormatOptions: { - defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING, - }, - showTimeZone: false, - showTSeparator: true, - timeZone: DateTimeColumnFormatter.DEFAULT_TIME_ZONE_ID, - }, }; static inputDebounceTime = 250; @@ -111,13 +91,7 @@ export class ColumnSpecificSectionContent extends PureComponent< this.handleFormatRuleCreate = this.handleFormatRuleCreate.bind(this); this.handleFormatRuleDelete = this.handleFormatRuleDelete.bind(this); - const { - formatter, - showTimeZone, - showTSeparator, - timeZone, - truncateNumbersWithPound, - } = props; + const { formatter, showTimeZone, showTSeparator, timeZone } = props; const formatSettings = formatter.map((item, i) => ({ ...item, @@ -135,7 +109,6 @@ export class ColumnSpecificSectionContent extends PureComponent< showTimeZone, showTSeparator, timeZone, - truncateNumbersWithPound, timestampAtMenuOpen: new Date(), }; } @@ -273,19 +246,7 @@ export class ColumnSpecificSectionContent extends PureComponent< } commitChanges(): void { - const { - formatSettings, - showTimeZone, - showTSeparator, - timeZone, - truncateNumbersWithPound, - } = this.state; - - const { - defaultDateTimeFormat, - defaultDecimalFormatOptions, - defaultIntegerFormatOptions, - } = this.props; + const { formatSettings } = this.state; const { dh } = this.props; @@ -296,39 +257,11 @@ export class ColumnSpecificSectionContent extends PureComponent< ) .map(removeFormatRuleExtraProps) ?? []; - const { settings, saveSettings } = this.props; - const newSettings: WorkspaceSettings = { - ...settings, + const { updateSettings } = this.props; + const newSettings = { formatter: formatter as FormattingRule[], - defaultDateTimeFormat, - showTimeZone, - showTSeparator, - timeZone, - truncateNumbersWithPound, }; - if ( - isValidFormat( - dh, - TableUtils.dataType.DECIMAL, - DecimalColumnFormatter.makeCustomFormat( - defaultDecimalFormatOptions.defaultFormatString - ) - ) - ) { - newSettings.defaultDecimalFormatOptions = defaultDecimalFormatOptions; - } - if ( - isValidFormat( - dh, - TableUtils.dataType.INT, - IntegerColumnFormatter.makeCustomFormat( - defaultIntegerFormatOptions.defaultFormatString - ) - ) - ) { - newSettings.defaultIntegerFormatOptions = defaultIntegerFormatOptions; - } - saveSettings(newSettings); + updateSettings(newSettings); } scrollToFormatBlockBottom(): void { @@ -577,6 +510,7 @@ export class ColumnSpecificSectionContent extends PureComponent< isInvalid: boolean ): ReactElement { const { defaultIntegerFormatOptions } = this.props; + assertNotNull(defaultIntegerFormatOptions); const { defaultFormatString } = defaultIntegerFormatOptions; const value = format.formatString ?? ''; return ( @@ -688,21 +622,18 @@ export class ColumnSpecificSectionContent extends PureComponent< const mapStateToProps = ( state: RootState -): Omit => ({ +): Omit => ({ formatter: getFormatter(state), - defaultDateTimeFormat: getDefaultDateTimeFormat(state), defaultDecimalFormatOptions: getDefaultDecimalFormatOptions(state), defaultIntegerFormatOptions: getDefaultIntegerFormatOptions(state), dh: getApi(state), showTimeZone: getShowTimeZone(state), showTSeparator: getShowTSeparator(state), - truncateNumbersWithPound: getTruncateNumbersWithPound(state), timeZone: getTimeZone(state), - settings: getSettings(state), }); const ConnectedColumnSpecificSectionContent = connect(mapStateToProps, { - saveSettings: saveSettingsAction, + updateSettings: updateSettingsAction, })(ColumnSpecificSectionContent); export default ConnectedColumnSpecificSectionContent; diff --git a/packages/code-studio/src/settings/FormattingSectionContent.test.tsx b/packages/code-studio/src/settings/FormattingSectionContent.test.tsx index 85fa85c612..86b92c1f30 100644 --- a/packages/code-studio/src/settings/FormattingSectionContent.test.tsx +++ b/packages/code-studio/src/settings/FormattingSectionContent.test.tsx @@ -35,7 +35,7 @@ function renderSectionContent({ showTSeparator = false, timeZone = '', defaultDateTimeFormat = '', - saveSettings = jest.fn(), + updateSettings = jest.fn(), scrollTo = jest.fn(), defaultDecimalFormatOptions = { defaultFormatString: DEFAULT_DECIMAL_STRING, @@ -57,7 +57,7 @@ function renderSectionContent({ timeZone={timeZone} defaultDateTimeFormat={defaultDateTimeFormat} truncateNumbersWithPound={truncateNumbersWithPound} - saveSettings={saveSettings} + updateSettings={updateSettings} scrollTo={scrollTo} defaultDecimalFormatOptions={defaultDecimalFormatOptions} defaultIntegerFormatOptions={defaultIntegerFormatOptions} @@ -90,8 +90,10 @@ describe('default decimal formatting', () => { it('updates settings when value is changed', async () => { const user = userEvent.setup({ delay: null }); - const saveSettings = jest.fn(); - const { getByLabelText, unmount } = renderSectionContent({ saveSettings }); + const updateSettings = jest.fn(); + const { getByLabelText, unmount } = renderSectionContent({ + updateSettings, + }); const newFormat = '00.0'; const input = getByLabelText('Decimal'); await user.clear(input); @@ -99,7 +101,7 @@ describe('default decimal formatting', () => { jest.runOnlyPendingTimers(); - expect(saveSettings).toHaveBeenCalledWith( + expect(updateSettings).toHaveBeenCalledWith( expect.objectContaining({ defaultDecimalFormatOptions: { defaultFormatString: newFormat }, }) @@ -110,12 +112,12 @@ describe('default decimal formatting', () => { it('resets to default', async () => { const user = userEvent.setup({ delay: null }); - const saveSettings = jest.fn(); + const updateSettings = jest.fn(); const defaultFormatOptions = { defaultFormatString: DEFAULT_DECIMAL_STRING, }; renderSectionContent({ - saveSettings, + updateSettings, defaultDecimalFormatOptions: { defaultFormatString: '000', }, @@ -131,9 +133,9 @@ describe('default decimal formatting', () => { jest.runOnlyPendingTimers(); - expect(saveSettings).toHaveBeenCalledWith( + expect(updateSettings).toHaveBeenCalledWith( expect.objectContaining({ - defaultDecimalFormatOptions: defaultFormatOptions, + defaultDecimalFormatOptions: undefined, }) ); }); @@ -150,8 +152,10 @@ describe('default integer formatting', () => { it('updates settings when value is changed', async () => { const user = userEvent.setup({ delay: null }); - const saveSettings = jest.fn(); - const { getByLabelText, unmount } = renderSectionContent({ saveSettings }); + const updateSettings = jest.fn(); + const { getByLabelText, unmount } = renderSectionContent({ + updateSettings, + }); const newFormat = '000,000'; const input = getByLabelText('Integer'); @@ -160,7 +164,7 @@ describe('default integer formatting', () => { jest.runOnlyPendingTimers(); - expect(saveSettings).toHaveBeenCalledWith( + expect(updateSettings).toHaveBeenCalledWith( expect.objectContaining({ defaultIntegerFormatOptions: { defaultFormatString: newFormat }, }) @@ -171,12 +175,12 @@ describe('default integer formatting', () => { it('resets to default', async () => { const user = userEvent.setup({ delay: null }); - const saveSettings = jest.fn(); + const updateSettings = jest.fn(); const defaultFormatOptions = { defaultFormatString: DEFAULT_INTEGER_STRING, }; renderSectionContent({ - saveSettings, + updateSettings, defaultIntegerFormatOptions: { defaultFormatString: '000', }, @@ -192,9 +196,9 @@ describe('default integer formatting', () => { jest.runOnlyPendingTimers(); - expect(saveSettings).toHaveBeenCalledWith( + expect(updateSettings).toHaveBeenCalledWith( expect.objectContaining({ - defaultIntegerFormatOptions: defaultFormatOptions, + defaultIntegerFormatOptions: undefined, }) ); }); diff --git a/packages/code-studio/src/settings/FormattingSectionContent.tsx b/packages/code-studio/src/settings/FormattingSectionContent.tsx index dba090f0c3..9947c52c26 100644 --- a/packages/code-studio/src/settings/FormattingSectionContent.tsx +++ b/packages/code-studio/src/settings/FormattingSectionContent.tsx @@ -11,7 +11,6 @@ import debounce from 'lodash.debounce'; import classNames from 'classnames'; import { Button, Checkbox } from '@deephaven/components'; import { - DateTimeColumnFormatter, IntegerColumnFormatter, DecimalColumnFormatter, TableUtils, @@ -23,15 +22,14 @@ import { getDefaultDateTimeFormat, getDefaultDecimalFormatOptions, getDefaultIntegerFormatOptions, - getFormatter, getTimeZone, getShowTimeZone, getShowTSeparator, getTruncateNumbersWithPound, - getSettings, - saveSettings as saveSettingsAction, + updateSettings as updateSettingsAction, RootState, WorkspaceSettings, + getDefaultSettings, } from '@deephaven/redux'; import './FormattingSectionContent.scss'; import type { DebouncedFunc } from 'lodash'; @@ -40,11 +38,8 @@ import { isSameDecimalOptions, isSameIntegerOptions, isValidFormat, - removeFormatRuleExtraProps, - isFormatRuleValidForSave, - ValidFormatterItem, } from './SettingsUtils'; -import type { FormatterItem, FormatOption } from './SettingsUtils'; +import type { FormatOption } from './SettingsUtils'; import DateTimeOptions from './DateTimeOptions'; import TimeZoneOptions from './TimeZoneOptions'; @@ -52,28 +47,18 @@ const log = Log.module('FormattingSectionContent'); interface FormattingSectionContentProps { dh: DhType; - formatter: FormatterItem[]; defaultDateTimeFormat: string; showTimeZone: boolean; showTSeparator: boolean; timeZone: string; truncateNumbersWithPound: boolean; - settings: WorkspaceSettings; - saveSettings: (settings: WorkspaceSettings) => void; + updateSettings: (settings: Partial) => void; defaultDecimalFormatOptions: FormatOption; defaultIntegerFormatOptions: FormatOption; - defaults: { - defaultDateTimeFormat: string; - defaultDecimalFormatOptions: FormatOption; - defaultIntegerFormatOptions: FormatOption; - showTimeZone: boolean; - showTSeparator: boolean; - timeZone: string; - }; + defaults: WorkspaceSettings; } interface FormattingSectionContentState { - formatSettings: FormatterItem[]; showTimeZone: boolean; showTSeparator: boolean; timeZone: string; @@ -90,19 +75,6 @@ export class FormattingSectionContent extends PureComponent< > { static defaultProps = { scrollTo: (): void => undefined, - defaults: { - defaultDateTimeFormat: - DateTimeColumnFormatter.DEFAULT_DATETIME_FORMAT_STRING, - defaultDecimalFormatOptions: { - defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING, - }, - defaultIntegerFormatOptions: { - defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING, - }, - showTimeZone: false, - showTSeparator: true, - timeZone: DateTimeColumnFormatter.DEFAULT_TIME_ZONE_ID, - }, }; static inputDebounceTime = 250; @@ -114,7 +86,7 @@ export class FormattingSectionContent extends PureComponent< this.commitChanges.bind(this), FormattingSectionContent.inputDebounceTime ); - + this.queueUpdate = this.queueUpdate.bind(this); this.handleDefaultDateTimeFormatChange = this.handleDefaultDateTimeFormatChange.bind(this); this.handleDefaultDecimalFormatChange = @@ -133,7 +105,6 @@ export class FormattingSectionContent extends PureComponent< this.handleTruncateNumbersWithPoundChange.bind(this); const { - formatter, defaultDateTimeFormat, defaultDecimalFormatOptions, defaultIntegerFormatOptions, @@ -143,15 +114,10 @@ export class FormattingSectionContent extends PureComponent< truncateNumbersWithPound, } = props; - const formatSettings = formatter.map((item, i) => ({ - ...item, - id: i, - })); - this.containerRef = React.createRef(); + this.pendingUpdates = []; this.state = { - formatSettings, showTimeZone, showTSeparator, timeZone, @@ -173,6 +139,8 @@ export class FormattingSectionContent extends PureComponent< debouncedCommitChanges: DebouncedFunc<() => void>; + pendingUpdates: Partial[]; + containerRef: RefObject; getCachedDateTimeFormatOptions = memoize( @@ -197,203 +165,153 @@ export class FormattingSectionContent extends PureComponent< } ); + queueUpdate(updates: Partial): void { + this.pendingUpdates.push(updates); + this.debouncedCommitChanges(); + } + handleDefaultDateTimeFormatChange( event: ChangeEvent ): void { log.debug('handleDefaultDateTimeFormatChange', event.target.value); - this.setState( - { - defaultDateTimeFormat: event.target.value, - }, - () => { - this.debouncedCommitChanges(); - } - ); + const update = { + defaultDateTimeFormat: event.target.value, + }; + this.setState(update); + this.queueUpdate(update); } handleDefaultDecimalFormatChange(event: ChangeEvent): void { log.debug('handleDefaultDecimalFormatChange', event.target.value); - this.setState( - { - defaultDecimalFormatOptions: { - defaultFormatString: event.target.value, - }, + const update = { + defaultDecimalFormatOptions: { + defaultFormatString: event.target.value, }, - () => { - this.debouncedCommitChanges(); - } - ); + }; + this.setState(update); + if ( + isValidFormat( + dh, + TableUtils.dataType.DECIMAL, + DecimalColumnFormatter.makeCustomFormat(event.target.value) + ) + ) { + this.queueUpdate(update); + } } handleDefaultIntegerFormatChange(event: ChangeEvent): void { log.debug('handleDefaultIntegerFormatChange', event.target.value); - this.setState( - { - defaultIntegerFormatOptions: { - defaultFormatString: event.target.value, - }, + const update = { + defaultIntegerFormatOptions: { + defaultFormatString: event.target.value, }, - () => { - this.debouncedCommitChanges(); - } - ); + }; + this.setState(update); + if ( + isValidFormat( + dh, + TableUtils.dataType.INT, + IntegerColumnFormatter.makeCustomFormat(event.target.value) + ) + ) { + this.queueUpdate(update); + } } handleShowTimeZoneChange(): void { - this.setState( - state => ({ - showTimeZone: !state.showTimeZone, - }), - () => { - this.debouncedCommitChanges(); - } - ); + const { showTimeZone } = this.state; + const update = { showTimeZone: !showTimeZone }; + this.setState(update); + this.queueUpdate(update); } handleShowTSeparatorChange(): void { - this.setState( - state => ({ - showTSeparator: !state.showTSeparator, - }), - () => { - this.debouncedCommitChanges(); - } - ); + const { showTSeparator } = this.state; + const update = { showTSeparator: !showTSeparator }; + this.setState(update); + this.queueUpdate(update); } handleTimeZoneChange(event: ChangeEvent): void { - this.setState( - { - timeZone: event.target.value, - }, - () => { - this.debouncedCommitChanges(); - } - ); + const update = { timeZone: event.target.value }; + this.setState(update); + this.queueUpdate(update); } handleResetDateTimeFormat(): void { const { defaults } = this.props; const { defaultDateTimeFormat, showTimeZone, showTSeparator } = defaults; log.debug('handleResetDateTimeFormat'); - this.setState( - { - defaultDateTimeFormat, - showTimeZone, - showTSeparator, - }, - () => { - this.debouncedCommitChanges(); - } - ); + this.setState({ + defaultDateTimeFormat, + showTimeZone, + showTSeparator, + }); + this.queueUpdate({ + defaultDateTimeFormat: undefined, + showTimeZone: undefined, + showTSeparator: undefined, + }); } handleResetTimeZone(): void { const { defaults } = this.props; const { timeZone } = defaults; log.debug('handleResetTimeZone'); - this.setState( - { - timeZone, - }, - () => { - this.debouncedCommitChanges(); - } - ); + this.setState({ + timeZone, + }); + this.queueUpdate({ + timeZone: undefined, + }); } handleResetDecimalFormat(): void { const { defaults } = this.props; const { defaultDecimalFormatOptions } = defaults; log.debug('handleResetDecimalFormat'); - this.setState( - { - defaultDecimalFormatOptions, - }, - () => { - this.debouncedCommitChanges(); - } - ); + this.setState({ + defaultDecimalFormatOptions, + }); + this.queueUpdate({ + defaultDecimalFormatOptions: undefined, + }); } handleResetIntegerFormat(): void { const { defaults } = this.props; const { defaultIntegerFormatOptions } = defaults; log.debug('handleResetIntegerFormat'); - this.setState( - { - defaultIntegerFormatOptions, - }, - () => { - this.debouncedCommitChanges(); - } - ); + this.setState({ + defaultIntegerFormatOptions, + }); + this.queueUpdate({ + defaultIntegerFormatOptions: undefined, + }); } handleTruncateNumbersWithPoundChange(): void { - this.setState( - state => ({ - truncateNumbersWithPound: !state.truncateNumbersWithPound, - }), - () => { - this.debouncedCommitChanges(); - } - ); + const { truncateNumbersWithPound } = this.state; + const update = { + truncateNumbersWithPound: truncateNumbersWithPound !== true, + }; + this.setState(update); + this.queueUpdate(update); } commitChanges(): void { - const { - formatSettings, - defaultDateTimeFormat, - showTimeZone, - showTSeparator, - timeZone, - defaultDecimalFormatOptions, - defaultIntegerFormatOptions, - truncateNumbersWithPound, - } = this.state; - const { dh } = this.props; - - const formatter = - formatSettings - .filter((format): format is ValidFormatterItem => - isFormatRuleValidForSave(dh, format) - ) - .map(removeFormatRuleExtraProps) ?? []; - - const { settings, saveSettings } = this.props; - const newSettings: WorkspaceSettings = { - ...settings, - formatter, - defaultDateTimeFormat, - showTimeZone, - showTSeparator, - timeZone, - truncateNumbersWithPound, - }; - if ( - isValidFormat( - dh, - TableUtils.dataType.DECIMAL, - DecimalColumnFormatter.makeCustomFormat( - defaultDecimalFormatOptions.defaultFormatString - ) - ) - ) { - newSettings.defaultDecimalFormatOptions = defaultDecimalFormatOptions; - } - if ( - isValidFormat( - dh, - TableUtils.dataType.INT, - IntegerColumnFormatter.makeCustomFormat( - defaultIntegerFormatOptions.defaultFormatString - ) - ) - ) { - newSettings.defaultIntegerFormatOptions = defaultIntegerFormatOptions; - } - saveSettings(newSettings); + const { updateSettings } = this.props; + const updates = this.pendingUpdates.reduce( + (acc, update) => ({ + ...acc, + ...update, + }), + {} + ); + this.pendingUpdates = []; + + updateSettings(updates); } render(): ReactElement { @@ -629,8 +547,7 @@ export class FormattingSectionContent extends PureComponent< const mapStateToProps = ( state: RootState -): Omit => ({ - formatter: getFormatter(state), +): Omit => ({ defaultDateTimeFormat: getDefaultDateTimeFormat(state), defaultDecimalFormatOptions: getDefaultDecimalFormatOptions(state), defaultIntegerFormatOptions: getDefaultIntegerFormatOptions(state), @@ -639,11 +556,11 @@ const mapStateToProps = ( showTSeparator: getShowTSeparator(state), truncateNumbersWithPound: getTruncateNumbersWithPound(state), timeZone: getTimeZone(state), - settings: getSettings(state), + defaults: getDefaultSettings(state), }); const ConnectedFormattingSectionContent = connect(mapStateToProps, { - saveSettings: saveSettingsAction, + updateSettings: updateSettingsAction, })(FormattingSectionContent); export default ConnectedFormattingSectionContent; diff --git a/packages/code-studio/src/settings/ShortcutsSectionContent.tsx b/packages/code-studio/src/settings/ShortcutsSectionContent.tsx index 8d6efbf0a9..2b8cc5ec30 100644 --- a/packages/code-studio/src/settings/ShortcutsSectionContent.tsx +++ b/packages/code-studio/src/settings/ShortcutsSectionContent.tsx @@ -2,24 +2,21 @@ import React, { useState, useMemo, useCallback } from 'react'; import { connect } from 'react-redux'; import { Shortcut, ShortcutRegistry } from '@deephaven/components'; import { - getSettings, getShortcutOverrides, RootState, - saveSettings as saveSettingsAction, + updateSettings as updateSettingsAction, WorkspaceSettings, } from '@deephaven/redux'; import ShortcutItem from './ShortcutItem'; type ShortcutSectionContentProps = { - settings: WorkspaceSettings; shortcutOverrides: WorkspaceSettings['shortcutOverrides']; - saveSettings: typeof saveSettingsAction; + updateSettings: typeof updateSettingsAction; }; function ShortcutSectionContent({ shortcutOverrides = {}, - settings, - saveSettings, + updateSettings, }: ShortcutSectionContentProps): JSX.Element { const saveShortcutOverrides = useCallback( (modifiedShortcuts: Shortcut[]) => { @@ -41,12 +38,11 @@ function ShortcutSectionContent({ } }); - saveSettings({ - ...settings, + updateSettings({ shortcutOverrides: newOverrides, }); }, - [settings, saveSettings, shortcutOverrides] + [updateSettings, shortcutOverrides] ); let categories = Array.from( @@ -143,12 +139,11 @@ function ShortcutCategory({ const mapStateToProps = ( state: RootState -): Pick => ({ - settings: getSettings(state), +): Pick => ({ shortcutOverrides: getShortcutOverrides(state), }); -const mapDispatchToProps = { saveSettings: saveSettingsAction }; +const mapDispatchToProps = { updateSettings: updateSettingsAction }; const ConnectedShortcutSectionContent = connect( mapStateToProps, diff --git a/packages/code-studio/src/storage/LocalWorkspaceStorage.ts b/packages/code-studio/src/storage/LocalWorkspaceStorage.ts index c8a0d8a195..18548fb118 100644 --- a/packages/code-studio/src/storage/LocalWorkspaceStorage.ts +++ b/packages/code-studio/src/storage/LocalWorkspaceStorage.ts @@ -1,15 +1,17 @@ import Log from '@deephaven/log'; +import { + WorkspaceStorage, + WorkspaceStorageLoadOptions, + CustomizableWorkspaceData, + CustomizableWorkspace, + WorkspaceSettings, + ServerConfigValues, +} from '@deephaven/redux'; import { DateTimeColumnFormatter, DecimalColumnFormatter, IntegerColumnFormatter, } from '@deephaven/jsapi-utils'; -import { - WorkspaceStorage, - Workspace, - WorkspaceData, - WorkspaceStorageLoadOptions, -} from '@deephaven/redux'; import UserLayoutUtils from '../main/UserLayoutUtils'; import LayoutStorage from './LayoutStorage'; @@ -21,36 +23,106 @@ const log = Log.module('LocalWorkspaceStorage'); export class LocalWorkspaceStorage implements WorkspaceStorage { static readonly STORAGE_KEY = 'deephaven.WorkspaceStorage'; - static async makeDefaultWorkspaceData( + static getBooleanServerConfig( + serverConfigValues: Map | undefined, + key: string + ): boolean | undefined { + if (serverConfigValues?.get(key)?.toLowerCase() === 'true') { + return true; + } + if (serverConfigValues?.get(key)?.toLowerCase() === 'false') { + return false; + } + return undefined; + } + + static makeDefaultWorkspaceSettings( + serverConfigValues: ServerConfigValues + ): WorkspaceSettings { + const settings = { + defaultDateTimeFormat: + DateTimeColumnFormatter.DEFAULT_DATETIME_FORMAT_STRING, + formatter: [], + timeZone: DateTimeColumnFormatter.DEFAULT_TIME_ZONE_ID, + showTimeZone: false, + showTSeparator: true, + disableMoveConfirmation: false, + defaultDecimalFormatOptions: { + defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING, + }, + defaultIntegerFormatOptions: { + defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING, + }, + truncateNumbersWithPound: false, + defaultNotebookSettings: { + isMinimapEnabled: false, + }, + }; + const serverSettings = { + defaultDateTimeFormat: serverConfigValues?.get('dateTimeFormat'), + formatter: [], + timeZone: serverConfigValues?.get('timeZone'), + showTimeZone: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'showTimeZone' + ), + showTSeparator: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'showTSeparator' + ), + disableMoveConfirmation: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'disableMoveConfirmation' + ), + defaultDecimalFormatOptions: + serverConfigValues?.get('decimalFormat') !== undefined + ? { + defaultFormatString: serverConfigValues?.get('decimalFormat'), + } + : undefined, + defaultIntegerFormatOptions: + serverConfigValues?.get('integerFormat') !== undefined + ? { + defaultFormatString: serverConfigValues?.get('integerFormat'), + } + : undefined, + truncateNumbersWithPound: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'truncateNumbersWithPound' + ), + defaultNotebookSettings: + serverConfigValues?.get('isMinimapEnabled') !== undefined + ? { + isMinimapEnabled: LocalWorkspaceStorage.getBooleanServerConfig( + serverConfigValues, + 'isMinimapEnabled' + ) as boolean, + } + : undefined, + }; + + const keys = Object.keys(serverSettings) as Array; + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + if (serverSettings[key] !== undefined) { + // @ts-expect-error override default for defined server settings + settings[key] = serverSettings[key]; + } + } + return settings; + } + + static async makeWorkspaceData( layoutStorage: LayoutStorage, options?: WorkspaceStorageLoadOptions - ): Promise { + ): Promise { const { filterSets, links, layoutConfig } = await UserLayoutUtils.getDefaultLayout( layoutStorage, options?.isConsoleAvailable ); - return { - settings: { - defaultDateTimeFormat: - DateTimeColumnFormatter.DEFAULT_DATETIME_FORMAT_STRING, - formatter: [], - timeZone: DateTimeColumnFormatter.DEFAULT_TIME_ZONE_ID, - showTimeZone: false, - showTSeparator: true, - disableMoveConfirmation: false, - defaultDecimalFormatOptions: { - defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING, - }, - defaultIntegerFormatOptions: { - defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING, - }, - truncateNumbersWithPound: false, - defaultNotebookSettings: { - isMinimapEnabled: false, - }, - }, + settings: {}, layoutConfig, closed: [{}], links, @@ -61,9 +133,9 @@ export class LocalWorkspaceStorage implements WorkspaceStorage { static async makeDefaultWorkspace( layoutStorage: LayoutStorage, options?: WorkspaceStorageLoadOptions - ): Promise { + ): Promise { return { - data: await LocalWorkspaceStorage.makeDefaultWorkspaceData( + data: await LocalWorkspaceStorage.makeWorkspaceData( layoutStorage, options ), @@ -77,13 +149,17 @@ export class LocalWorkspaceStorage implements WorkspaceStorage { } // eslint-disable-next-line class-methods-use-this - async load(options?: WorkspaceStorageLoadOptions): Promise { + async load( + options?: WorkspaceStorageLoadOptions + ): Promise { try { - return JSON.parse( + const workspace = JSON.parse( localStorage.getItem(LocalWorkspaceStorage.STORAGE_KEY) ?? '' ); + return workspace; } catch (e) { log.info('Unable to load workspace data, initializing to default data'); + return LocalWorkspaceStorage.makeDefaultWorkspace( this.layoutStorage, options @@ -92,7 +168,7 @@ export class LocalWorkspaceStorage implements WorkspaceStorage { } // eslint-disable-next-line class-methods-use-this - async save(workspace: Workspace): Promise { + async save(workspace: CustomizableWorkspace): Promise { localStorage.setItem( LocalWorkspaceStorage.STORAGE_KEY, JSON.stringify(workspace) diff --git a/packages/code-studio/src/styleguide/StyleGuideInit.tsx b/packages/code-studio/src/styleguide/StyleGuideInit.tsx index e2bca2d4a9..c8a1a3c38e 100644 --- a/packages/code-studio/src/styleguide/StyleGuideInit.tsx +++ b/packages/code-studio/src/styleguide/StyleGuideInit.tsx @@ -2,11 +2,11 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { + CustomizableWorkspace, getWorkspace, PayloadActionCreator, RootState, setWorkspace as setWorkspaceAction, - Workspace, } from '@deephaven/redux'; import StyleGuide from './StyleGuide'; import LocalWorkspaceStorage from '../storage/LocalWorkspaceStorage'; @@ -16,8 +16,8 @@ import { ExportedLayout } from '../storage/LayoutStorage'; * Initialize data needed for the styleguide */ function StyleGuideInit(props: { - workspace: Workspace; - setWorkspace: PayloadActionCreator; + workspace: CustomizableWorkspace; + setWorkspace: PayloadActionCreator; }): JSX.Element | null { const { workspace, setWorkspace } = props; @@ -43,7 +43,7 @@ StyleGuideInit.defaultProps = { const mapStateToProps = ( state: RootState ): { - workspace: Workspace; + workspace: CustomizableWorkspace; } => ({ workspace: getWorkspace(state), }); diff --git a/packages/dashboard-core-plugins/src/ChartPlugin.tsx b/packages/dashboard-core-plugins/src/ChartPlugin.tsx index 5910b5c991..3e16249937 100644 --- a/packages/dashboard-core-plugins/src/ChartPlugin.tsx +++ b/packages/dashboard-core-plugins/src/ChartPlugin.tsx @@ -1,6 +1,7 @@ import { forwardRef, useMemo } from 'react'; import { useApi } from '@deephaven/jsapi-bootstrap'; import { useConnection } from '@deephaven/jsapi-components'; +import { assertNotNull } from '@deephaven/utils'; import { ChartModel, ChartModelFactory, @@ -80,11 +81,9 @@ async function createChartModel( type: dh.VariableType.TABLE, }; const table = await connection.getObject(definition); - new IrisGridUtils(dh).applyTableSettings( - table, - tableSettings, - getTimeZone(store.getState()) - ); + const timeZone = getTimeZone(store.getState()); + assertNotNull(timeZone); + new IrisGridUtils(dh).applyTableSettings(table, tableSettings, timeZone); return ChartModelFactory.makeModelFromSettings( dh, diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index fbb85c2821..29ff9d54e3 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -52,12 +52,13 @@ import { } from '@deephaven/jsapi-utils'; import Log from '@deephaven/log'; import { + CustomizableWorkspace, getSettings, getUser, getWorkspace, RootState, User, - Workspace, + WorkspaceSettings, } from '@deephaven/redux'; import { assertNotNull, @@ -156,8 +157,8 @@ interface StateProps { tableColumn?: LinkColumn ) => boolean; user: User; - workspace: Workspace; - settings: { timeZone: string }; + workspace: CustomizableWorkspace; + settings: WorkspaceSettings; } interface IrisGridPanelState { @@ -409,7 +410,7 @@ export class IrisGridPanel extends PureComponent< Plugin: TablePluginComponent | undefined, model: IrisGridModel | undefined, user: User, - workspace: Workspace, + workspace: CustomizableWorkspace, pluginState: unknown ) => { if ( diff --git a/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx b/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx index 5ec6ad73b7..a0f040dccd 100644 --- a/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx @@ -34,9 +34,8 @@ import { } from '@deephaven/icons'; import { getFileStorage, - saveSettings as saveSettingsAction, + updateSettings as updateSettingsAction, RootState, - getSettings, WorkspaceSettings, getDefaultNotebookSettings, } from '@deephaven/redux'; @@ -89,8 +88,7 @@ interface NotebookPanelProps extends DashboardPanelProps { panelState: PanelState; notebooksUrl: string; defaultNotebookSettings: NotebookSetting; - settings: WorkspaceSettings; - saveSettings: (settings: WorkspaceSettings) => void; + updateSettings: (settings: Partial) => void; } interface NotebookPanelState { @@ -155,7 +153,7 @@ class NotebookPanel extends Component { isPreview: false, session: null, sessionLanguage: null, - defaultNotebookSettings: null, + defaultNotebookSettings: { isMinimapEnabled: true }, }; static languageFromFileName(fileName: string): string | null { @@ -789,14 +787,13 @@ class NotebookPanel extends Component { } handleMinimapChange(): void { - const { settings, defaultNotebookSettings, saveSettings } = this.props; - const newSettings: WorkspaceSettings = { - ...settings, + const { defaultNotebookSettings, updateSettings } = this.props; + const newSettings = { defaultNotebookSettings: { isMinimapEnabled: !defaultNotebookSettings.isMinimapEnabled, }, }; - saveSettings(newSettings); + updateSettings(newSettings); } updateEditorWordWrap(): void { @@ -1433,14 +1430,9 @@ const mapStateToProps = ( ownProps: { localDashboardId: string } ): Pick< NotebookPanelProps, - | 'defaultNotebookSettings' - | 'fileStorage' - | 'session' - | 'sessionLanguage' - | 'settings' + 'defaultNotebookSettings' | 'fileStorage' | 'session' | 'sessionLanguage' > => { const fileStorage = getFileStorage(state); - const settings = getSettings(state); const defaultNotebookSettings = getDefaultNotebookSettings(state); const sessionWrapper = getDashboardSessionWrapper( state, @@ -1451,8 +1443,7 @@ const mapStateToProps = ( const { type: sessionLanguage } = sessionConfig ?? {}; return { fileStorage, - settings, - defaultNotebookSettings, + defaultNotebookSettings: defaultNotebookSettings as NotebookSetting, session, sessionLanguage, }; @@ -1460,7 +1451,7 @@ const mapStateToProps = ( const ConnectedNotebookPanel = connect( mapStateToProps, - { saveSettings: saveSettingsAction }, + { updateSettings: updateSettingsAction }, null, { forwardRef: true } )(NotebookPanel); diff --git a/packages/redux/src/actionTypes.ts b/packages/redux/src/actionTypes.ts index 145c85b8f4..3a2fd4a347 100644 --- a/packages/redux/src/actionTypes.ts +++ b/packages/redux/src/actionTypes.ts @@ -16,6 +16,8 @@ export const SET_USER = 'SET_USER'; export const SET_WORKSPACE = 'SET_WORKSPACE'; +export const SET_DEFAULT_WORKSPACE_SETTINGS = 'SET_DEFAULT_WORKSPACE_SETTINGS'; + export const SET_WORKSPACE_STORAGE = 'SET_WORKSPACE_STORAGE'; export const SET_SERVER_CONFIG_VALUES = 'SET_SERVER_CONFIG_VALUES'; diff --git a/packages/redux/src/actions.ts b/packages/redux/src/actions.ts index acb4c3390c..ef8db904e9 100644 --- a/packages/redux/src/actions.ts +++ b/packages/redux/src/actions.ts @@ -14,13 +14,13 @@ import { SET_FILE_STORAGE, SET_SERVER_CONFIG_VALUES, SET_API, + SET_DEFAULT_WORKSPACE_SETTINGS, } from './actionTypes'; import type { + CustomizableWorkspace, RootState, ServerConfigValues, User, - Workspace, - WorkspaceData, WorkspaceSettings, WorkspaceStorage, } from './store'; @@ -41,11 +41,20 @@ export const setApi: PayloadActionCreator = api => ({ payload: api, }); -export const setWorkspace: PayloadActionCreator = workspace => ({ +export const setWorkspace: PayloadActionCreator< + CustomizableWorkspace +> = workspace => ({ type: SET_WORKSPACE, payload: workspace, }); +export const setDefaultWorkspaceSettings: PayloadActionCreator< + WorkspaceSettings +> = settings => ({ + type: SET_DEFAULT_WORKSPACE_SETTINGS, + payload: settings, +}); + export const setWorkspaceStorage: PayloadActionCreator< WorkspaceStorage > = workspaceStorage => ({ @@ -73,16 +82,15 @@ export const setFileStorage: PayloadActionCreator< */ export const saveWorkspace = ( - workspace: Workspace + workspace: CustomizableWorkspace ): ThunkAction< - Promise, + Promise, RootState, never, PayloadAction > => (dispatch, getState) => { dispatch(setWorkspace(workspace)); - const { storage } = getState(); const { workspaceStorage } = storage; return workspaceStorage.save(workspace); @@ -94,9 +102,9 @@ export const saveWorkspace = */ export const updateWorkspaceData = ( - workspaceData: Partial + workspaceData: Partial ): ThunkAction< - Promise, + Promise, RootState, never, PayloadAction @@ -109,6 +117,10 @@ export const updateWorkspaceData = data: { ...data, ...workspaceData, + settings: { + ...data.settings, + ...workspaceData.settings, + }, }, }; return dispatch(saveWorkspace(newWorkspace)); @@ -118,18 +130,17 @@ export const updateWorkspaceData = * Sets the specified settings locally and saves them remotely * @param settings The settings to save */ -export const saveSettings = +export const updateSettings = ( - settings: WorkspaceSettings + settings: Partial ): ThunkAction< - Promise, + Promise, RootState, never, PayloadAction > => dispatch => dispatch(updateWorkspaceData({ settings })); - export const setActiveTool: PayloadActionCreator = payload => ({ type: SET_ACTIVE_TOOL, payload, diff --git a/packages/redux/src/reducers/defaultWorkspaceSettings.ts b/packages/redux/src/reducers/defaultWorkspaceSettings.ts new file mode 100644 index 0000000000..56f41578da --- /dev/null +++ b/packages/redux/src/reducers/defaultWorkspaceSettings.ts @@ -0,0 +1,7 @@ +/** + * Default values for workspace settings. Used when the user has not modified a setting yet. + */ +import { SET_DEFAULT_WORKSPACE_SETTINGS } from '../actionTypes'; +import { replaceReducer } from './common'; + +export default replaceReducer(SET_DEFAULT_WORKSPACE_SETTINGS, null); diff --git a/packages/redux/src/reducers/index.ts b/packages/redux/src/reducers/index.ts index a16387931c..79a38c83af 100644 --- a/packages/redux/src/reducers/index.ts +++ b/packages/redux/src/reducers/index.ts @@ -4,6 +4,7 @@ import plugins from './plugins'; import storage from './storage'; import user from './user'; import workspace from './workspace'; +import defaultWorkspaceSettings from './defaultWorkspaceSettings'; import serverConfigValues from './serverConfigValues'; const reducers = { @@ -13,6 +14,7 @@ const reducers = { storage, user, workspace, + defaultWorkspaceSettings, serverConfigValues, }; diff --git a/packages/redux/src/selectors.ts b/packages/redux/src/selectors.ts index dfbbad7ae6..8ba98e34fb 100644 --- a/packages/redux/src/selectors.ts +++ b/packages/redux/src/selectors.ts @@ -1,4 +1,8 @@ -import type { RootState } from './store'; +import type { + CustomizableWorkspace, + RootState, + WorkspaceSettings, +} from './store'; const EMPTY_OBJECT = Object.freeze({}); @@ -6,7 +10,7 @@ const EMPTY_MAP: ReadonlyMap = new Map(); export type Selector = (store: State) => R; -type Settings = State['workspace']['data']['settings']; +type Settings = State['defaultWorkspaceSettings']; export const getApi = (store: State): State['api'] => store.api; @@ -41,15 +45,39 @@ export const getWorkspaceStorage = ( store: State ): State['storage']['workspaceStorage'] => getStorage(store).workspaceStorage; +export const getDefaultWorkspaceSettings = ( + store: State +): State['defaultWorkspaceSettings'] => store.defaultWorkspaceSettings; + // Workspace export const getWorkspace = ( store: State -): State['workspace'] => store.workspace; +): CustomizableWorkspace => { + const { workspace } = store; + return workspace; +}; // Settings export const getSettings = ( store: State -): Settings => getWorkspace(store).data.settings; +): WorkspaceSettings => { + const customizedSettings = getWorkspace(store).data.settings; + + const settings = { ...getDefaultWorkspaceSettings(store) }; + const keys = Object.keys(customizedSettings) as (keyof WorkspaceSettings)[]; + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + if (customizedSettings[key] !== undefined) { + // @ts-expect-error assign non-undefined customized settings to settings + settings[key] = customizedSettings[key]; + } + } + return settings; +}; + +export const getDefaultSettings = ( + store: State +): Settings => store.defaultWorkspaceSettings; export const getDefaultDateTimeFormat = ( store: State @@ -72,7 +100,7 @@ export const getDefaultIntegerFormatOptions = < export const getFormatter = ( store: State -): Settings['formatter'] => getSettings(store).formatter; +): Settings['formatter'] => getSettings(store).formatter ?? []; export const getTimeZone = ( store: State @@ -96,7 +124,7 @@ export const getTruncateNumbersWithPound = < export const getDisableMoveConfirmation = ( store: State ): Settings['disableMoveConfirmation'] => - getSettings(store).disableMoveConfirmation || false; + getSettings(store).disableMoveConfirmation === true; export const getShortcutOverrides = ( store: State diff --git a/packages/redux/src/store.ts b/packages/redux/src/store.ts index c67c805bb0..6462d81162 100644 --- a/packages/redux/src/store.ts +++ b/packages/redux/src/store.ts @@ -53,7 +53,7 @@ export interface WorkspaceSettings { mac?: { [id: string]: ValidKeyState }; }; defaultNotebookSettings: { - isMinimapEnabled: boolean; + isMinimapEnabled?: boolean; }; } @@ -65,6 +65,15 @@ export interface WorkspaceData { settings: WorkspaceSettings; } +export interface CustomizableWorkspaceData + extends Omit { + settings: Partial; +} + +export interface CustomizableWorkspace { + data: CustomizableWorkspaceData; +} + export interface Workspace { data: WorkspaceData; } @@ -75,8 +84,10 @@ export type WorkspaceStorageLoadOptions = { }; export interface WorkspaceStorage { - load: (options?: WorkspaceStorageLoadOptions) => Promise; - save: (workspace: Workspace) => Promise; + load: ( + options?: WorkspaceStorageLoadOptions + ) => Promise; + save: (workspace: CustomizableWorkspace) => Promise; } export type RootState = { @@ -85,7 +96,8 @@ export type RootState = { plugins: PluginModuleMap; storage: Storage; user: User; - workspace: Workspace; + workspace: CustomizableWorkspace; + defaultWorkspaceSettings: WorkspaceSettings; dashboardData: Record; layoutStorage: unknown; serverConfigValues: ServerConfigValues;