mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core-dialogs): CB-747 add radio and checkbox to Menu component
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user