Variables: Adds drag and drop in variables list (#42503)

* Variables: adds drag and drop in variables list

* Refactor: fixes after PR comments

* Chore: updates after PR comments

* Refactor: adds styles during dragging

* Docs: update doc

* Chore: pushing Drone
This commit is contained in:
Hugo Häggmark 2021-12-06 10:36:58 +01:00 committed by GitHub
parent fdc7eef0f4
commit 769c82cf66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 442 additions and 255 deletions

View File

@ -11,7 +11,7 @@ The variables page lets you [add]({{< relref "variable-types/_index.md" >}}) var
## Move
You can move a variable up or down the list using the up and down arrows respectively.
You can move a variable up or down the list using drag and drop.
## Clone

View File

@ -3,7 +3,7 @@ import { StoreState, ThunkResult } from 'app/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { changeVariableEditorExtended } from '../editor/reducer';
import { addVariable, changeVariableProp } from '../state/sharedReducer';
import { getNewVariabelIndex, getVariable } from '../state/selectors';
import { getNewVariableIndex, getVariable } from '../state/selectors';
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import {
AdHocVariabelFilterUpdate,
@ -144,7 +144,7 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
};
const global = false;
const index = getNewVariabelIndex(getState());
const index = getNewVariableIndex(getState());
const identifier: VariableIdentifier = { type: 'adhoc', id: model.id };
dispatch(

View File

@ -27,7 +27,6 @@ describe('AdHocFilter', () => {
// Select value
userEvent.click(screen.getByText('select value'));
screen.debug(screen.getAllByTestId('AdHocFilterValue-value-wrapper'));
// There are already some filters rendered
const selectEl2 = screen.getAllByTestId('AdHocFilterValue-value-wrapper')[2];
await selectEvent.select(selectEl2, 'val3', { container: document.body });

View File

@ -50,8 +50,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
this.props.switchToEditMode(identifier);
};
onNewVariable = (event: MouseEvent) => {
event.preventDefault();
onNewVariable = () => {
this.props.switchToNewMode();
};
@ -104,20 +103,19 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
</div>
{!variableToEdit && (
<>
<VariableEditorList
dashboard={this.props.dashboard}
variables={this.props.variables}
onAddClick={this.onNewVariable}
onEditClick={this.onEditVariable}
onChangeVariableOrder={this.onChangeVariableOrder}
onDuplicateVariable={this.onDuplicateVariable}
onRemoveVariable={this.onRemoveVariable}
onAdd={this.onNewVariable}
onEdit={this.onEditVariable}
onChangeOrder={this.onChangeVariableOrder}
onDuplicate={this.onDuplicateVariable}
onDelete={this.onRemoveVariable}
usages={this.props.usages}
usagesNetwork={this.props.usagesNetwork}
/>
)}
{!variableToEdit && this.props.variables.length > 0 && (
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
</>
)}
{variableToEdit && <VariableEditorEditor identifier={toVariableIdentifier(variableToEdit)} />}
</div>

View File

@ -1,60 +1,91 @@
import React, { FC, MouseEvent, PureComponent } from 'react';
import { css } from '@emotion/css';
import { Icon, IconButton, useStyles } from '@grafana/ui';
import React, { ReactElement } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { selectors } from '@grafana/e2e-selectors';
import { GrafanaTheme } from '@grafana/data';
import EmptyListCTA from '../../../core/components/EmptyListCTA/EmptyListCTA';
import { QueryVariableModel, VariableModel } from '../types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { DashboardModel } from '../../dashboard/state';
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
import { isAdHoc } from '../guard';
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
import { VariableModel } from '../types';
import { VariableIdentifier } from '../state/types';
import { UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
import { VariableEditorListRow } from './VariableEditorListRow';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
export interface Props {
variables: VariableModel[];
dashboard: DashboardModel | null;
usages: VariableUsageTree[];
usagesNetwork: UsagesToNetwork[];
onAddClick: (event: MouseEvent) => void;
onEditClick: (identifier: VariableIdentifier) => void;
onChangeVariableOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
onDuplicateVariable: (identifier: VariableIdentifier) => void;
onRemoveVariable: (identifier: VariableIdentifier) => void;
onAdd: () => void;
onEdit: (identifier: VariableIdentifier) => void;
onChangeOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
onDuplicate: (identifier: VariableIdentifier) => void;
onDelete: (identifier: VariableIdentifier) => void;
}
enum MoveType {
down = 1,
up = -1,
export function VariableEditorList({
variables,
usages,
usagesNetwork,
onChangeOrder,
onAdd,
onEdit,
onDelete,
onDuplicate,
}: Props): ReactElement {
const onDragEnd = (result: DropResult) => {
if (!result.destination || !result.source) {
return;
}
export class VariableEditorList extends PureComponent<Props> {
onEditClick = (event: MouseEvent, identifier: VariableIdentifier) => {
event.preventDefault();
this.props.onEditClick(identifier);
const identifier = JSON.parse(result.draggableId);
onChangeOrder(identifier, result.source.index, result.destination.index);
};
onChangeVariableOrder = (event: MouseEvent, variable: VariableModel, moveType: MoveType) => {
event.preventDefault();
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index, variable.index + moveType);
};
onDuplicateVariable = (event: MouseEvent, identifier: VariableIdentifier) => {
event.preventDefault();
this.props.onDuplicateVariable(identifier);
};
onRemoveVariable = (event: MouseEvent, identifier: VariableIdentifier) => {
event.preventDefault();
this.props.onRemoveVariable(identifier);
};
render() {
return (
<div>
<div>
{this.props.variables.length === 0 && (
{variables.length === 0 && <EmptyVariablesList onAdd={onAdd} />}
{variables.length > 0 && (
<div>
<table
className="filter-table filter-table--hover"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
>
<thead>
<tr>
<th>Variable</th>
<th>Definition</th>
<th colSpan={5} />
</tr>
</thead>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="variables-list" direction="vertical">
{(provided) => (
<tbody ref={provided.innerRef} {...provided.droppableProps}>
{variables.map((variable, index) => (
<VariableEditorListRow
index={index}
key={`${variable.name}-${index}`}
variable={variable}
usageTree={usages}
usagesNetwork={usagesNetwork}
onDelete={onDelete}
onDuplicate={onDuplicate}
onEdit={onEdit}
/>
))}
{provided.placeholder}
</tbody>
)}
</Droppable>
</DragDropContext>
</table>
</div>
)}
</div>
</div>
);
}
function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement {
return (
<div>
<EmptyListCTA
title="There are no variables yet"
@ -73,160 +104,11 @@ export class VariableEditorList extends PureComponent<Props> {
</p>`,
}}
infoBoxTitle="What do variables do?"
onClick={this.props.onAddClick}
onClick={(event) => {
event.preventDefault();
onAdd();
}}
/>
</div>
)}
{this.props.variables.length > 0 && (
<div>
<table
className="filter-table filter-table--hover"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
>
<thead>
<tr>
<th>Variable</th>
<th>Definition</th>
<th colSpan={6} />
</tr>
</thead>
<tbody>
{this.props.variables.map((state, index) => {
const variable = state as QueryVariableModel;
const definition = variable.definition
? variable.definition
: typeof variable.query === 'string'
? variable.query
: '';
const usages = getVariableUsages(variable.id, this.props.usages);
const passed = usages > 0 || isAdHoc(variable);
return (
<tr key={`${variable.name}-${index}`}>
<td style={{ width: '1%' }}>
<span
onClick={(event) => this.onEditClick(event, toVariableIdentifier(variable))}
className="pointer template-variable"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowNameFields(
variable.name
)}
>
{variable.name}
</span>
</td>
<td
style={{ maxWidth: '200px' }}
onClick={(event) => this.onEditClick(event, toVariableIdentifier(variable))}
className="pointer max-width"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(
variable.name
)}
>
{definition}
</td>
<td style={{ width: '1%' }}>
<VariableCheckIndicator passed={passed} />
</td>
<td style={{ width: '1%' }}>
<VariableUsagesButton
id={variable.id}
isAdhoc={isAdHoc(variable)}
usages={this.props.usagesNetwork}
/>
</td>
<td style={{ width: '1%' }}>
{index > 0 && (
<IconButton
onClick={(event) => this.onChangeVariableOrder(event, variable, MoveType.up)}
name="arrow-up"
title="Move variable up"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(
variable.name
)}
/>
)}
</td>
<td style={{ width: '1%' }}>
{index < this.props.variables.length - 1 && (
<IconButton
onClick={(event) => this.onChangeVariableOrder(event, variable, MoveType.down)}
name="arrow-down"
title="Move variable down"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(
variable.name
)}
/>
)}
</td>
<td style={{ width: '1%' }}>
<IconButton
onClick={(event) => this.onDuplicateVariable(event, toVariableIdentifier(variable))}
name="copy"
title="Duplicate variable"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(
variable.name
)}
/>
</td>
<td style={{ width: '1%' }}>
<IconButton
onClick={(event) => this.onRemoveVariable(event, toVariableIdentifier(variable))}
name="trash-alt"
title="Remove variable"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(
variable.name
)}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
}
interface VariableCheckIndicatorProps {
passed: boolean;
}
const VariableCheckIndicator: FC<VariableCheckIndicatorProps> = ({ passed }) => {
const style = useStyles(getStyles);
if (passed) {
return (
<Icon
name="check"
className={style.iconPassed}
title="This variable is referenced by other variables or dashboard."
/>
);
}
return (
<Icon
name="exclamation-triangle"
className={style.iconFailed}
title="This variable is not referenced by any variable or dashboard."
/>
);
};
const getStyles = (theme: GrafanaTheme) => ({
iconPassed: css`
color: ${theme.palette.greenBase};
`,
iconFailed: css`
color: ${theme.palette.orange};
`,
});

View File

@ -0,0 +1,185 @@
import React, { ReactElement } from 'react';
import { css } from '@emotion/css';
import { Draggable } from 'react-beautiful-dnd';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
import { hasOptions, isAdHoc, isQuery } from '../guard';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
import { VariableModel } from '../types';
export interface VariableEditorListRowProps {
index: number;
variable: VariableModel;
usageTree: VariableUsageTree[];
usagesNetwork: UsagesToNetwork[];
onEdit: (identifier: VariableIdentifier) => void;
onDuplicate: (identifier: VariableIdentifier) => void;
onDelete: (identifier: VariableIdentifier) => void;
}
export function VariableEditorListRow({
index,
variable,
usageTree,
usagesNetwork,
onEdit: propsOnEdit,
onDuplicate: propsOnDuplicate,
onDelete: propsOnDelete,
}: VariableEditorListRowProps): ReactElement {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const definition = getDefinition(variable);
const usages = getVariableUsages(variable.id, usageTree);
const passed = usages > 0 || isAdHoc(variable);
const identifier = toVariableIdentifier(variable);
return (
<Draggable draggableId={JSON.stringify(identifier)} index={index}>
{(provided, snapshot) => (
<tr
ref={provided.innerRef}
{...provided.draggableProps}
style={{
userSelect: snapshot.isDragging ? 'none' : 'auto',
background: snapshot.isDragging ? theme.colors.background.secondary : undefined,
...provided.draggableProps.style,
}}
>
<td className={styles.column}>
<span
onClick={(event) => {
event.preventDefault();
propsOnEdit(identifier);
}}
className={styles.nameLink}
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowNameFields(variable.name)}
>
{variable.name}
</span>
</td>
<td
className={styles.definitionColumn}
onClick={(event) => {
event.preventDefault();
propsOnEdit(identifier);
}}
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(variable.name)}
>
{definition}
</td>
<td className={styles.column}>
<VariableCheckIndicator passed={passed} />
</td>
<td className={styles.column}>
<VariableUsagesButton id={variable.id} isAdhoc={isAdHoc(variable)} usages={usagesNetwork} />
</td>
<td className={styles.column}>
<IconButton
onClick={(event) => {
event.preventDefault();
propsOnDuplicate(identifier);
}}
name="copy"
title="Duplicate variable"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(variable.name)}
/>
</td>
<td className={styles.column}>
<IconButton
onClick={(event) => {
event.preventDefault();
propsOnDelete(identifier);
}}
name="trash-alt"
title="Remove variable"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(variable.name)}
/>
</td>
<td className={styles.column}>
<div {...provided.dragHandleProps} className={styles.dragHandle}>
<Icon name="draggabledots" size="lg" />
</div>
</td>
</tr>
)}
</Draggable>
);
}
function getDefinition(model: VariableModel): string {
let definition = '';
if (isQuery(model)) {
if (model.definition) {
definition = model.definition;
} else if (typeof model.query === 'string') {
definition = model.query;
}
} else if (hasOptions(model)) {
definition = model.query;
}
return definition;
}
interface VariableCheckIndicatorProps {
passed: boolean;
}
function VariableCheckIndicator({ passed }: VariableCheckIndicatorProps): ReactElement {
const styles = useStyles2(getStyles);
if (passed) {
return (
<Icon
name="check"
className={styles.iconPassed}
title="This variable is referenced by other variables or dashboard."
/>
);
}
return (
<Icon
name="exclamation-triangle"
className={styles.iconFailed}
title="This variable is not referenced by any variable or dashboard."
/>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
dragHandle: css`
cursor: grab;
`,
column: css`
width: 1%;
`,
nameLink: css`
cursor: pointer;
color: ${theme.colors.primary.text};
`,
definitionColumn: css`
width: 100%;
max-width: 200px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
white-space: nowrap;
`,
iconPassed: css`
color: ${theme.v1.palette.greenBase};
`,
iconFailed: css`
color: ${theme.v1.palette.orange};
`,
};
}

View File

@ -1,5 +1,5 @@
import { ThunkResult } from '../../../types';
import { getEditorVariables, getNewVariabelIndex, getVariable, getVariables } from '../state/selectors';
import { getEditorVariables, getNewVariableIndex, getVariable, getVariables } from '../state/selectors';
import {
changeVariableNameFailed,
changeVariableNameSucceeded,
@ -88,7 +88,7 @@ export const switchToNewMode = (type: VariableType = 'query'): ThunkResult<void>
const id = getNextAvailableId(type, getVariables(getState()));
const identifier = { type, id };
const global = false;
const index = getNewVariabelIndex(getState());
const index = getNewVariableIndex(getState());
const model = cloneDeep(variableAdapters.get(type).initialState);
model.id = id;
model.name = id;

View File

@ -2,7 +2,14 @@ import { combineReducers } from '@reduxjs/toolkit';
import { LoadingState } from '@grafana/data';
import { NEW_VARIABLE_ID, VariablesState } from './types';
import { VariableHide, VariableModel } from '../types';
import {
DashboardVariableModel,
initialVariableModelState,
OrgVariableModel,
UserVariableModel,
VariableHide,
VariableModel,
} from '../types';
import { VariableAdapter } from '../adapters';
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
@ -12,10 +19,69 @@ import { DashboardState } from '../../../types';
export const getVariableState = (
noOfVariables: number,
inEditorIndex = -1,
includeEmpty = false
includeEmpty = false,
includeSystem = false
): Record<string, VariableModel> => {
const variables: Record<string, VariableModel> = {};
if (includeSystem) {
const dashboardModel: DashboardVariableModel = {
...initialVariableModelState,
id: '__dashboard',
name: '__dashboard',
type: 'system',
index: -3,
skipUrlSync: true,
hide: VariableHide.hideVariable,
current: {
value: {
name: 'A dashboard title',
uid: 'An dashboard UID',
toString: () => 'A dashboard title',
},
},
};
const orgModel: OrgVariableModel = {
...initialVariableModelState,
id: '__org',
name: '__org',
type: 'system',
index: -2,
skipUrlSync: true,
hide: VariableHide.hideVariable,
current: {
value: {
name: 'An org name',
id: 1,
toString: () => '1',
},
},
};
const userModel: UserVariableModel = {
...initialVariableModelState,
id: '__user',
name: '__user',
type: 'system',
index: -1,
skipUrlSync: true,
hide: VariableHide.hideVariable,
current: {
value: {
login: 'admin',
id: 1,
email: 'admin@test',
toString: () => '1',
},
},
};
variables[dashboardModel.id] = dashboardModel;
variables[orgModel.id] = orgModel;
variables[userModel.id] = userModel;
}
for (let index = 0; index < noOfVariables; index++) {
variables[index] = {
id: index.toString(),

View File

@ -29,12 +29,12 @@ export const getVariableWithName = (name: string, state: StoreState = getState()
};
export const getVariables = (state: StoreState = getState()): VariableModel[] => {
const filter = (variable: VariableModel) => {
return variable.type !== 'system';
return getFilteredVariables(defaultVariablesFilter, state);
};
return getFilteredVariables(filter, state);
};
export function defaultVariablesFilter(variable: VariableModel): boolean {
return variable.type !== 'system';
}
export const getSubMenuVariables = memoizeOne((variables: Record<string, VariableModel>): VariableModel[] => {
return getVariables(getState());
@ -46,9 +46,14 @@ export const getEditorVariables = (state: StoreState): VariableModel[] => {
export type GetVariables = typeof getVariables;
export const getNewVariabelIndex = (state: StoreState = getState()): number => {
return Object.values(state.templating.variables).length;
};
export function getNewVariableIndex(state: StoreState = getState()): number {
return getNextVariableIndex(Object.values(state.templating.variables));
}
export function getNextVariableIndex(variables: VariableModel[]): number {
const sorted = variables.filter(defaultVariablesFilter).sort((v1, v2) => v1.index - v2.index);
return sorted.length > 0 ? sorted[sorted.length - 1].index + 1 : 0;
}
export function getVariablesIsDirty(state: StoreState = getState()): boolean {
return state.templating.transaction.isDirty;

View File

@ -176,12 +176,13 @@ describe('sharedReducer', () => {
describe('when duplicateVariable is dispatched', () => {
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const initialState: VariablesState = getVariableState(3, -1, false, true);
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(duplicateVariable(payload))
.thenStateShouldEqual({
...initialState,
'0': {
id: '0',
type: 'query',
@ -235,7 +236,7 @@ describe('sharedReducer', () => {
describe('when changeVariableOrder is dispatched', () => {
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ id: '1', type: 'query' }, { fromIndex: 1, toIndex: 0 });
const payload = toVariablePayload({ id: '2', type: 'query' }, { fromIndex: 2, toIndex: 0 });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(changeVariableOrder(payload))
@ -253,6 +254,55 @@ describe('sharedReducer', () => {
error: null,
description: null,
},
'1': {
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
index: 2,
label: 'Label-1',
skipUrlSync: false,
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'2': {
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
index: 0,
label: 'Label-2',
skipUrlSync: false,
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
});
});
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ id: '0', type: 'query' }, { fromIndex: 0, toIndex: 2 });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(changeVariableOrder(payload))
.thenStateShouldEqual({
'0': {
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
index: 2,
label: 'Label-0',
skipUrlSync: false,
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'1': {
id: '1',
type: 'query',
@ -271,7 +321,7 @@ describe('sharedReducer', () => {
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
index: 2,
index: 1,
label: 'Label-2',
skipUrlSync: false,
global: false,

View File

@ -6,6 +6,7 @@ import { AddVariable, getInstanceState, initialVariablesState, VariablePayload,
import { variableAdapters } from '../adapters';
import { changeVariableNameSucceeded } from '../editor/reducer';
import { ensureStringValues } from '../utils';
import { getNextVariableIndex } from './selectors';
const sharedReducerSlice = createSlice({
name: 'templating/shared',
@ -71,7 +72,7 @@ const sharedReducerSlice = createSlice({
const original = cloneDeep<VariableModel>(state[action.payload.id]);
const name = `copy_of_${original.name}`;
const newId = action.payload.data?.newId ?? name;
const index = Object.keys(state).length;
const index = getNextVariableIndex(Object.values(state));
state[newId] = {
...cloneDeep(variableAdapters.get(action.payload.type).initialState),
...original,
@ -84,16 +85,17 @@ const sharedReducerSlice = createSlice({
state: VariablesState,
action: PayloadAction<VariablePayload<{ fromIndex: number; toIndex: number }>>
) => {
const variables = Object.values(state).map((s) => s);
const fromVariable = variables.find((v) => v.index === action.payload.data.fromIndex);
const toVariable = variables.find((v) => v.index === action.payload.data.toIndex);
if (fromVariable) {
state[fromVariable.id].index = action.payload.data.toIndex;
const { toIndex, fromIndex } = action.payload.data;
const variableStates = Object.values(state);
for (let index = 0; index < variableStates.length; index++) {
const variable = variableStates[index];
if (variable.index === fromIndex) {
variable.index = toIndex;
} else if (variable.index > fromIndex && variable.index <= toIndex) {
variable.index--;
} else if (variable.index < fromIndex && variable.index >= toIndex) {
variable.index++;
}
if (toVariable) {
state[toVariable.id].index = action.payload.data.fromIndex;
}
},
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {