diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index ae3085bc055..e324b09075c 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -25,7 +25,7 @@ export class DashboardEditableElement implements EditableDashboardElement { const { body } = dashboard.useState(); const dashboardOptions = useMemo(() => { - return new OptionsPaneCategoryDescriptor({ + const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: t('dashboard.options.title', 'Dashboard options'), id: 'dashboard-options', isOpenable: false, @@ -48,6 +48,14 @@ export class DashboardEditableElement implements EditableDashboardElement { render: () => , }) ); + + if (body.getOptions) { + for (const option of body.getOptions()) { + editPaneHeaderOptions.addItem(option); + } + } + + return editPaneHeaderOptions; }, [body, dashboard]); return [dashboardOptions]; diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx index 3e6a7fa98ae..a14b10f485b 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx @@ -29,7 +29,7 @@ export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEdita isOpenable: false, renderTitle: () => ( this.onDelete()} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index e92ed0f5466..71099c6fa02 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -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 './RowItemEditor'; +import { getEditOptions } from './RowItemEditor'; import { RowItemRenderer } from './RowItemRenderer'; import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; import { RowItems } from './RowItems'; @@ -60,19 +59,42 @@ export class RowItem return getEditOptions(this); } - public renderActions(): ReactNode { - return renderActions(this); - } - public onDelete() { - const layout = sceneGraph.getAncestor(this, RowsLayoutManager); - layout.removeRow(this); + this._getParentLayout().removeRow(this); } public createMultiSelectedElement(items: SceneObject[]): RowItems { return new RowItems(items.filter((item) => item instanceof RowItem)); } + public onAddPanel(panel = getDefaultVizPanel()) { + this.getLayout().addPanel(panel); + } + + public onAddRowAbove() { + this._getParentLayout().addRowAbove(this); + } + + public onAddRowBelow() { + this._getParentLayout().addRowBelow(this); + } + + public onMoveUp() { + this._getParentLayout().moveRowUp(this); + } + + public onMoveDown() { + this._getParentLayout().moveRowDown(this); + } + + public isFirstRow(): boolean { + return this._getParentLayout().isFirstRow(this); + } + + public isLastRow(): boolean { + return this._getParentLayout().isLastRow(this); + } + public getRepeatVariable(): string | undefined { return this._getRepeatBehavior()?.state.variableName; } @@ -110,6 +132,10 @@ export class RowItem this.setState({ isCollapsed: !this.state.isCollapsed }); } + private _getParentLayout(): RowsLayoutManager { + return sceneGraph.getAncestor(this, RowsLayoutManager); + } + private _getRepeatBehavior(): RowItemRepeaterBehavior | undefined { return this.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior); } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index e45f9fa99ba..3ff3181129d 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -1,8 +1,7 @@ import { useMemo } from 'react'; -import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Alert, Button, Input, RadioButtonGroup, Switch, TextLink } from '@grafana/ui'; +import { Alert, Input, Switch, TextLink } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -10,18 +9,25 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { EditPaneHeader } from '../../edit-pane/EditPaneHeader'; import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; -import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; +import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector'; import { RowItem } from './RowItem'; export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] { + const { layout } = model.useState(); const rowOptions = useMemo(() => { - return new OptionsPaneCategoryDescriptor({ - title: t('dashboard.rows-layout.row-options.title', 'Row options'), + const dashboard = getDashboardSceneFor(model); + + const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ + title: t('dashboard.rows-layout.row-options.title', 'Row'), id: 'row-options', - isOpenDefault: true, + isOpenable: false, + renderTitle: () => ( + model.onDelete()} /> + ), }) .addItem( new OptionsPaneItemDescriptor({ @@ -31,8 +37,22 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] ) .addItem( new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.row-options.height.title', 'Height'), - render: () => , + title: t('dashboard.layout.common.layout', 'Layout'), + render: () => , + }) + ); + + if (layout.getOptions) { + for (const option of layout.getOptions()) { + editPaneHeaderOptions.addItem(option); + } + } + + editPaneHeaderOptions + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat for'), + render: () => , }) ) .addItem( @@ -41,42 +61,23 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] render: () => , }) ); - }, [model]); - const rowRepeatOptions = useMemo(() => { - const dashboard = getDashboardSceneFor(model); + return editPaneHeaderOptions; + }, [layout, model]); - return new OptionsPaneCategoryDescriptor({ - title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'), - id: 'row-repeat-options', - isOpenDefault: true, - }).addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Variable'), - render: () => , - }) - ); - }, [model]); - - const { layout } = model.useState(); - const layoutOptions = useLayoutCategory(layout); - - return [rowOptions, rowRepeatOptions, layoutOptions]; -} - -export function renderActions(model: RowItem) { - return ( - <> - - model.onDelete()} icon="trash-alt" /> - > - ); + return [rowOptions]; } function RowTitleInput({ row }: { row: RowItem }) { const { title } = row.useState(); - return row.onChangeTitle(e.currentTarget.value)} />; + return ( + row.onChangeTitle(e.currentTarget.value)} + /> + ); } function RowHeaderSwitch({ row }: { row: RowItem }) { @@ -85,17 +86,6 @@ function RowHeaderSwitch({ row }: { row: RowItem }) { return row.onHeaderHiddenToggle()} />; } -function RowHeightSelect({ row }: { row: RowItem }) { - const { height = 'expand' } = row.useState(); - - const options: Array> = [ - { label: t('dashboard.rows-layout.row-options.height.expand', 'Expand'), value: 'expand' }, - { label: t('dashboard.rows-layout.row-options.height.min', 'Min'), value: 'min' }, - ]; - - return row.onChangeHeight(option)} />; -} - function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) { const { layout } = row.useState(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx new file mode 100644 index 00000000000..496b5130515 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx @@ -0,0 +1,93 @@ +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 { RowItem } from './RowItem'; + +interface RowItemMenuProps { + model: RowItem; +} + +export function RowItemMenu({ model }: RowItemMenuProps) { + const styles = useStyles2(getStyles); + + return ( + + ( + + model.onAddPanel()} + /> + + model.onAddRowAbove()} + /> + model.onAddRowBelow()} + /> + + )} + > + + Add + + + ( + + model.onMoveUp()} + disabled={model.isFirstRow()} + /> + + model.onMoveDown()} + disabled={model.isLastRow()} + /> + + )} + > + + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + display: 'flex', + alignItems: 'center', + flex: 1, + justifyContent: 'flex-end', + gap: theme.spacing(1), + }), +}); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index 67b90fa4e91..97c78ae938b 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -4,13 +4,14 @@ import { useMemo, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; -import { Button, Icon, useElementSelection, useStyles2 } from '@grafana/ui'; +import { Checkbox, clearButtonStyles, Icon, useElementSelection, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { isClonedKey } from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; import { RowItem } from './RowItem'; +import { RowItemMenu } from './RowItemMenu'; export function RowItemRenderer({ model }: SceneComponentProps) { const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState(); @@ -18,6 +19,7 @@ export function RowItemRenderer({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); const { isEditing, showHiddenElements } = dashboard.useState(); const styles = useStyles2(getStyles); + const clearStyles = useStyles2(clearButtonStyles); const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); const ref = useRef(null); const shouldGrow = !isCollapsed && height === 'expand'; @@ -35,9 +37,14 @@ export function RowItemRenderer({ model }: SceneComponentProps) { > {(!isHeaderHidden || (isEditing && showHiddenElements)) && ( + {!isClone && isEditing && ( + + + + )} model.onCollapseToggle()} - className={styles.rowTitleButton} + className={cx(clearStyles, styles.rowTitleButton)} aria-label={ isCollapsed ? t('dashboard.rows-layout.row.expand', 'Expand row') @@ -50,9 +57,7 @@ export function RowItemRenderer({ model }: SceneComponentProps) { {titleInterpolated} - {!isClone && isEditing && ( - onSelect?.(evt)} /> - )} + {!isClone && isEditing && } )} {!isCollapsed && } @@ -69,17 +74,6 @@ function getStyles(theme: GrafanaTheme2) { padding: theme.spacing(0, 0, 0.5, 0), margin: theme.spacing(0, 0, 1, 0), alignItems: 'center', - - '&:hover, &:focus-within': { - '& > div': { - opacity: 1, - }, - }, - - '& > div': { - marginBottom: 0, - marginRight: theme.spacing(1), - }, }), rowTitleButton: css({ display: 'flex', @@ -118,5 +112,9 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', opacity: 0, }), + checkboxWrapper: css({ + display: 'flex', + alignItems: 'center', + }), }; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx index 597eaf495d8..727504fde08 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx @@ -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 { RowItem } from './RowItem'; -import { getEditOptions, renderActions } from './RowItemsEditor'; +import { getEditOptions } from './RowItemsEditor'; export class RowItems implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; @@ -26,10 +25,6 @@ export class RowItems implements MultiSelectedEditableDashboardElement { return getEditOptions(this); } - public renderActions(): ReactNode { - return renderActions(this); - } - public getRows(): RowItem[] { return this._rows; } @@ -41,4 +36,6 @@ export class RowItems implements MultiSelectedEditableDashboardElement { public onHeaderHiddenToggle(value: boolean, indeterminate: boolean) { this._rows.forEach((row) => row.onHeaderHiddenToggle(indeterminate ? true : !value)); } + + public getNumberOfRowsSelected = () => this._rows.length; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx index bd4837f3d59..9ce5700109d 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx @@ -1,15 +1,25 @@ -import { Button, Checkbox, Stack, Text } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; +import { Checkbox } 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 { RowItems } from './RowItems'; export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] { const options = new OptionsPaneCategoryDescriptor({ - title: t('dashboard.edit-pane.row.multi-select.options-header', 'Multi-selected Row options'), + title: '', id: `ms-row-options-${model.key}`, - isOpenDefault: true, + isOpenable: false, + renderTitle: () => ( + model.onDelete()} + /> + ), }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.row.header.title', 'Row header'), @@ -20,24 +30,6 @@ export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] return [options]; } -export function renderActions(model: RowItems) { - const rows = model.getRows(); - - return ( - - - - No. of rows selected: {{ length }} - - - - - model.onDelete()} icon="trash-alt" /> - - - ); -} - function RowHeaderCheckboxMulti({ model }: { model: RowItems }) { const rows = model.getRows(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 89a75f29769..a4959f810ec 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -41,17 +41,17 @@ export class RowsLayoutManager extends SceneObjectBase i // Try to add new panels to the selected row const selectedRows = dashboardSceneGraph.getAllSelectedObjects(this).filter((obj) => obj instanceof RowItem); if (selectedRows.length > 0) { - return selectedRows.forEach((row) => row.getLayout().addPanel(vizPanel)); + return selectedRows.forEach((row) => row.onAddPanel(vizPanel)); } // If we don't have selected row add it to the first row if (this.state.rows.length > 0) { - return this.state.rows[0].getLayout().addPanel(vizPanel); + return this.state.rows[0].onAddPanel(vizPanel); } // Otherwise fallback to adding a new row and a panel this.addNewRow(); - this.state.rows[this.state.rows.length - 1].getLayout().addPanel(vizPanel); + this.state.rows[this.state.rows.length - 1].onAddPanel(vizPanel); } public getVizPanels(): VizPanel[] { @@ -103,11 +103,81 @@ export class RowsLayoutManager extends SceneObjectBase i }); } + public addRowAbove(row: RowItem) { + const rows = this.state.rows; + const index = rows.indexOf(row); + rows.splice(index, 0, new RowItem()); + this.setState({ rows }); + } + + public addRowBelow(row: RowItem) { + const rows = this.state.rows; + let index = rows.indexOf(row); + + // Be sure we don't add a row between an original row and one of its clones + while (rows[index + 1] && isClonedKey(rows[index + 1].state.key!)) { + index = index + 1; + } + + rows.splice(index + 1, 0, new RowItem()); + this.setState({ rows }); + } + public removeRow(row: RowItem) { const rows = this.state.rows.filter((r) => r !== row); this.setState({ rows: rows.length === 0 ? [new RowItem()] : rows }); } + public moveRowUp(row: RowItem) { + const rows = [...this.state.rows]; + const originalIndex = rows.indexOf(row); + + if (originalIndex === 0) { + return; + } + + let moveToIndex = originalIndex - 1; + + // Be sure we don't add a row between an original row and one of its clones + while (rows[moveToIndex] && isClonedKey(rows[moveToIndex].state.key!)) { + moveToIndex = moveToIndex - 1; + } + + rows.splice(originalIndex, 1); + rows.splice(moveToIndex, 0, row); + this.setState({ rows }); + } + + public moveRowDown(row: RowItem) { + const rows = [...this.state.rows]; + const originalIndex = rows.indexOf(row); + + if (originalIndex === rows.length - 1) { + return; + } + + let moveToIndex = originalIndex + 1; + + // Be sure we don't add a row between an original row and one of its clones + while (rows[moveToIndex] && isClonedKey(rows[moveToIndex].state.key!)) { + moveToIndex = moveToIndex + 1; + } + + rows.splice(moveToIndex + 1, 0, row); + rows.splice(originalIndex, 1); + + this.setState({ rows }); + } + + public isFirstRow(row: RowItem): boolean { + return this.state.rows[0] === row; + } + + public isLastRow(row: RowItem): boolean { + const filteredRow = this.state.rows.filter((r) => !isClonedKey(r.state.key!)); + return filteredRow[filteredRow.length - 1] === row; + } + public static createEmpty(): RowsLayoutManager { return new RowsLayoutManager({ rows: [new RowItem()] }); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 14d83ff4b77..dd2c244c452 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1084,8 +1084,7 @@ "title": "Row header" }, "multi-select": { - "options-header": "Multi-selected Row options", - "selection-number": "No. of rows selected: {{length}}" + "title": "{{length}} rows selected" } } }, @@ -1167,7 +1166,7 @@ "delete": "Delete", "duplicate": "Duplicate", "layout": "Layout", - "panels-title": "{{length}} Panels Selected" + "panels-title": "{{length}} panels selected" } }, "options": { @@ -1219,6 +1218,15 @@ "row": { "collapse": "Collapse row", "expand": "Expand row", + "menu": { + "add": "Add row", + "add-panel": "Panel", + "add-row-above": "Row above", + "add-row-below": "Row below", + "move-down": "Move row down", + "move-row": "Move row", + "move-up": "Move row up" + }, "new": "New row", "repeat": { "learn-more": "Learn more", @@ -1227,18 +1235,14 @@ }, "row-options": { "height": { - "expand": "Expand", - "hide-row-header": "Hide row header", - "min": "Min", - "title": "Height" + "hide-row-header": "Hide row header" }, "repeat": { - "title": "Repeat options", "variable": { - "title": "Variable" + "title": "Repeat for" } }, - "title": "Row options", + "title": "Row", "title-option": "Title" } }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 4e3adb37edd..e92128c86f6 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1084,8 +1084,7 @@ "title": "Ŗőŵ ĥęäđęř" }, "multi-select": { - "options-header": "Mūľŧį-şęľęčŧęđ Ŗőŵ őpŧįőʼnş", - "selection-number": "Ńő. őƒ řőŵş şęľęčŧęđ: {{length}}" + "title": "{{length}} řőŵş şęľęčŧęđ" } } }, @@ -1167,7 +1166,7 @@ "delete": "Đęľęŧę", "duplicate": "Đūpľįčäŧę", "layout": "Ŀäyőūŧ", - "panels-title": "{{length}} Päʼnęľş Ŝęľęčŧęđ" + "panels-title": "{{length}} päʼnęľş şęľęčŧęđ" } }, "options": { @@ -1219,6 +1218,15 @@ "row": { "collapse": "Cőľľäpşę řőŵ", "expand": "Ēχpäʼnđ řőŵ", + "menu": { + "add": "Åđđ řőŵ", + "add-panel": "Päʼnęľ", + "add-row-above": "Ŗőŵ äþővę", + "add-row-below": "Ŗőŵ þęľőŵ", + "move-down": "Mővę řőŵ đőŵʼn", + "move-row": "Mővę řőŵ", + "move-up": "Mővę řőŵ ūp" + }, "new": "Ńęŵ řőŵ", "repeat": { "learn-more": "Ŀęäřʼn mőřę", @@ -1227,18 +1235,14 @@ }, "row-options": { "height": { - "expand": "Ēχpäʼnđ", - "hide-row-header": "Ħįđę řőŵ ĥęäđęř", - "min": "Mįʼn", - "title": "Ħęįģĥŧ" + "hide-row-header": "Ħįđę řőŵ ĥęäđęř" }, "repeat": { - "title": "Ŗępęäŧ őpŧįőʼnş", "variable": { - "title": "Väřįäþľę" + "title": "Ŗępęäŧ ƒőř" } }, - "title": "Ŗőŵ őpŧįőʼnş", + "title": "Ŗőŵ", "title-option": "Ŧįŧľę" } },