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}
usages={this.props.usages}
usagesNetwork={this.props.usagesNetwork}
/>
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
</>
<VariableEditorList
variables={this.props.variables}
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,67 +1,98 @@
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 class VariableEditorList extends PureComponent<Props> {
onEditClick = (event: MouseEvent, identifier: VariableIdentifier) => {
event.preventDefault();
this.props.onEditClick(identifier);
export function VariableEditorList({
variables,
usages,
usagesNetwork,
onChangeOrder,
onAdd,
onEdit,
onDelete,
onDuplicate,
}: Props): ReactElement {
const onDragEnd = (result: DropResult) => {
if (!result.destination || !result.source) {
return;
}
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 (
return (
<div>
<div>
<div>
{this.props.variables.length === 0 && (
<div>
<EmptyListCTA
title="There are no variables yet"
buttonIcon="calculator-alt"
buttonTitle="Add variable"
infoBox={{
__html: ` <p>
{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"
buttonIcon="calculator-alt"
buttonTitle="Add variable"
infoBox={{
__html: ` <p>
Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server
or sensor names in your metric queries you can use variables in their place. Variables are shown as
list boxes at the top of the dashboard. These drop-down lists make it easy to change the data
@ -71,162 +102,13 @@ export class VariableEditorList extends PureComponent<Props> {
</a>
for more information.
</p>`,
}}
infoBoxTitle="What do variables do?"
onClick={this.props.onAddClick}
/>
</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."
}}
infoBoxTitle="What do variables do?"
onClick={(event) => {
event.preventDefault();
onAdd();
}}
/>
);
}
return (
<Icon
name="exclamation-triangle"
className={style.iconFailed}
title="This variable is not referenced by any variable or dashboard."
/>
</div>
);
};
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,13 +29,13 @@ 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(filter, state);
return getFilteredVariables(defaultVariablesFilter, 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;
}
if (toVariable) {
state[toVariable.id].index = action.payload.data.fromIndex;
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++;
}
}
},
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {