From 87e06ef5a8242830fc2a8f4e9a0497b60c7e2efb Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Tue, 9 Mar 2021 01:15:40 +0300 Subject: [PATCH] feat(core-dialogs): CB-747 add radio and checkbox to Menu component --- .../NavigationTreeContextMenuService.ts | 71 ++++++++++++++++--- webapp/packages/core-app/src/locales/en.ts | 2 + webapp/packages/core-app/src/locales/ru.ts | 2 + .../src/ConnectionInfoResource.ts | 6 +- .../src/ConnectionNavigatorViewSettings.ts | 41 +++++++++-- .../src/Menu/ContextMenu/ContextMenu.ts | 8 ++- .../src/Menu/ContextMenu/IContextMenuItem.ts | 1 + .../core-dialogs/src/Menu/IMenuPanel.ts | 4 ++ .../src/Menu/MenuPanel/MenuTrigger.tsx | 36 +++++++++- .../src/Menu/MenuPanel/menuPanelStyles.ts | 25 ++++++- .../src/Menu/models/ComputedMenuItemModel.ts | 10 ++- .../src/Menu/models/MenuOptionsStore.ts | 2 + .../ServerConfigurationNavigatorViewForm.tsx | 4 +- 13 files changed, 186 insertions(+), 26 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index 1357a6ecd3..312064af8b 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -6,10 +6,11 @@ * 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, getNavigatorView, isNavigatorSettingEnabled, NavigatorViewSettingsKeys } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; +import type { NavigatorSettingsInput } from '@cloudbeaver/core-sdk'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; @@ -46,23 +47,43 @@ export class NavigationTreeContextMenuService { }); } - private isConnectionSimpleView(nodeId: string): boolean { + private getConnectionViewSettings(nodeId: string) { const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeId); const connection = this.connectionInfoResource.get(connectionId); if (!connection) { + return null; + } + + return connection.navigatorSettings; + } + + private getConnectionNavigatorView(nodeId: string) { + const connectionViewSettings = this.getConnectionViewSettings(nodeId); + + if (!connectionViewSettings) { + return null; + } + + return getNavigatorView(connectionViewSettings); + } + + private isConnectionViewSettingEnabled(nodeId: string, setting: NavigatorViewSettingsKeys) { + const connectionViewSettings = this.getConnectionViewSettings(nodeId); + + if (!connectionViewSettings) { return false; } - return isSimpleNavigatorView(connection.navigatorSettings); + return isNavigatorSettingEnabled(setting, connectionViewSettings); } - private async changeConnectionView(nodeId: string, simple: boolean) { + private async changeConnectionView(nodeId: string, settings: NavigatorSettingsInput) { 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 +109,17 @@ export class NavigationTreeContextMenuService { { id: 'simple', title: 'app_navigationTree_connection_view_option_simple', - isDisabled: context => this.isConnectionSimpleView(context.data.id), + type: 'radio', + isChecked: context => this.getConnectionNavigatorView(context.data.id) === 'simple', 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, + showSystemObjects: this.isConnectionViewSettingEnabled(context.data.id, 'showSystemObjects'), + }), } ); this.contextMenuService.addMenuItem( @@ -101,12 +127,39 @@ export class NavigationTreeContextMenuService { { id: 'advanced', title: 'app_navigationTree_connection_view_option_advanced', - isDisabled: context => !this.isConnectionSimpleView(context.data.id), + type: 'radio', + isChecked: context => this.getConnectionNavigatorView(context.data.id) === 'advanced', 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, + showSystemObjects: this.isConnectionViewSettingEnabled(context.data.id, 'showSystemObjects'), + }), + } + ); + this.contextMenuService.addMenuItem( + this.getNodeViewMenuItemToken(), + { + id: 'systemObjects', + title: 'app_navigationTree_connection_view_option_showSystemObjects', + isChecked: context => this.isConnectionViewSettingEnabled(context.data.id, 'showSystemObjects'), + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType + && context.data.objectFeatures.includes(EObjectFeature.dataSource); + }, + onClick: async context => { + const currentSettings = this.getConnectionViewSettings(context.data.id); + 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..ea49c50235 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-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 4da8b2fb2b..b73565ca68 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -22,10 +22,10 @@ import { GetUserConnectionsQueryVariables, ResourceKey, ResourceKeyUtils, + NavigatorSettingsInput, } from '@cloudbeaver/core-sdk'; import { ConnectionsResource, DatabaseConnection } from './Administration/ConnectionsResource'; -import { CONNECTION_NAVIGATOR_VIEW_SETTINGS } from './ConnectionNavigatorViewSettings'; export type Connection = DatabaseConnection & { authProperties?: UserConnectionAuthPropertiesFragment[] }; export type ConnectionInitConfig = Omit; @@ -167,10 +167,8 @@ export class ConnectionInfoResource extends CachedMapResource { + async changeConnectionView(id: string, settings: NavigatorSettingsInput): Promise { await this.performUpdate(id, [], async () => { - const settings = simple ? CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple : CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced; - const { connection } = await this.graphQLService.sdk.setConnectionNavigatorSettings({ id, settings, diff --git a/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts b/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts index d3e66ec0c0..dd5a54e0c7 100644 --- a/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts +++ b/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts @@ -8,9 +8,12 @@ import type { NavigatorSettingsInput } from '@cloudbeaver/core-sdk'; -type NavigatorView = 'simple' | 'advanced'; +export type NavigatorView = 'simple' | 'advanced' | 'custom'; +export type NavigatorViewSettingsKeys = keyof NavigatorSettingsInput; -export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record = { +type NavigatorViewSettings = Record, NavigatorSettingsInput>; + +export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: NavigatorViewSettings = { simple: { showOnlyEntities: true, hideFolders: true, @@ -31,8 +34,34 @@ export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record settings[key as keyof NavigatorSettingsInput] !== simple[key as keyof NavigatorSettingsInput]); +export function getNavigatorView(settings: NavigatorSettingsInput): NavigatorView { + const isSimple = isNavigatorViewSettingsEqual(settings, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple); + const isAdvanced = isNavigatorViewSettingsEqual(settings, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced); + + if (isSimple) { + return 'simple'; + } + + if (isAdvanced) { + return 'advanced'; + } + + return 'custom'; +} + +export function isNavigatorSettingEnabled( + setting: NavigatorViewSettingsKeys, settings: NavigatorSettingsInput +): boolean { + return settings[setting]; +} + +function isNavigatorViewSettingsEqual(settings: NavigatorSettingsInput, settingsToCompare: NavigatorSettingsInput) { + return !(Object.keys(settingsToCompare) as NavigatorViewSettingsKeys[]) + .some(key => { + // we need to exclude it for now, cause we haven't implement custom navigator view logic yet + if (key === 'showSystemObjects') { + return false; + } + return settings[key] !== settingsToCompare[key]; + }); } diff --git a/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts b/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts index cf30d8e1ec..2911161a49 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 { MenuItemControlType, MenuOptionsStore } from '../models/MenuOptionsStore'; import type { IContextMenuItem } from './IContextMenuItem'; import type { IMenuContext } from './IMenuContext'; @@ -89,6 +89,8 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions isHidden?: () => boolean; // set icon or getter icon?: string; + isChecked?: () => boolean; + type?: MenuItemControlType; iconGetter?: () => string | undefined; constructor(private options: IContextMenuItem, @@ -97,6 +99,7 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions this.title = options.title; this.titleGetter = options.titleGetter; this.icon = options.icon; + this.type = options.type; this.iconGetter = options.iconGetter; this.id = `${options.id}-${context.contextId!}`; @@ -110,6 +113,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..f0f054be6b 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 { MenuItemControlType } from './models/MenuOptionsStore'; + export type MenuMod = 'primary' | 'surface' | 'secondary'; export interface IMenuPanel { @@ -28,6 +30,8 @@ export interface IMenuItem { isHidden?: boolean; icon?: string; // path to icon or svg icon name panel?: IMenuPanel; // if menu has sub-items + type?: MenuItemControlType; + isChecked?: boolean; } export type MenuTriggerProps = PropsWithChildren<{ diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx index 658d440e30..23aafd1b11 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,40 @@ const MenuPanelElement = observer(function MenuPanelElement({ ); } + if (item.isChecked !== undefined) { + if (item.type === 'radio') { + return styled(styles)( + + + + + ); + } + 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?: MenuItemControlType; rtl?: boolean; get title(): TLocalizationToken { @@ -47,15 +49,21 @@ export class ComputedMenuItemModel implements IMenuItem { return this.options.isHidden ? this.options.isHidden() : false; } + get isChecked() { + return this.options.isChecked ? this.options.isChecked() : undefined; + } + 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.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..c09bc08a3a 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 MenuItemControlType = 'checkbox' | 'radio'; export interface IMenuItemOptions { id: string; // set title or getter @@ -24,6 +25,7 @@ export interface IMenuItemOptions { order?: number; isPanel?: boolean; panel?: IMenuPanel; + type?: MenuItemControlType; rtl?: boolean; } 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..f91780f6cf 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, getNavigatorView } from '@cloudbeaver/core-connections'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; @@ -24,7 +24,7 @@ export const ServerConfigurationNavigatorViewForm: React.FC = observer(fu }) { const translate = useTranslate(); - const isSimpleView = isSimpleNavigatorView(configs.navigatorConfig); + const isSimpleView = getNavigatorView(configs.navigatorConfig) === 'simple'; const onNavigatorViewChangeHandler = useCallback((value: boolean) => { if (value) {