feat(core-dialogs): CB-747 add radio and checkbox to Menu component

This commit is contained in:
Naumov Alexey
2021-03-09 01:15:40 +03:00
parent a188a34bf9
commit 87e06ef5a8
13 changed files with 186 additions and 26 deletions
@@ -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<NavNode>(
@@ -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<NavNode>(
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'],
});
},
}
);
}
@@ -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'],
@@ -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', 'Редактирование'],
@@ -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<InitConnectionMutationVariables, 'includeOrigin' | 'customIncludeOriginDetails' | 'includeAuthProperties' | 'customIncludeNetworkHandlerCredentials'>;
@@ -167,10 +167,8 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
return this.get(config.id)!;
}
async changeConnectionView(id: string, simple: boolean): Promise<Connection> {
async changeConnectionView(id: string, settings: NavigatorSettingsInput): Promise<Connection> {
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,
@@ -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<NavigatorView, NavigatorSettingsInput> = {
type NavigatorViewSettings = Record<Exclude<NavigatorView, 'custom'>, NavigatorSettingsInput>;
export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: NavigatorViewSettings = {
simple: {
showOnlyEntities: true,
hideFolders: true,
@@ -31,8 +34,34 @@ export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record<NavigatorView, Navigator
},
};
export function isSimpleNavigatorView(settings: NavigatorSettingsInput): boolean {
const simple = CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple;
return !Object.keys(simple)
.some(key => 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];
});
}
@@ -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<T> implements IComputedMenuItemOptions
isHidden?: () => boolean;
// set icon or getter
icon?: string;
isChecked?: () => boolean;
type?: MenuItemControlType;
iconGetter?: () => string | undefined;
constructor(private options: IContextMenuItem<T>,
@@ -97,6 +99,7 @@ class ComputedMenuItemOptionsWithContext<T> 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<T> implements IComputedMenuItemOptions
if (options.isHidden) {
this.isHidden = () => options.isHidden!(this.context);
}
if (options.isChecked) {
this.isChecked = () => options.isChecked!(this.context);
}
}
}
@@ -19,4 +19,5 @@ export interface IContextMenuItem<T> extends IMenuItemOptions {
isDisabled?: (context: IMenuContext<T>) => boolean;
// When the item is present in menu it can be hidden based on certain conditions
isHidden?: (context: IMenuContext<T>) => boolean;
isChecked?: (context: IMenuContext<T>) => boolean;
}
@@ -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<{
@@ -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)(
<MenuItemRadio
{...menu}
{...use({ hidden: item.isHidden })}
aria-label={item.id}
disabled={item.isDisabled}
name={item.id}
value={item.title}
checked={item.isChecked}
onClick={onClick}
>
<MenuPanelItem menuItem={item} style={style} />
</MenuItemRadio>
);
}
return styled(styles)(
<MenuItemCheckbox
{...menu}
{...use({ hidden: item.isHidden })}
aria-label={item.id}
disabled={item.isDisabled}
name={item.id}
value={item.title}
checked={item.isChecked}
onClick={onClick}
>
<MenuPanelItem menuItem={item} style={style} />
</MenuItemCheckbox>
);
}
return styled(styles)(
<MenuItem
{...menu}
@@ -52,11 +52,11 @@ export const menuPanelStyles = composes(
margin-top: -12px;
}
}
MenuItem {
MenuItem, MenuItemCheckbox, MenuItemRadio {
display: flex;
border: none;
background: none;
padding: 6px 16px;
padding: 6px 16px 6px 20px;
text-align: left;
outline: none;
color: inherit;
@@ -75,6 +75,27 @@ export const menuPanelStyles = composes(
}
}
}
MenuItemCheckbox:global([aria-checked="true"]) {
position: relative;
&:before {
content: "✓";
position: absolute;
left: 6px;
top: 4px;
}
}
MenuItemRadio:global([aria-checked="true"]) {
position: relative;
&:before {
content: "•";
position: absolute;
left: 6px;
top: -3px;
font-size: 2em;
}
}
menu-panel-item {
flex: 1;
display: flex;
@@ -11,18 +11,20 @@ import { computed, makeObservable } from 'mobx';
import type { IMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs';
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
import type { IMenuItemOptions } from './MenuOptionsStore';
import type { IMenuItemOptions, MenuItemControlType } from './MenuOptionsStore';
export interface IComputedMenuItemOptions extends IMenuItemOptions {
onClick?: () => 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;
@@ -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;
}
@@ -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<Props> = observer(fu
}) {
const translate = useTranslate();
const isSimpleView = isSimpleNavigatorView(configs.navigatorConfig);
const isSimpleView = getNavigatorView(configs.navigatorConfig) === 'simple';
const onNavigatorViewChangeHandler = useCallback((value: boolean) => {
if (value) {