1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

fix(1-3173): clear "removed tags" when you bulk update tags (#8952)

This PR fixes a bug wherein the list of tags to remove from a group of
tags wouldn't be correctly updated.

## Repro steps
- Add a console log line to
`frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx`'s
`ManagebulkTagsDialog`. Log the value of the`payload` variable.
- Pick a flag with no tags.
- Add tag A -> before submitting, you should have one added tag and zero
removed flags. After submitting, both should be empty.
- Now remove tag A -> before submitting, you should have one removed tag
and zero added tag. After submitting, both should be empty
- Notice that removed flags hasn't been emptied, but still contains tag
A.
- Now add tab B -> before submitting, you should have tag B in added and
nothing in removed. Notice that tag A is still in removed.



## Discussion points

This gives us both a `clear` and a `reset` event, which is unfortunate
because they sound like they do the same thing. I'd suggest renaming the
`clear` event (because it doesn't really clear the state completely),
but I'm not sure to what. Happy to do that if you have a suggestion.

I have not tested that submission of the form actually resets the state.
I spent about 45 minutes looking at it, but couldn't find a way that was
sensible and worked (considered spying: couldn't make it work;
considered refactoring and extracting components: think that's too much
of a change). I think this is benign enough that it can go without a
test for that thing actually being called.

I did, however, test the different reducer commands.
This commit is contained in:
Thomas Heartman 2024-12-12 09:31:39 +01:00 committed by GitHub
parent 37a3ec9599
commit 7a436347cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 120 additions and 9 deletions

View File

@ -0,0 +1,89 @@
import { payloadReducer } from './ManageBulkTagsDialog';
describe('payloadReducer', () => {
it('should add a tag to addedTags and remove it from removedTags', () => {
const initialState = {
addedTags: [{ type: 'simple', value: 'A' }],
removedTags: [
{ type: 'simple', value: 'B' },
{ type: 'simple', value: 'C' },
],
};
const action = {
type: 'add' as const,
payload: { type: 'simple', value: 'B' },
};
const newState = payloadReducer(initialState, action);
expect(newState).toMatchObject({
addedTags: [
{ type: 'simple', value: 'A' },
{ type: 'simple', value: 'B' },
],
removedTags: [{ type: 'simple', value: 'C' }],
});
});
it('should remove a tag from addedTags and add it to removedTags', () => {
const initialState = {
addedTags: [
{ type: 'simple', value: 'A' },
{ type: 'simple', value: 'B' },
],
removedTags: [{ type: 'simple', value: 'C' }],
};
const action = {
type: 'remove' as const,
payload: { type: 'simple', value: 'B' },
};
const newState = payloadReducer(initialState, action);
expect(newState).toMatchObject({
addedTags: [{ type: 'simple', value: 'A' }],
removedTags: [
{ type: 'simple', value: 'C' },
{ type: 'simple', value: 'B' },
],
});
});
it('should empty addedTags and set removedTags to the payload on clear', () => {
const initialState = {
addedTags: [{ type: 'simple', value: 'A' }],
removedTags: [{ type: 'simple', value: 'B' }],
};
const action = {
type: 'clear' as const,
payload: [{ type: 'simple', value: 'C' }],
};
const newState = payloadReducer(initialState, action);
expect(newState).toMatchObject({
addedTags: [],
removedTags: [{ type: 'simple', value: 'C' }],
});
});
it('should empty both addedTags and removedTags on reset', () => {
const initialState = {
addedTags: [{ type: 'simple', value: 'test' }],
removedTags: [{ type: 'simple', value: 'test2' }],
};
const action = {
type: 'reset' as const,
};
const newState = payloadReducer(initialState, action);
expect(newState).toMatchObject({
addedTags: [],
removedTags: [],
});
});
});

View File

@ -1,4 +1,4 @@
import { useEffect, useReducer, useState, type VFC } from 'react';
import { type FC, useEffect, useReducer, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import {
type AutocompleteProps,
@ -46,7 +46,7 @@ const mergeTags = (tags: ITag[], newTag: ITag) => [
const filterTags = (tags: ITag[], tag: ITag) =>
tags.filter((x) => !(x.value === tag.value && x.type === tag.type));
const payloadReducer = (
export const payloadReducer = (
state: Payload,
action:
| {
@ -56,7 +56,8 @@ const payloadReducer = (
| {
type: 'clear';
payload: ITag[];
},
}
| { type: 'reset' },
) => {
switch (action.type) {
case 'add':
@ -76,6 +77,11 @@ const payloadReducer = (
addedTags: [],
removedTags: action.payload,
};
case 'reset':
return {
addedTags: [],
removedTags: [],
};
default:
return state;
}
@ -87,7 +93,7 @@ const emptyTagType = {
icon: '',
};
export const ManageBulkTagsDialog: VFC<IManageBulkTagsDialogProps> = ({
export const ManageBulkTagsDialog: FC<IManageBulkTagsDialogProps> = ({
open,
initialValues,
initialIndeterminateValues,
@ -106,6 +112,11 @@ export const ManageBulkTagsDialog: VFC<IManageBulkTagsDialogProps> = ({
removedTags: [],
});
const submitAndReset = () => {
onSubmit(payload);
dispatch({ type: 'reset' });
};
const resetTagType = (
tagType: ITagType = tagTypes.length > 0 ? tagTypes[0] : emptyTagType,
) => {
@ -230,7 +241,7 @@ export const ManageBulkTagsDialog: VFC<IManageBulkTagsDialogProps> = ({
secondaryButtonText='Cancel'
primaryButtonText='Save tags'
title='Update feature flag tags'
onClick={() => onSubmit(payload)}
onClick={submitAndReset}
disabledPrimaryButton={
payload.addedTags.length === 0 &&
payload.removedTags.length === 0
@ -244,7 +255,7 @@ export const ManageBulkTagsDialog: VFC<IManageBulkTagsDialogProps> = ({
>
Tags allow you to group features together
</Typography>
<form id={formId} onSubmit={() => onSubmit(payload)}>
<form id={formId} onSubmit={submitAndReset}>
<StyledDialogFormContent>
<TagTypeSelect
key={tagTypesLoading ? 'loading' : tagTypes.length}

View File

@ -7,6 +7,7 @@ import {
useTheme,
} from '@mui/material';
import type { ITagType } from 'interfaces/tags';
import type { HTMLAttributes } from 'react';
interface ITagSelect {
options: ITagType[];
@ -37,8 +38,15 @@ export const TagTypeSelect = ({
disableClearable
value={value}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
renderOption={(
{
key,
...props
}: JSX.IntrinsicAttributes & HTMLAttributes<HTMLLIElement>,
option,
) => (
<ListItem
key={key}
{...props}
style={{
alignItems: 'flex-start',

View File

@ -52,7 +52,10 @@ export const TagsInput = ({
};
const renderOption = (
props: JSX.IntrinsicAttributes &
{
key,
...props
}: JSX.IntrinsicAttributes &
React.ClassAttributes<HTMLLIElement> &
React.LiHTMLAttributes<HTMLLIElement>,
option: TagOption,
@ -64,7 +67,7 @@ export const TagsInput = ({
indeterminateOption.title === option.title,
) ?? false;
return (
<li {...props}>
<li key={key} {...props}>
<ConditionallyRender
condition={Boolean(option.inputValue)}
show={<Add sx={{ mr: (theme) => theme.spacing(0.5) }} />}