diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index 1357a6ecd3..ade2a43743 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { ConnectionInfoResource, isSimpleNavigatorView } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual, NavigatorViewSettings } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -46,23 +46,25 @@ export class NavigationTreeContextMenuService { }); } - private isConnectionSimpleView(nodeId: string): boolean { - const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeId); + private getConnectionFromNodeId(nodeId: string) { + return this.connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeId)); + } - const connection = this.connectionInfoResource.get(connectionId); + private isSimpleNavigatorView(nodeId: string) { + const currentSettings = this.getConnectionFromNodeId(nodeId)?.navigatorSettings; - if (!connection) { + if (!currentSettings) { return false; } - return isSimpleNavigatorView(connection.navigatorSettings); + return isNavigatorViewSettingsEqual(currentSettings, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple); } - private async changeConnectionView(nodeId: string, simple: boolean) { + private async changeConnectionView(nodeId: string, settings: NavigatorViewSettings) { const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeId); try { - await this.connectionInfoResource.changeConnectionView(connectionId, simple); + await this.connectionInfoResource.changeConnectionView(connectionId, settings); await this.navNodeManagerService.refreshTree(nodeId); } catch (exception) { this.notificationService.logException(exception); @@ -88,12 +90,15 @@ export class NavigationTreeContextMenuService { { id: 'simple', title: 'app_navigationTree_connection_view_option_simple', - isDisabled: context => this.isConnectionSimpleView(context.data.id), + type: 'radio', + isChecked: context => this.isSimpleNavigatorView(context.data.id), isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType && context.data.objectFeatures.includes(EObjectFeature.dataSource); }, - onClick: async context => await this.changeConnectionView(context.data.id, true), + onClick: async context => + await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple), + } ); this.contextMenuService.addMenuItem( @@ -101,12 +106,40 @@ export class NavigationTreeContextMenuService { { id: 'advanced', title: 'app_navigationTree_connection_view_option_advanced', - isDisabled: context => !this.isConnectionSimpleView(context.data.id), + type: 'radio', + isChecked: context => !this.isSimpleNavigatorView(context.data.id), + separator: true, isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType && context.data.objectFeatures.includes(EObjectFeature.dataSource); }, - onClick: async context => await this.changeConnectionView(context.data.id, false), + onClick: async context => + await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced), + } + ); + + this.contextMenuService.addMenuItem( + this.getNodeViewMenuItemToken(), + { + id: 'systemObjects', + title: 'app_navigationTree_connection_view_option_showSystemObjects', + type: 'checkbox', + isChecked: context => !!this.getConnectionFromNodeId(context.data.id)?.navigatorSettings.showSystemObjects, + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType + && context.data.objectFeatures.includes(EObjectFeature.dataSource); + }, + onClick: async context => { + const currentSettings = this.getConnectionFromNodeId(context.data.id)?.navigatorSettings; + if (!currentSettings) { + return; + } + + return await this.changeConnectionView(context.data.id, { + ...currentSettings, + showSystemObjects: !currentSettings.showSystemObjects, + }); + }, } ); } diff --git a/webapp/packages/core-app/src/locales/en.ts b/webapp/packages/core-app/src/locales/en.ts index 35a9f0eb99..52c35f6a9c 100644 --- a/webapp/packages/core-app/src/locales/en.ts +++ b/webapp/packages/core-app/src/locales/en.ts @@ -13,6 +13,8 @@ export default [ ['app_navigationTree_connection_view', 'Connection view'], ['app_navigationTree_connection_view_option_simple', 'Simple'], ['app_navigationTree_connection_view_option_advanced', 'Advanced'], + ['app_navigationTree_connection_view_option_custom', 'Custom'], + ['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'], ['app_navigationTree_refreshNode', 'Refresh'], ['app_navigationTree_context_disconnect', 'Disconnect'], ['app_shared_inlineEditor_dialog_title', 'Edit mode'], diff --git a/webapp/packages/core-app/src/locales/ru.ts b/webapp/packages/core-app/src/locales/ru.ts index 9ab94a9074..66fb95bdb8 100644 --- a/webapp/packages/core-app/src/locales/ru.ts +++ b/webapp/packages/core-app/src/locales/ru.ts @@ -16,6 +16,8 @@ export default [ ['app_navigationTree_connection_view', 'Отображение'], ['app_navigationTree_connection_view_option_simple', 'Упрощенное'], ['app_navigationTree_connection_view_option_advanced', 'Продвинутое'], + ['app_navigationTree_connection_view_option_custom', 'Настраиваемое'], + ['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'], ['app_navigationTree_refreshNode', 'Обновить'], ['app_navigationTree_context_disconnect', 'Отключить'], ['app_shared_inlineEditor_dialog_title', 'Редактирование'], diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index 2f7ffd77cb..9b32abc2f9 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -8,18 +8,20 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useContext, useState } from 'react'; -import { use } from 'reshadow'; import type { IGridItemsLayoutProps } from '../../Containers/LayoutProps'; import { FormContext } from '../FormContext'; import { isControlPresented } from '../isControlPresented'; import { CheckboxMarkup } from './CheckboxMarkup'; +export type CheckboxMod = 'primary' | 'small'; + export type CheckboxBaseProps = Omit, 'onChange' | 'type' | 'value' | 'checked'> & IGridItemsLayoutProps & { value?: string; checkboxLabel?: string; - mod?: 'surface'; long?: boolean; + mod?: CheckboxMod[]; + ripple?: boolean; }; type CheckboxOnChangeEvent = @@ -55,8 +57,9 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ checkboxLabel, checked: checkedControlled, children, - className, mod, + ripple, + className, long, autoHide, onChange, @@ -114,8 +117,9 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ checked={checked} label={checkboxLabel} className={className} + mod={mod} + ripple={ripple} onChange={handleChange} - {...use({ mod })} /> ); }); diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx index a52a2c468a..5c7de15f09 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx @@ -8,7 +8,9 @@ import styled, { css } from 'reshadow'; -import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { Composes, composes, useStyles } from '@cloudbeaver/core-theming'; + +import type { CheckboxMod } from './Checkbox'; const checkboxStyles = composes( css` @@ -46,7 +48,7 @@ const checkboxStyles = composes( ` ); -const checkboxMod = { +const checkboxMod: Record = { primary: composes( css` checkbox { @@ -54,6 +56,25 @@ const checkboxMod = { } ` ), + small: composes( + css` + checkbox { + composes: theme-checkbox_small from global; + } + `, + css` + checkbox-container { + & checkbox { + width: 14px; + height: 14px; + } + & checkbox-background { + width: 14px; + height: 14px; + } + } + `, + ), }; const checkboxState = { @@ -76,12 +97,14 @@ const checkboxState = { interface ICheckboxMarkupProps extends React.InputHTMLAttributes { label?: string; indeterminate?: boolean; + ripple?: boolean; + mod?: CheckboxMod[]; } export const CheckboxMarkup: React.FC = function CheckboxMarkup({ - label, className, title, ...rest + label, className, title, mod = ['primary'], ripple = true, ...rest }) { - return styled(useStyles(checkboxStyles, checkboxMod.primary, rest.disabled + return styled(useStyles(checkboxStyles, ...(mod || []).map(mod => checkboxMod[mod]), rest.disabled && checkboxState.disabled, rest.checked && checkboxState.checked))( @@ -92,7 +115,9 @@ export const CheckboxMarkup: React.FC = function CheckboxM - + {ripple && ( + + )} {label && rest.id && {label}} diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx index 14286e6d5c..a48f10bf75 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -49,7 +49,7 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ {...rest} checked={checkedControlled} /> - {children} + {children} ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/Radio.tsx b/webapp/packages/core-blocks/src/FormControls/Radio.tsx index 0f056ec80d..5778d2db31 100644 --- a/webapp/packages/core-blocks/src/FormControls/Radio.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Radio.tsx @@ -17,24 +17,24 @@ import { RadioGroupContext } from './RadioGroupContext'; const radioStyles = composes( css` - radio { - composes: theme-radio from global; - } - radio-background { - composes: theme-radio_background from global; - } - input { - composes: theme-radio_native-control from global; - } - radio-outer-circle { - composes: theme-radio_outer-circle from global; - } - radio-inner-circle { - composes: theme-radio_inner-circle from global; - } - radio-ripple { - composes: theme-radio_ripple from global; - } + radio { + composes: theme-radio from global; + } + radio-background { + composes: theme-radio_background from global; + } + input { + composes: theme-radio_native-control from global; + } + radio-outer-circle { + composes: theme-radio_outer-circle from global; + } + radio-inner-circle { + composes: theme-radio_inner-circle from global; + } + radio-ripple { + composes: theme-radio_ripple from global; + } `, css` field { @@ -55,10 +55,41 @@ const radioMod = { } ` ), + small: composes( + css` + radio { + composes: theme-radio_small from global; + } + `, + css` + field { + & radio { + width: 14px; + height: 14px; + } + & radio-background { + width: 14px; + height: 14px; + } + & radio-inner-circle { + border-width: 7px; + } + } + ` + ), }; +const noRippleStyles = composes( + css` + radio { + composes: theme-radio_no-ripple from global; + } + ` +); + type BaseProps = Omit, 'onChange' | 'value' | 'checked'> & { mod?: Array; + ripple?: boolean; }; type ControlledProps = BaseProps & { @@ -91,6 +122,7 @@ export const Radio: RadioType = observer(function Radio({ checked: controlledChecked, onChange, mod, + ripple = true, className, children, ...rest @@ -131,7 +163,7 @@ export const Radio: RadioType = observer(function Radio({ checked = state[name] === value; } - return styled(useStyles(radioStyles, ...(mod || []).map(mod => radioMod[mod])))( + return styled(useStyles(radioStyles, ...(mod || []).map(mod => radioMod[mod]), !ripple && noRippleStyles))( - + {ripple && } diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx index f70915493b..255a5eba99 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx @@ -85,7 +85,6 @@ const RenderField: React.FC = observer(function RenderField({ checkboxLabel={property.displayName} title={property.description} disabled={disabled} - mod='surface' /> ); } diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx index c7078bd6c7..e347462b0f 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx @@ -212,7 +212,6 @@ export const Options: TabContainerPanelComponent = obse disabled={edit || form.form.disabled} readOnly={form.form.readonly} // autoHide={} // maybe better to use autoHide - mod='surface' >{translate('connections_connection_template')} )} @@ -245,7 +244,6 @@ export const Options: TabContainerPanelComponent = obse value={data.config.connectionId + 'authNeeded'} state={data.config} disabled={form.form.disabled || form.form.readonly} - mod='surface' gridItemMax >{translate('connections_connection_edit_save_credentials')} diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx index 2ef0e66eac..782230a28e 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx @@ -12,7 +12,7 @@ import styled from 'reshadow'; import { css } from 'reshadow'; import { useAdministrationSettings } from '@cloudbeaver/core-administration'; -import { Group, SubmittingForm, useMapResource, Button, ColoredContainer, InputFieldNew, FieldCheckboxNew, BASE_CONTAINERS_STYLES, SwitchNew, Grid, GroupItem, Container } from '@cloudbeaver/core-blocks'; +import { Group, SubmittingForm, useMapResource, Button, ColoredContainer, InputFieldNew, FieldCheckboxNew, BASE_CONTAINERS_STYLES, SwitchNew, GroupItem, Container } from '@cloudbeaver/core-blocks'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -160,7 +160,6 @@ export const SSH: TabContainerPanelComponent = observer value={SSH_TUNNEL_ID + ' savePassword'} state={state} disabled={disabled || !enabled || form.form.readonly} - mod='surface' >{translate('connections_network_handler_ssh_tunnel_save_password')} )} diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 1642d42f91..0197656119 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -23,15 +23,26 @@ import { ResourceKey, ResourceKeyUtils, TestConnectionMutation, + NavigatorSettingsInput, } from '@cloudbeaver/core-sdk'; import { ConnectionsResource, DatabaseConnection } from './Administration/ConnectionsResource'; -import { CONNECTION_NAVIGATOR_VIEW_SETTINGS } from './ConnectionNavigatorViewSettings'; +import type { NavigatorViewSettings } from './ConnectionNavigatorViewSettings'; export type Connection = DatabaseConnection & { authProperties?: UserConnectionAuthPropertiesFragment[] }; export type ConnectionInitConfig = Omit; export type ConnectionInfoIncludes = Omit; +export const DEFAULT_NAVIGATOR_VIEW_SETTINGS: NavigatorSettingsInput = { + showOnlyEntities: false, + hideFolders: false, + hideVirtualModel: false, + hideSchemas: false, + mergeEntities: false, + showSystemObjects: false, + showUtilityObjects: false, +}; + @injectable() export class ConnectionInfoResource extends CachedMapResource { readonly onConnectionCreate: IExecutor; @@ -170,13 +181,12 @@ export class ConnectionInfoResource extends CachedMapResource { + async changeConnectionView(id: string, settings: NavigatorViewSettings): Promise { await this.performUpdate(id, [], async () => { - const settings = simple ? CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple : CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced; - + const connectionNavigatorViewSettings = this.get(id)?.navigatorSettings || DEFAULT_NAVIGATOR_VIEW_SETTINGS; const { connection } = await this.graphQLService.sdk.setConnectionNavigatorSettings({ id, - settings, + settings: { ...connectionNavigatorViewSettings, ...settings }, ...this.getDefaultIncludes(), ...this.getIncludesMap(id), }); diff --git a/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts b/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts index d3e66ec0c0..a0e9fd5576 100644 --- a/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts +++ b/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts @@ -8,16 +8,16 @@ import type { NavigatorSettingsInput } from '@cloudbeaver/core-sdk'; -type NavigatorView = 'simple' | 'advanced'; +export type NavigatorView = 'simple' | 'advanced'; +export type NavigatorViewSettings = Partial; -export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record = { +export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record = { simple: { showOnlyEntities: true, hideFolders: true, hideVirtualModel: true, hideSchemas: false, mergeEntities: false, - showSystemObjects: false, showUtilityObjects: false, }, advanced: { @@ -26,13 +26,13 @@ export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record settings[key as keyof NavigatorSettingsInput] !== simple[key as keyof NavigatorSettingsInput]); +export function isNavigatorViewSettingsEqual( + settings: NavigatorSettingsInput, settingsToCompare: NavigatorViewSettings +): boolean { + return !(Object.keys(settingsToCompare) as Array) + .some(key => settings[key] !== settingsToCompare[key]); } diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index fd0e8c8702..8ae53df07a 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -127,7 +127,6 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ checkboxLabel={translate('connections_connection_edit_save_credentials')} disabled={controller.isAuthenticating} state={controller.config} - mod='surface' /> )} diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx index a888d28260..9adc3657ff 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx @@ -81,7 +81,6 @@ export const SSHAuthForm: React.FC = observer(function SSHAuthForm({ state={state} checkboxLabel={translate('connections_network_handler_ssh_tunnel_save_password')} disabled={disabled} - mod='surface' /> )} diff --git a/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts b/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts index cf30d8e1ec..0894ad83cc 100644 --- a/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts +++ b/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts @@ -11,7 +11,7 @@ import { uuid } from '@cloudbeaver/core-utils'; import type { IMenuPanel } from '../IMenuPanel'; import { ComputedMenuItemModel, IComputedMenuItemOptions } from '../models/ComputedMenuItemModel'; -import { MenuOptionsStore } from '../models/MenuOptionsStore'; +import { MenuItemType, MenuOptionsStore } from '../models/MenuOptionsStore'; import type { IContextMenuItem } from './IContextMenuItem'; import type { IMenuContext } from './IMenuContext'; @@ -89,6 +89,9 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions isHidden?: () => boolean; // set icon or getter icon?: string; + isChecked?: () => boolean; + type?: MenuItemType; + separator?: boolean; iconGetter?: () => string | undefined; constructor(private options: IContextMenuItem, @@ -97,6 +100,8 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions this.title = options.title; this.titleGetter = options.titleGetter; this.icon = options.icon; + this.type = options.type; + this.separator = options.separator; this.iconGetter = options.iconGetter; this.id = `${options.id}-${context.contextId!}`; @@ -110,6 +115,9 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions if (options.isHidden) { this.isHidden = () => options.isHidden!(this.context); } + if (options.isChecked) { + this.isChecked = () => options.isChecked!(this.context); + } } } diff --git a/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts b/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts index ae11b53f56..88822b08d3 100644 --- a/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts +++ b/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts @@ -19,4 +19,5 @@ export interface IContextMenuItem extends IMenuItemOptions { isDisabled?: (context: IMenuContext) => boolean; // When the item is present in menu it can be hidden based on certain conditions isHidden?: (context: IMenuContext) => boolean; + isChecked?: (context: IMenuContext) => boolean; } diff --git a/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts b/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts index 6e8f40db3e..90a32ac124 100644 --- a/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts +++ b/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts @@ -12,6 +12,8 @@ import type { MenuInitialState } from 'reakit/Menu'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; import type { Style } from '@cloudbeaver/core-theming'; +import type { MenuItemType } from './models/MenuOptionsStore'; + export type MenuMod = 'primary' | 'surface' | 'secondary'; export interface IMenuPanel { @@ -28,6 +30,9 @@ export interface IMenuItem { isHidden?: boolean; icon?: string; // path to icon or svg icon name panel?: IMenuPanel; // if menu has sub-items + type?: MenuItemType; + separator?: boolean; + isChecked?: boolean; } export type MenuTriggerProps = PropsWithChildren<{ diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx index 08e33ce955..bdf8f57420 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx @@ -9,10 +9,11 @@ import { useObserver } from 'mobx-react-lite'; import styled from 'reshadow'; -import { Icon } from '@cloudbeaver/core-blocks'; +import { Checkbox, Icon, IconOrImage, Radio } from '@cloudbeaver/core-blocks'; import type { IMenuItem } from '@cloudbeaver/core-dialogs'; import { Translate } from '@cloudbeaver/core-localization'; import { useStyles, Style } from '@cloudbeaver/core-theming'; +import { use } from '@reshadow/react'; import { menuPanelStyles } from './menuPanelStyles'; @@ -25,17 +26,37 @@ export const MenuPanelItem: React.FC = function MenuPanelIte menuItem, style = [], }) { - const { title, panel } = useObserver(() => ({ // TODO: provide title and panel via props + const { title, panel, icon, isChecked, controlType, separator } = useObserver(() => ({ // TODO: provide title and panel via props title: menuItem.title, panel: menuItem.panel, + icon: menuItem.icon, + isChecked: menuItem.isChecked, + controlType: menuItem.type, + separator: menuItem.separator, })); + let control = null; + + if (controlType === 'radio') { + control = ; + } else if (controlType === 'checkbox') { + control = ; + } + return styled(useStyles(menuPanelStyles, ...style))( - - + + + {icon ? ( + + ) : control} + + - {panel && } + + {panel && } + + ); }; diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx index 658d440e30..fb02eb8ddf 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx @@ -12,7 +12,7 @@ import { } from 'react'; import { MenuButton, - Menu, MenuItem, MenuStateReturn, useMenuState + Menu, MenuItem, MenuStateReturn, useMenuState, MenuItemCheckbox, MenuItemRadio } from 'reakit/Menu'; import styled, { use } from 'reshadow'; @@ -117,6 +117,42 @@ const MenuPanelElement = observer(function MenuPanelElement({ ); } + if (item.type === 'radio') { + return styled(styles)( + + + + + ); + } + + if (item.type === 'checkbox') { + return styled(styles)( + + + + + ); + } + return styled(styles)( void; isDisabled?: () => boolean; isHidden?: () => boolean; + isChecked?: () => boolean; } export class ComputedMenuItemModel implements IMenuItem { id: string; onClick?: () => void; panel?: IMenuPanel; + type?: MenuItemType; + separator?: boolean; rtl?: boolean; get title(): TLocalizationToken { @@ -47,15 +50,22 @@ export class ComputedMenuItemModel implements IMenuItem { return this.options.isHidden ? this.options.isHidden() : false; } + get isChecked() { + return this.options.isChecked ? this.options.isChecked() : false; + } + constructor(private options: IComputedMenuItemOptions) { makeObservable(this, { title: computed, isDisabled: computed, icon: computed, isHidden: computed, + isChecked: computed, }); this.id = options.id; + this.type = options.type; + this.separator = options.separator; this.rtl = options.rtl; this.panel = options.panel; this.onClick = this.options.onClick; diff --git a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts index e538733c49..fd458186a8 100644 --- a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts +++ b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts @@ -13,6 +13,7 @@ import type { IMenuPanel } from '../IMenuPanel'; const DEFAULT_ITEM_ORDER = 100; +export type MenuItemType = 'checkbox' | 'radio'; export interface IMenuItemOptions { id: string; // set title or getter @@ -24,6 +25,8 @@ export interface IMenuItemOptions { order?: number; isPanel?: boolean; panel?: IMenuPanel; + type?: MenuItemType; + separator?: boolean; rtl?: boolean; } diff --git a/webapp/packages/core-theming/src/styles/_checkbox.scss b/webapp/packages/core-theming/src/styles/_checkbox.scss index 9b1be876c2..8066cc7b31 100644 --- a/webapp/packages/core-theming/src/styles/_checkbox.scss +++ b/webapp/packages/core-theming/src/styles/_checkbox.scss @@ -6,44 +6,53 @@ * you may not use this file except in compliance with the License. */ - @import '@material/checkbox/mixins'; - $mdc-checkbox-icon-size: 16px; +@import "@material/checkbox/mixins"; +$mdc-checkbox-icon-size: 16px; - @mixin checkbox-core-styles($query: mdc-feature-all()) { - @include mdc-checkbox-core-styles($query); - - .theme-checkbox { - composes: mdc-checkbox; - } - .theme-checkbox_native-control { - composes: mdc-checkbox__native-control; - } - .theme-checkbox__background { - composes: mdc-checkbox__background; - } - .theme-checkbox__checkmark { - composes: mdc-checkbox__checkmark; - } - .theme-checkbox__checkmark-path { - composes: mdc-checkbox__checkmark-path; - } - .theme-checkbox__mixedmark { +@mixin checkbox-core-styles($query: mdc-feature-all()) { + @include mdc-checkbox-core-styles($query); + + .theme-checkbox { + composes: mdc-checkbox; + } + .theme-checkbox_native-control { + composes: mdc-checkbox__native-control; + } + .theme-checkbox__background { + composes: mdc-checkbox__background; + } + .theme-checkbox__checkmark { + composes: mdc-checkbox__checkmark; + } + .theme-checkbox__checkmark-path { + composes: mdc-checkbox__checkmark-path; + } + .theme-checkbox__mixedmark { composes: mdc-checkbox__mixedmark; - } - .theme-checkbox__ripple { + } + .theme-checkbox__ripple { composes: mdc-checkbox__ripple; - } - .theme-checkbox--checked { - composes: mdc-checkbox--selected; - } - .theme-checkbox--disabled { - composes: mdc-checkbox--disabled; - } - - .theme-checkbox_primary { - $mdc-checkbox-mark-color: $mdc-theme-on-primary; - @include mdc-checkbox-ink-color($mdc-checkbox-mark-color, $query); - @include mdc-checkbox-disabled-ink-color(rgba($mdc-checkbox-mark-color, .50), $query); - @include mdc-checkbox-container-colors($mdc-checkbox-border-color, transparent, $mdc-theme-primary, $mdc-theme-primary ) - } - } \ No newline at end of file + } + .theme-checkbox--checked { + composes: mdc-checkbox--selected; + } + .theme-checkbox--disabled { + composes: mdc-checkbox--disabled; + } + + .theme-checkbox_primary { + $mdc-checkbox-mark-color: $mdc-theme-on-primary; + @include mdc-checkbox-ink-color($mdc-checkbox-mark-color, $query); + @include mdc-checkbox-disabled-ink-color(rgba($mdc-checkbox-mark-color, 0.5), $query); + @include mdc-checkbox-container-colors( + $mdc-checkbox-border-color, + transparent, + $mdc-theme-primary, + $mdc-theme-primary + ); + } + + .theme-checkbox_small { + @include mdc-checkbox-density(-3); + } +} diff --git a/webapp/packages/core-theming/src/styles/_color.scss b/webapp/packages/core-theming/src/styles/_color.scss index 6db461a5b8..60a84fdfca 100644 --- a/webapp/packages/core-theming/src/styles/_color.scss +++ b/webapp/packages/core-theming/src/styles/_color.scss @@ -52,7 +52,7 @@ } .theme-border-color-#{$style} { @include mdc-feature-targets($feat-color) { - @include mdc-theme-prop(border-color, $style); + @include mdc-theme-prop(border-color, $style, true); } } } diff --git a/webapp/packages/core-theming/src/styles/_radio.scss b/webapp/packages/core-theming/src/styles/_radio.scss index 86ff192b38..b0037f20e5 100644 --- a/webapp/packages/core-theming/src/styles/_radio.scss +++ b/webapp/packages/core-theming/src/styles/_radio.scss @@ -6,11 +6,11 @@ * you may not use this file except in compliance with the License. */ -@import '@material/radio/mixins'; +@import "@material/radio/mixins"; @mixin radio-core-styles($query: mdc-feature-all()) { @include mdc-radio-core-styles; - + .theme-radio { composes: mdc-radio; } @@ -32,8 +32,8 @@ .theme-radio_primary { $mdc-radio-baseline-theme-color: primary; - $mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), .54); - $mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), .26); + $mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), 0.54); + $mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), 0.26); @include mdc-radio-unchecked-stroke-color($mdc-radio-unchecked-color, $query: $query); @include mdc-radio-checked-stroke-color($mdc-radio-baseline-theme-color, $query: $query); @@ -43,4 +43,12 @@ @include mdc-radio-disabled-ink-color($mdc-radio-disabled-circle-color, $query: $query); @include mdc-radio-focus-indicator-color($mdc-radio-baseline-theme-color, $query: $query); } -} \ No newline at end of file + + .theme-radio_small { + @include mdc-radio-density(-3); + } + + .theme-radio_no-ripple { + @include mdc-radio-ripple-size(0); + } +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx index c9c4b89ed8..89575ede21 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import React, { useCallback } from 'react'; import { FormGroup, Switch } from '@cloudbeaver/core-blocks'; -import { isSimpleNavigatorView, CONNECTION_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; +import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-connections'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; @@ -24,13 +24,13 @@ export const ServerConfigurationNavigatorViewForm: React.FC = observer(fu }) { const translate = useTranslate(); - const isSimpleView = isSimpleNavigatorView(configs.navigatorConfig); + const isSimpleView = isNavigatorViewSettingsEqual(configs.navigatorConfig, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple); const onNavigatorViewChangeHandler = useCallback((value: boolean) => { if (value) { - configs.navigatorConfig = { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple }; + configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple }; } else { - configs.navigatorConfig = { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced }; + configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced }; } }, [configs]); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 441dafeff1..e752b6f2ec 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -10,7 +10,7 @@ import { observable, makeObservable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { UsersResource } from '@cloudbeaver/core-authentication'; -import { CONNECTION_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; +import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor } from '@cloudbeaver/core-executor'; @@ -152,7 +152,7 @@ export class ServerConfigurationService { customConnectionsEnabled: true, sessionExpireTime: 30, }, - navigatorConfig: { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced }, + navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, }; } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx index 484eda2186..61b575f16e 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx @@ -103,7 +103,6 @@ export const UserInfo: TabContainerPanelComponent = observer(fun checkboxLabel={role.roleName || role.roleId} checked={!!controller.credentials.roles.get(role.roleId)} disabled={controller.isSaving} - mod='surface' onChange={checked => handleRoleChange(role.roleId, checked)} /> diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx index a2cc493e69..b1161b584d 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx @@ -115,7 +115,6 @@ export const ConnectionDialog = observer(function ConnectionDialog({ checkboxLabel={translate('connections_connection_edit_save_credentials')} disabled={controller.isConnecting} state={controller.config} - mod='surface' /> )}