Dashboards: Redesign tab edit pane and header (#101523)
* add tab item changes * redesign tab edit pane and header actions * Merge branch 'main' into serge/redesign-tab-edit-pane * address comments * adjust i18n typo * sanitise url
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions, renderActions } from './TabItemEditor';
|
||||
import { getEditOptions } from './TabItemEditor';
|
||||
import { TabItemRenderer } from './TabItemRenderer';
|
||||
import { TabItems } from './TabItems';
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
@@ -56,14 +55,6 @@ export class TabItem
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public getParentLayout(): TabsLayoutManager {
|
||||
return sceneGraph.getAncestor(this, TabsLayoutManager);
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
const layout = sceneGraph.getAncestor(this, TabsLayoutManager);
|
||||
layout.removeTab(this);
|
||||
@@ -73,7 +64,43 @@ export class TabItem
|
||||
return new TabItems(items.filter((item) => item instanceof TabItem));
|
||||
}
|
||||
|
||||
public onAddPanel(panel = getDefaultVizPanel()) {
|
||||
this.getLayout().addPanel(panel);
|
||||
}
|
||||
|
||||
public onAddTabBefore() {
|
||||
this._getParentLayout().addTabBefore(this);
|
||||
}
|
||||
|
||||
public onAddTabAfter() {
|
||||
this._getParentLayout().addTabAfter(this);
|
||||
}
|
||||
|
||||
public onMoveLeft() {
|
||||
this._getParentLayout().moveTabLeft(this);
|
||||
}
|
||||
|
||||
public onMoveRight() {
|
||||
this._getParentLayout().moveTabRight(this);
|
||||
}
|
||||
|
||||
public isFirstTab(): boolean {
|
||||
return this._getParentLayout().isFirstTab(this);
|
||||
}
|
||||
|
||||
public isLastTab(): boolean {
|
||||
return this._getParentLayout().isLastTab(this);
|
||||
}
|
||||
|
||||
public onChangeTitle(title: string) {
|
||||
this.setState({ title });
|
||||
}
|
||||
|
||||
public getParentLayout(): TabsLayoutManager {
|
||||
return this._getParentLayout();
|
||||
}
|
||||
|
||||
private _getParentLayout(): TabsLayoutManager {
|
||||
return sceneGraph.getAncestor(this, TabsLayoutManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Button, Input } from '@grafana/ui';
|
||||
import { Input } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
@@ -12,9 +13,12 @@ import { TabItem } from './TabItem';
|
||||
export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] {
|
||||
const tabOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title', 'Tab options'),
|
||||
title: '',
|
||||
id: 'tab-options',
|
||||
isOpenDefault: true,
|
||||
isOpenable: false,
|
||||
renderTitle: () => (
|
||||
<EditPaneHeader title={t('dashboard.tabs-layout.tab-options.title', 'Tab')} onDelete={() => model.onDelete()} />
|
||||
),
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
|
||||
@@ -29,15 +33,6 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
|
||||
return [tabOptions, layoutOptions];
|
||||
}
|
||||
|
||||
export function renderActions(tab: TabItem): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => tab.onDelete()} icon="trash-alt" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TabTitleInput({ tab }: { tab: TabItem }) {
|
||||
const { title } = tab.useState();
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Dropdown, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
|
||||
interface Props {
|
||||
model: TabItem;
|
||||
}
|
||||
|
||||
export function TabItemMenu({ model }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<ToolbarButtonRow className={styles.container}>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-panel', 'Panel')}
|
||||
label={t('dashboard.tabs-layout.tab.menu.add-panel', 'Panel')}
|
||||
onClick={() => model.onAddPanel()}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-tab-before', 'Tab before')}
|
||||
label={t('dashboard.tabs-layout.tab.menu.add-tab-above', 'Tab before')}
|
||||
onClick={() => model.onAddTabBefore()}
|
||||
/>
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-tab-after', 'Tab after')}
|
||||
label={t('dashboard.tabs-layout.tab.menu.add-tab-after', 'Tab after')}
|
||||
onClick={() => model.onAddTabAfter()}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
|
||||
title={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
|
||||
tooltip={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
|
||||
icon="plus"
|
||||
iconSize="xs"
|
||||
variant="default"
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
aria-label={t('dashboard.tabs-layout.tab.menu.move-left', 'Move tab left')}
|
||||
label={t('dashboard.tabs-layout.tab.menu.move-left', 'Move tab left')}
|
||||
onClick={() => model.onMoveLeft()}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
aria-label={t('dashboard.tabs-layout.tab.menu.move-right', 'Move tab right')}
|
||||
label={t('dashboard.tabs-layout.tab.menu.move-right', 'Move tab right')}
|
||||
onClick={() => model.onMoveRight()}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
|
||||
title={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
|
||||
tooltip={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
|
||||
icon="arrows-h"
|
||||
iconSize="md"
|
||||
variant="default"
|
||||
/>
|
||||
</Dropdown>
|
||||
</ToolbarButtonRow>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
gap: theme.spacing(0),
|
||||
flexShrink: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,41 +1,98 @@
|
||||
import { useMemo } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { GrafanaTheme2, locationUtil, textUtil } from '@grafana/data';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Tab, useElementSelection } from '@grafana/ui';
|
||||
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { Checkbox, clearButtonStyles, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { getFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
|
||||
export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
|
||||
const { title, key } = model.useState();
|
||||
const isClone = useMemo(() => isClonedKey(key!), [key]);
|
||||
const parentLayout = model.getParentLayout();
|
||||
const { tabs, currentTabIndex } = parentLayout.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
|
||||
const { isSelected, onSelect } = useElementSelection(key);
|
||||
const myIndex = tabs.findIndex((tab) => tab === model);
|
||||
const isActive = myIndex === currentTabIndex;
|
||||
const location = useLocation();
|
||||
const href = locationUtil.getUrlForPartial(location, { tab: myIndex });
|
||||
const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { tab: myIndex }));
|
||||
const styles = useStyles2(getStyles);
|
||||
const clearStyles = useStyles2(clearButtonStyles);
|
||||
|
||||
return (
|
||||
<Tab
|
||||
className={!isClone && isSelected ? 'dashboard-selected-element' : undefined}
|
||||
label={titleInterpolated}
|
||||
active={isActive}
|
||||
href={href}
|
||||
onPointerDown={(evt) => {
|
||||
if (isEditing && isActive && !isClone) {
|
||||
evt.stopPropagation();
|
||||
onSelect?.(evt);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<div className={cx(styles.container, isSelected && 'dashboard-selected-element')} role="presentation">
|
||||
<span onPointerDown={onSelect}>
|
||||
<Checkbox value={!!isSelected} />
|
||||
</span>
|
||||
|
||||
<a
|
||||
href={href}
|
||||
className={cx(clearStyles, styles.label, isActive ? styles.labelActive : styles.labelNotActive)}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
>
|
||||
{titleInterpolated}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
listStyle: 'none',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
whiteSpace: 'nowrap',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
label: css({
|
||||
color: theme.colors.text.secondary,
|
||||
padding: theme.spacing(1, 1.5, 0.5),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
userSelect: 'none',
|
||||
|
||||
display: 'block',
|
||||
height: '100%',
|
||||
|
||||
svg: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
|
||||
'&:focus-visible': getFocusStyles(theme),
|
||||
|
||||
'&::before': {
|
||||
display: 'block',
|
||||
content: '" "',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '4px',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
bottom: 0,
|
||||
},
|
||||
}),
|
||||
labelNotActive: css({
|
||||
'a:hover, &:hover, &:focus': {
|
||||
color: theme.colors.text.primary,
|
||||
|
||||
'&::before': {
|
||||
backgroundColor: theme.colors.action.hover,
|
||||
},
|
||||
},
|
||||
}),
|
||||
labelActive: css({
|
||||
color: theme.colors.text.primary,
|
||||
overflow: 'hidden',
|
||||
|
||||
'&::before': {
|
||||
backgroundImage: theme.colors.gradients.brandHorizontal,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
@@ -8,7 +7,7 @@ import { EditableDashboardElementInfo } from '../types/EditableDashboardElement'
|
||||
import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { getEditOptions, renderActions } from './TabItemsEditor';
|
||||
import { getEditOptions } from './TabItemsEditor';
|
||||
|
||||
export class TabItems implements MultiSelectedEditableDashboardElement {
|
||||
public readonly isMultiSelectedEditableDashboardElement = true;
|
||||
@@ -26,10 +25,6 @@ export class TabItems implements MultiSelectedEditableDashboardElement {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public getTabs(): TabItem[] {
|
||||
return this._tabs;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Button, Stack, Text } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
|
||||
|
||||
import { TabItems } from './TabItems';
|
||||
|
||||
export function getEditOptions(_model: TabItems): OptionsPaneCategoryDescriptor[] {
|
||||
export function getEditOptions(model: TabItems): OptionsPaneCategoryDescriptor[] {
|
||||
const tabOptions = useMemo(() => {
|
||||
const tabs = model.getTabs();
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.edit-pane.tab.multi-select.options-header', 'Multi-selected Tab options'),
|
||||
title: ``,
|
||||
id: 'ms-tab-options',
|
||||
isOpenDefault: true,
|
||||
isOpenable: false,
|
||||
renderTitle: () => (
|
||||
<EditPaneHeader
|
||||
title={t('dashboard.tabs-layout.multi-select.title', '{{length}} tabs selected', { length: tabs.length })}
|
||||
onDelete={() => model.onDelete()}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
}, [model]);
|
||||
|
||||
return [tabOptions];
|
||||
}
|
||||
|
||||
export function renderActions(model: TabItems) {
|
||||
const tabs = model.getTabs();
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Text>
|
||||
<Trans i18nKey="dashboard.edit-pane.tab.multi-select.selection-number">No. of tabs selected: </Trans>
|
||||
{tabs.length}
|
||||
</Text>
|
||||
<Stack direction="row">
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => model.onDelete()} icon="trash-alt" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,50 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
this.setState({ tabs, currentTabIndex: 0 });
|
||||
}
|
||||
|
||||
public addTabBefore(tab: TabItem) {
|
||||
const newTab = new TabItem();
|
||||
const tabs = this.state.tabs.slice();
|
||||
tabs.splice(tabs.indexOf(tab), 0, newTab);
|
||||
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex });
|
||||
}
|
||||
|
||||
public addTabAfter(tab: TabItem) {
|
||||
const newTab = new TabItem();
|
||||
const tabs = this.state.tabs.slice();
|
||||
tabs.splice(tabs.indexOf(tab) + 1, 0, newTab);
|
||||
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex + 1 });
|
||||
}
|
||||
|
||||
public moveTabLeft(tab: TabItem) {
|
||||
const currentIndex = this.state.tabs.indexOf(tab);
|
||||
if (currentIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
const tabs = this.state.tabs.slice();
|
||||
tabs.splice(currentIndex, 1);
|
||||
tabs.splice(currentIndex - 1, 0, tab);
|
||||
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex - 1 });
|
||||
}
|
||||
|
||||
public moveTabRight(tab: TabItem) {
|
||||
const currentIndex = this.state.tabs.indexOf(tab);
|
||||
if (currentIndex >= this.state.tabs.length - 1) {
|
||||
return;
|
||||
}
|
||||
const tabs = this.state.tabs.slice();
|
||||
tabs.splice(currentIndex, 1);
|
||||
tabs.splice(currentIndex + 1, 0, tab);
|
||||
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex + 1 });
|
||||
}
|
||||
|
||||
public isFirstTab(tab: TabItem): boolean {
|
||||
return this.state.tabs[0] === tab;
|
||||
}
|
||||
|
||||
public isLastTab(tab: TabItem): boolean {
|
||||
return this.state.tabs[this.state.tabs.length - 1] === tab;
|
||||
}
|
||||
|
||||
public static createEmpty(): TabsLayoutManager {
|
||||
const tab = new TabItem();
|
||||
return new TabsLayoutManager({ tabs: [tab] });
|
||||
|
||||
+33
-8
@@ -1,9 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { Divider, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
|
||||
import { TabItemMenu } from './TabItemMenu';
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
||||
export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLayoutManager>) {
|
||||
@@ -11,13 +15,23 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
const { tabs, currentTabIndex } = model.useState();
|
||||
const currentTab = tabs[currentTabIndex];
|
||||
const { layout } = currentTab.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabsBar className={styles.tabsContainer}>
|
||||
{tabs.map((tab) => (
|
||||
<tab.Component model={tab} key={tab.state.key!} />
|
||||
))}
|
||||
<TabsBar className={styles.tabsWrapper}>
|
||||
<div className={styles.tabsRow}>
|
||||
<div className={styles.tabsContainer}>
|
||||
{tabs.map((tab, idx) => (
|
||||
<Fragment key={tab.state.key!}>
|
||||
{isEditing && idx > 0 && <Divider direction="vertical" />}
|
||||
<tab.Component model={tab} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{isEditing && <TabItemMenu model={currentTab} />}
|
||||
</div>
|
||||
</TabsBar>
|
||||
<TabContent className={styles.tabContentContainer}>
|
||||
{currentTab && <layout.Component model={layout} />}
|
||||
@@ -27,10 +41,21 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
tabsWrapper: css({
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
tabsRow: css({
|
||||
justifyContent: 'space-between',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
}),
|
||||
tabsContainer: css({
|
||||
flexShrink: 1,
|
||||
padding: '2px 2px 0 2px',
|
||||
marginBottom: theme.spacing(1),
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
overflowX: 'scroll',
|
||||
overflowY: 'visible',
|
||||
paddingInline: theme.spacing(0.125),
|
||||
}),
|
||||
tabContentContainer: css({
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
@@ -1087,12 +1087,6 @@
|
||||
"options-header": "Multi-selected Row options",
|
||||
"selection-number": "No. of rows selected: {{length}}"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"multi-select": {
|
||||
"options-header": "Multi-selected Tab options",
|
||||
"selection-number": "No. of tabs selected: "
|
||||
}
|
||||
}
|
||||
},
|
||||
"editpane": {
|
||||
@@ -1250,12 +1244,27 @@
|
||||
},
|
||||
"tabs-layout": {
|
||||
"description": "Tabs layout",
|
||||
"menu": {
|
||||
"move-tab": "Move tab"
|
||||
},
|
||||
"multi-select": {
|
||||
"title": "{{length}} tabs selected"
|
||||
},
|
||||
"name": "Tabs",
|
||||
"tab": {
|
||||
"menu": {
|
||||
"add": "Add tab",
|
||||
"add-panel": "Panel",
|
||||
"add-tab-above": "Tab before",
|
||||
"add-tab-after": "Tab after",
|
||||
"add-tab-before": "Tab before",
|
||||
"move-left": "Move tab left",
|
||||
"move-right": "Move tab right"
|
||||
},
|
||||
"new": "New tab"
|
||||
},
|
||||
"tab-options": {
|
||||
"title": "Tab options",
|
||||
"title": "Tab",
|
||||
"title-option": "Title"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1087,12 +1087,6 @@
|
||||
"options-header": "Mūľŧį-şęľęčŧęđ Ŗőŵ őpŧįőʼnş",
|
||||
"selection-number": "Ńő. őƒ řőŵş şęľęčŧęđ: {{length}}"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"multi-select": {
|
||||
"options-header": "Mūľŧį-şęľęčŧęđ Ŧäþ őpŧįőʼnş",
|
||||
"selection-number": "Ńő. őƒ ŧäþş şęľęčŧęđ: "
|
||||
}
|
||||
}
|
||||
},
|
||||
"editpane": {
|
||||
@@ -1250,12 +1244,27 @@
|
||||
},
|
||||
"tabs-layout": {
|
||||
"description": "Ŧäþş ľäyőūŧ",
|
||||
"menu": {
|
||||
"move-tab": "Mővę ŧäþ"
|
||||
},
|
||||
"multi-select": {
|
||||
"title": "{{length}} ŧäþş şęľęčŧęđ"
|
||||
},
|
||||
"name": "Ŧäþş",
|
||||
"tab": {
|
||||
"menu": {
|
||||
"add": "Åđđ ŧäþ",
|
||||
"add-panel": "Päʼnęľ",
|
||||
"add-tab-above": "Ŧäþ þęƒőřę",
|
||||
"add-tab-after": "Ŧäþ äƒŧęř",
|
||||
"add-tab-before": "Ŧäþ þęƒőřę",
|
||||
"move-left": "Mővę ŧäþ ľęƒŧ",
|
||||
"move-right": "Mővę ŧäþ řįģĥŧ"
|
||||
},
|
||||
"new": "Ńęŵ ŧäþ"
|
||||
},
|
||||
"tab-options": {
|
||||
"title": "Ŧäþ őpŧįőʼnş",
|
||||
"title": "Ŧäþ",
|
||||
"title-option": "Ŧįŧľę"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user