Navigation: Implement logic for docking nav menu (#76188)

* Create a state for dockedMegaMenu and the function to manage it

* Add the dockedMenu icon and handle the status when clicking it

* Add Megamenu to section nav area when it is docked

* get logic working

* fix mobile

* refactor state + persist in localStorage

* adjust icon and don't use position absolute

* restore old rudderstack tracking

* use Flex instead

* adjust feature toggle to be experimental

* extract out localStorage handling into utils

* don't need separate file

* use store.set/get instead

---------

Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
This commit is contained in:
Ashley Harrison 2023-10-10 14:55:52 +01:00 committed by GitHub
parent de2d8f50e8
commit 930c753340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 117 additions and 35 deletions

View File

@ -64,7 +64,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `newDBLibrary` | Use jmoiron/sqlx rather than xorm for a few backend services | | `newDBLibrary` | Use jmoiron/sqlx rather than xorm for a few backend services |
| `autoMigrateOldPanels` | Migrate old angular panels to supported versions (graph, table-old, worldmap, etc) | | `autoMigrateOldPanels` | Migrate old angular panels to supported versions (graph, table-old, worldmap, etc) |
| `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. | | `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. |
| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu |
| `grpcServer` | Run the GRPC server | | `grpcServer` | Run the GRPC server |
| `accessControlOnCall` | Access control primitives for OnCall | | `accessControlOnCall` | Access control primitives for OnCall |
| `nestedFolders` | Enable folder nesting | | `nestedFolders` | Enable folder nesting |
@ -97,6 +96,7 @@ Experimental features might be changed or removed without prior notice.
| `scenes` | Experimental framework to build interactive dashboards | | `scenes` | Experimental framework to build interactive dashboards |
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu |
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | | `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
| `alertingBacktesting` | Rule backtesting API for alerting | | `alertingBacktesting` | Rule backtesting API for alerting |

View File

@ -225,6 +225,7 @@ export const availableIconsIndex = {
'vertical-align-bottom': true, 'vertical-align-bottom': true,
'vertical-align-center': true, 'vertical-align-center': true,
'vertical-align-top': true, 'vertical-align-top': true,
'web-section-alt': true,
'wrap-text': true, 'wrap-text': true,
rss: true, rss: true,
x: true, x: true,

View File

@ -164,7 +164,7 @@ var (
{ {
Name: "dockedMegaMenu", Name: "dockedMegaMenu",
Description: "Enable support for a persistent (docked) navigation menu", Description: "Enable support for a persistent (docked) navigation menu",
Stage: FeatureStagePublicPreview, Stage: FeatureStageExperimental,
FrontendOnly: true, FrontendOnly: true,
Owner: grafanaFrontendPlatformSquad, Owner: grafanaFrontendPlatformSquad,
}, },

View File

@ -22,7 +22,7 @@ disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,fals
logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false,false
dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false,false dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false,false
topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false,false topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false,false
dockedMegaMenu,preview,@grafana/grafana-frontend-platform,false,false,false,true dockedMegaMenu,experimental,@grafana/grafana-frontend-platform,false,false,false,true
grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false,false grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false,false
entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,false entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,false
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
22 logRequestsInstrumentedAsUnknown experimental @grafana/hosted-grafana-team false false false false
23 dataConnectionsConsole GA @grafana/plugins-platform-backend false false false false
24 topnav deprecated @grafana/grafana-frontend-platform false false false false
25 dockedMegaMenu preview experimental @grafana/grafana-frontend-platform false false false true
26 grpcServer preview @grafana/grafana-app-platform-squad false false false false
27 entityStore experimental @grafana/grafana-app-platform-squad true false false false
28 cloudWatchCrossAccountQuerying GA @grafana/aws-datasources false false false false

View File

@ -10,6 +10,7 @@ import { CommandPalette } from 'app/features/commandPalette/CommandPalette';
import { KioskMode } from 'app/types'; import { KioskMode } from 'app/types';
import { AppChromeMenu } from './AppChromeMenu'; import { AppChromeMenu } from './AppChromeMenu';
import { MegaMenu as DockedMegaMenu } from './DockedMegaMenu/MegaMenu';
import { MegaMenu } from './MegaMenu/MegaMenu'; import { MegaMenu } from './MegaMenu/MegaMenu';
import { NavToolbar } from './NavToolbar/NavToolbar'; import { NavToolbar } from './NavToolbar/NavToolbar';
import { SectionNav } from './SectionNav/SectionNav'; import { SectionNav } from './SectionNav/SectionNav';
@ -53,7 +54,7 @@ export function AppChrome({ children }: Props) {
pageNav={state.pageNav} pageNav={state.pageNav}
actions={state.actions} actions={state.actions}
onToggleSearchBar={chrome.onToggleSearchBar} onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={chrome.onToggleMegaMenu} onToggleMegaMenu={() => chrome.setMegaMenu(state.megaMenu === 'closed' ? 'open' : 'closed')}
onToggleKioskMode={chrome.onToggleKioskMode} onToggleKioskMode={chrome.onToggleKioskMode}
/> />
</div> </div>
@ -64,6 +65,9 @@ export function AppChrome({ children }: Props) {
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && ( {state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && (
<SectionNav model={state.sectionNav} /> <SectionNav model={state.sectionNav} />
)} )}
{config.featureToggles.dockedMegaMenu && !state.chromeless && state.megaMenu === 'docked' && (
<DockedMegaMenu className={styles.dockedMegaMenu} onClose={() => chrome.setMegaMenu('closed')} />
)}
<div className={styles.pageContainer} id="pageContent"> <div className={styles.pageContainer} id="pageContent">
{children} {children}
</div> </div>
@ -74,7 +78,7 @@ export function AppChrome({ children }: Props) {
{config.featureToggles.dockedMegaMenu ? ( {config.featureToggles.dockedMegaMenu ? (
<AppChromeMenu /> <AppChromeMenu />
) : ( ) : (
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} /> <MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu('closed')} />
)} )}
<CommandPalette /> <CommandPalette />
</> </>
@ -102,6 +106,12 @@ const getStyles = (theme: GrafanaTheme2) => {
contentChromeless: css({ contentChromeless: css({
paddingTop: 0, paddingTop: 0,
}), }),
dockedMegaMenu: css({
background: theme.colors.background.primary,
borderRight: `1px solid ${theme.colors.border.weak}`,
borderTop: `1px solid ${theme.colors.border.weak}`,
zIndex: theme.zIndex.navbarFixed,
}),
topNav: css({ topNav: css({
display: 'flex', display: 'flex',
position: 'fixed', position: 'fixed',

View File

@ -27,8 +27,8 @@ export function AppChromeMenu({}: Props) {
const animationSpeed = theme.transitions.duration.shortest; const animationSpeed = theme.transitions.duration.shortest;
const animationStyles = useStyles2(getAnimStyles, animationSpeed); const animationStyles = useStyles2(getAnimStyles, animationSpeed);
const isOpen = state.megaMenuOpen; const isOpen = state.megaMenu === 'open';
const onClose = () => chrome.setMegaMenu(false); const onClose = () => chrome.setMegaMenu('closed');
const { overlayProps, underlayProps } = useOverlay( const { overlayProps, underlayProps } = useOverlay(
{ {

View File

@ -2,7 +2,7 @@ import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { AppEvents, NavModel, NavModelItem, PageLayoutType, UrlQueryValue } from '@grafana/data'; import { AppEvents, NavModel, NavModelItem, PageLayoutType, UrlQueryValue } from '@grafana/data';
import { locationService, reportInteraction } from '@grafana/runtime'; import { config, locationService, reportInteraction } from '@grafana/runtime';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import store from 'app/core/store'; import store from 'app/core/store';
@ -17,11 +17,13 @@ export interface AppChromeState {
pageNav?: NavModelItem; pageNav?: NavModelItem;
actions?: React.ReactNode; actions?: React.ReactNode;
searchBarHidden?: boolean; searchBarHidden?: boolean;
megaMenuOpen?: boolean; megaMenu: 'open' | 'closed' | 'docked';
kioskMode: KioskMode | null; kioskMode: KioskMode | null;
layout: PageLayoutType; layout: PageLayoutType;
} }
const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked';
export class AppChromeService { export class AppChromeService {
searchBarStorageKey = 'SearchBar_Hidden'; searchBarStorageKey = 'SearchBar_Hidden';
private currentRoute?: RouteDescriptor; private currentRoute?: RouteDescriptor;
@ -31,6 +33,8 @@ export class AppChromeService {
chromeless: true, // start out hidden to not flash it on pages without chrome chromeless: true, // start out hidden to not flash it on pages without chrome
sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } }, sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } },
searchBarHidden: store.getBool(this.searchBarStorageKey, false), searchBarHidden: store.getBool(this.searchBarStorageKey, false),
megaMenu:
config.featureToggles.dockedMegaMenu && store.getBool(DOCKED_LOCAL_STORAGE_KEY, false) ? 'docked' : 'closed',
kioskMode: null, kioskMode: null,
layout: PageLayoutType.Canvas, layout: PageLayoutType.Canvas,
}); });
@ -93,14 +97,14 @@ export class AppChromeService {
return useObservable(this.state, this.state.getValue()); return useObservable(this.state, this.state.getValue());
} }
public onToggleMegaMenu = () => { public setMegaMenu = (newMegaMenuState: AppChromeState['megaMenu']) => {
const isOpen = !this.state.getValue().megaMenuOpen; if (config.featureToggles.dockedMegaMenu) {
reportInteraction('grafana_toggle_menu_clicked', { action: isOpen ? 'open' : 'close' }); store.set(DOCKED_LOCAL_STORAGE_KEY, newMegaMenuState === 'docked');
this.update({ megaMenuOpen: isOpen }); reportInteraction('grafana_mega_menu_state', { state: newMegaMenuState });
}; } else {
reportInteraction('grafana_toggle_menu_clicked', { action: newMegaMenuState === 'open' ? 'open' : 'close' });
public setMegaMenu = (megaMenuOpen: boolean) => { }
this.update({ megaMenuOpen }); this.update({ megaMenu: newMegaMenuState });
}; };
public onToggleSearchBar = () => { public onToggleSearchBar = () => {

View File

@ -35,7 +35,7 @@ const setup = () => {
]; ];
const grafanaContext = getGrafanaContextMock(); const grafanaContext = getGrafanaContextMock();
grafanaContext.chrome.onToggleMegaMenu(); grafanaContext.chrome.setMegaMenu('open');
return render( return render(
<TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}> <TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}>

View File

@ -6,6 +6,9 @@ import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui';
import { Flex } from '@grafana/ui/src/unstable';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from 'app/core/internationalization';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { MegaMenuItem } from './MegaMenuItem'; import { MegaMenuItem } from './MegaMenuItem';
@ -22,6 +25,8 @@ export const MegaMenu = React.memo(
const navBarTree = useSelector((state) => state.navBarTree); const navBarTree = useSelector((state) => state.navBarTree);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const location = useLocation(); const location = useLocation();
const { chrome } = useGrafana();
const state = chrome.useState();
const navTree = cloneDeep(navBarTree); const navTree = cloneDeep(navBarTree);
@ -32,13 +37,16 @@ export const MegaMenu = React.memo(
const activeItem = getActiveItem(navItems, location.pathname); const activeItem = getActiveItem(navItems, location.pathname);
const handleDockedMenu = () => {
chrome.setMegaMenu(state.megaMenu === 'docked' ? 'closed' : 'docked');
};
return ( return (
<div data-testid="navbarmenu" ref={ref} {...restProps}> <div data-testid="navbarmenu" ref={ref} {...restProps}>
<div className={styles.mobileHeader}> <div className={styles.mobileHeader}>
<Icon name="bars" size="xl" /> <Icon name="bars" size="xl" />
<IconButton <IconButton
aria-label="Close navigation menu" tooltip={t('navigation.megamenu.close', 'Close menu')}
tooltip="Close menu"
name="times" name="times"
onClick={onClose} onClick={onClose}
size="xl" size="xl"
@ -48,8 +56,27 @@ export const MegaMenu = React.memo(
<nav className={styles.content}> <nav className={styles.content}>
<CustomScrollbar showScrollIndicators hideHorizontalTrack> <CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}> <ul className={styles.itemList}>
{navItems.map((link) => ( {navItems.map((link, index) => (
<MegaMenuItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} /> <Flex key={link.text} direction="row" alignItems="center">
<MegaMenuItem
link={link}
onClick={state.megaMenu === 'open' ? onClose : undefined}
activeItem={activeItem}
/>
{index === 0 && (
<IconButton
className={styles.dockMenuButton}
tooltip={
state.megaMenu === 'docked'
? t('navigation.megamenu.undock', 'Undock menu')
: t('navigation.megamenu.dock', 'Dock menu')
}
name="web-section-alt"
onClick={handleDockedMenu}
variant="secondary"
/>
)}
</Flex>
))} ))}
</ul> </ul>
</CustomScrollbar> </CustomScrollbar>
@ -65,8 +92,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
content: css({ content: css({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, height: '100%',
minHeight: 0, minHeight: 0,
position: 'relative',
}), }),
mobileHeader: css({ mobileHeader: css({
display: 'flex', display: 'flex',
@ -79,10 +107,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
}, },
}), }),
itemList: css({ itemList: css({
display: 'grid', display: 'flex',
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, flexDirection: 'column',
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
listStyleType: 'none', listStyleType: 'none',
minWidth: MENU_WIDTH, minWidth: MENU_WIDTH,
[theme.breakpoints.up('md')]: {
width: MENU_WIDTH,
},
}),
dockMenuButton: css({
marginRight: theme.spacing(2),
}), }),
}); });

View File

@ -15,11 +15,11 @@ import { hasChildMatch } from './utils';
interface Props { interface Props {
link: NavModelItem; link: NavModelItem;
activeItem?: NavModelItem; activeItem?: NavModelItem;
onClose?: () => void; onClick?: () => void;
level?: number; level?: number;
} }
export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) { export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment; const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment;
const isActive = link === activeItem; const isActive = link === activeItem;
@ -29,13 +29,13 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) {
const showExpandButton = linkHasChildren(link) || link.emptyMessage; const showExpandButton = linkHasChildren(link) || link.emptyMessage;
return ( return (
<li> <li className={styles.listItem}>
<div className={styles.collapsibleSectionWrapper}> <div className={styles.collapsibleSectionWrapper}>
<MegaMenuItemText <MegaMenuItemText
isActive={isActive} isActive={isActive}
onClick={() => { onClick={() => {
link.onClick?.(); link.onClick?.();
onClose?.(); onClick?.();
}} }}
target={link.target} target={link.target}
url={link.url} url={link.url}
@ -75,7 +75,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) {
key={`${link.text}-${childLink.text}`} key={`${link.text}-${childLink.text}`}
link={childLink} link={childLink}
activeItem={activeItem} activeItem={activeItem}
onClose={onClose} onClick={onClick}
level={level + 1} level={level + 1}
/> />
)) ))
@ -121,6 +121,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
alignItems: 'center', alignItems: 'center',
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
}), }),
listItem: css({
flex: 1,
}),
isActive: css({ isActive: css({
color: theme.colors.text.primary, color: theme.colors.text.primary,

View File

@ -29,7 +29,7 @@ const setup = () => {
]; ];
const grafanaContext = getGrafanaContextMock(); const grafanaContext = getGrafanaContextMock();
grafanaContext.chrome.onToggleMegaMenu(); grafanaContext.chrome.setMegaMenu('open');
return render( return render(
<TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}> <TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}>

View File

@ -46,10 +46,10 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P
); );
useEffect(() => { useEffect(() => {
if (state.megaMenuOpen) { if (state.megaMenu === 'open') {
setIsOpen(true); setIsOpen(true);
} }
}, [state.megaMenuOpen]); }, [state.megaMenu]);
return ( return (
<OverlayContainer> <OverlayContainer>

View File

@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css';
import React, { useLayoutEffect } from 'react'; import React, { useLayoutEffect } from 'react';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
@ -108,7 +109,7 @@ const getStyles = (theme: GrafanaTheme2) => {
margin: theme.spacing(0, 0, 0, 0), margin: theme.spacing(0, 0, 0, 0),
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
margin: theme.spacing(2, 2, 0, 1), margin: theme.spacing(2, 2, 0, config.featureToggles.dockedMegaMenu ? 2 : 1),
padding: theme.spacing(3), padding: theme.spacing(3),
}, },
}), }),

View File

@ -722,6 +722,11 @@
"kiosk": { "kiosk": {
"tv-alert": "Drücke ESC, um den Kiosk-Modus zu verlassen" "tv-alert": "Drücke ESC, um den Kiosk-Modus zu verlassen"
}, },
"megamenu": {
"close": "",
"dock": "",
"undock": ""
},
"toolbar": { "toolbar": {
"enable-kiosk": "Kiosk-Modus aktivieren", "enable-kiosk": "Kiosk-Modus aktivieren",
"toggle-menu": "Menü umschalten", "toggle-menu": "Menü umschalten",

View File

@ -722,6 +722,11 @@
"kiosk": { "kiosk": {
"tv-alert": "Press ESC to exit kiosk mode" "tv-alert": "Press ESC to exit kiosk mode"
}, },
"megamenu": {
"close": "Close menu",
"dock": "Dock menu",
"undock": "Undock menu"
},
"toolbar": { "toolbar": {
"enable-kiosk": "Enable kiosk mode", "enable-kiosk": "Enable kiosk mode",
"toggle-menu": "Toggle menu", "toggle-menu": "Toggle menu",

View File

@ -728,6 +728,11 @@
"kiosk": { "kiosk": {
"tv-alert": "Pulse ESC para salir del modo de quiosco" "tv-alert": "Pulse ESC para salir del modo de quiosco"
}, },
"megamenu": {
"close": "",
"dock": "",
"undock": ""
},
"toolbar": { "toolbar": {
"enable-kiosk": "Activar el modo de quiosco", "enable-kiosk": "Activar el modo de quiosco",
"toggle-menu": "Activar o desactivar menú", "toggle-menu": "Activar o desactivar menú",

View File

@ -728,6 +728,11 @@
"kiosk": { "kiosk": {
"tv-alert": "Appuyez sur ESC pour quitter le mode kiosque" "tv-alert": "Appuyez sur ESC pour quitter le mode kiosque"
}, },
"megamenu": {
"close": "",
"dock": "",
"undock": ""
},
"toolbar": { "toolbar": {
"enable-kiosk": "Activer le mode kiosque", "enable-kiosk": "Activer le mode kiosque",
"toggle-menu": "Afficher/Masquer le menu", "toggle-menu": "Afficher/Masquer le menu",

View File

@ -722,6 +722,11 @@
"kiosk": { "kiosk": {
"tv-alert": "Přęşş ĒŜC ŧő ęχįŧ ĸįőşĸ mőđę" "tv-alert": "Přęşş ĒŜC ŧő ęχįŧ ĸįőşĸ mőđę"
}, },
"megamenu": {
"close": "Cľőşę męʼnū",
"dock": "Đőčĸ męʼnū",
"undock": "Ůʼnđőčĸ męʼnū"
},
"toolbar": { "toolbar": {
"enable-kiosk": "Ēʼnäþľę ĸįőşĸ mőđę", "enable-kiosk": "Ēʼnäþľę ĸįőşĸ mőđę",
"toggle-menu": "Ŧőģģľę męʼnū", "toggle-menu": "Ŧőģģľę męʼnū",

View File

@ -716,6 +716,11 @@
"kiosk": { "kiosk": {
"tv-alert": "按 ESC 退出 kiosk 模式" "tv-alert": "按 ESC 退出 kiosk 模式"
}, },
"megamenu": {
"close": "",
"dock": "",
"undock": ""
},
"toolbar": { "toolbar": {
"enable-kiosk": "启用 kiosk 模式", "enable-kiosk": "启用 kiosk 模式",
"toggle-menu": "切换菜单", "toggle-menu": "切换菜单",