From db478ced45a2c403ba8b31f89220cf6d48d4fb55 Mon Sep 17 00:00:00 2001 From: Jaryk Date: Wed, 28 Jun 2023 13:53:53 +0200 Subject: [PATCH] Variables: Detect a name for duplicated variable (#68110) * Variables: Detect a name for duplicated variable * Variables: create copies of copies --- .../variables/state/sharedReducer.test.ts | 76 +++++++++++++++++++ .../features/variables/state/sharedReducer.ts | 23 +++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/public/app/features/variables/state/sharedReducer.test.ts b/public/app/features/variables/state/sharedReducer.test.ts index fba5885cc89..6f221e21e49 100644 --- a/public/app/features/variables/state/sharedReducer.test.ts +++ b/public/app/features/variables/state/sharedReducer.test.ts @@ -150,6 +150,82 @@ describe('sharedReducer', () => { }, }); }); + + it('then state should be correct', () => { + const initialState: VariablesState = getVariableState(3, -1, false, true); + initialState['1'].name = 'copy_of_Name-1_2'; + const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' }); + reducerTester() + .givenReducer(sharedReducer, initialState) + .whenActionIsDispatched(duplicateVariable(payload)) + .thenStateShouldEqual({ + ...initialState, + '11': { + ...initialQueryVariableModelState, + ...initialState['1'], + id: '11', + name: 'copy_of_copy_of_Name-1_2', + index: 3, + }, + }); + }); + + it('then state should be correct', () => { + const initialState: VariablesState = getVariableState(3, -1, false, true); + initialState['0'].name = 'Name-0'; + initialState['1'].name = 'copy_of_Name-0_2'; + const payload = toVariablePayload({ id: '0', type: 'query' }, { newId: '01' }); + reducerTester() + .givenReducer(sharedReducer, initialState) + .whenActionIsDispatched(duplicateVariable(payload)) + .thenStateShouldEqual({ + ...initialState, + '01': { + ...initialQueryVariableModelState, + ...initialState['0'], + id: '01', + name: 'copy_of_Name-0_3', + index: 3, + }, + }); + }); + + it('then state should be correct', () => { + const initialState: VariablesState = getVariableState(3, -1, false, true); + initialState['1'].name = 'copy_of_Name-1_2'; + const duplicateOne = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' }); + const duplicateTwo = toVariablePayload({ id: '1', type: 'query' }, { newId: '12' }); + const duplicateThree = toVariablePayload({ id: '1', type: 'query' }, { newId: '13' }); + reducerTester() + .givenReducer(sharedReducer, initialState) + .whenActionIsDispatched(duplicateVariable(duplicateOne)) + .whenActionIsDispatched(duplicateVariable(duplicateTwo)) + .whenActionIsDispatched(duplicateVariable(duplicateThree)) + .thenStateShouldEqual({ + ...initialState, + '11': { + ...initialQueryVariableModelState, + ...initialState['1'], + id: '11', + name: 'copy_of_copy_of_Name-1_2', + index: 3, + }, + '12': { + ...initialQueryVariableModelState, + ...initialState['1'], + id: '12', + name: 'copy_of_copy_of_Name-1_2_1', + index: 4, + }, + '13': { + ...initialQueryVariableModelState, + ...initialState['1'], + id: '13', + name: 'copy_of_copy_of_Name-1_2_2', + index: 5, + }, + }); + }); }); describe('when changeVariableOrder is dispatched', () => { diff --git a/public/app/features/variables/state/sharedReducer.ts b/public/app/features/variables/state/sharedReducer.ts index 4c5bda5f013..fefea202464 100644 --- a/public/app/features/variables/state/sharedReducer.ts +++ b/public/app/features/variables/state/sharedReducer.ts @@ -73,8 +73,29 @@ const sharedReducerSlice = createSlice({ } }, duplicateVariable: (state: VariablesState, action: PayloadAction>) => { + function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + const original = cloneDeep(state[action.payload.id]); - const name = `copy_of_${original.name}`; + const copyRegex = new RegExp(`^copy_of_${escapeRegExp(original.name)}(_(\\d+))?$`); + + const copies = Object.values(state) + .map(({ name }) => name.match(copyRegex)) + .filter((v): v is RegExpMatchArray => v != null); + const numberedCopies = copies.map((match) => match[2]).filter((v): v is string => v != null); + + const suffix = ((): number | null => { + if (copies.length === 0) { + return null; + } + if (numberedCopies.length === 0) { + return 1; + } + return numberedCopies.map((v) => +v).sort((a, b) => b - a)[0] + 1; + })(); + + const name = `copy_of_${original.name}${suffix ? `_${suffix}` : ''}`; const newId = action.payload.data?.newId ?? name; const index = getNextVariableIndex(Object.values(state)); state[newId] = {