From 1c13c7e6d74c3f1c227e51d4e1d4d338921c043b Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 26 Jan 2024 10:58:36 -0500 Subject: [PATCH 01/21] Trying some stuff... --- .../src/components/TagCell/TagCell.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index 217fb603e7b..88859380c65 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -8,6 +8,7 @@ import Plus from 'src/assets/icons/plusSign.svg'; import { CircleProgress } from 'src/components/CircleProgress'; import { IconButton } from 'src/components/IconButton'; import { Tag } from 'src/components/Tag/Tag'; +import { useWindowDimensions } from 'src/hooks/useWindowDimensions'; import { omittedProps } from 'src/utilities/omittedProps'; import { AddTag } from './AddTag'; @@ -20,7 +21,7 @@ interface TagCellProps { } // https://stackoverflow.com/questions/143815/determine-if-an-html-elements-content-overflows -const checkOverflow = (el: any) => { +const checkOverflow = (el: HTMLElement) => { const curOverflow = el.style.overflow; if (!curOverflow || curOverflow === 'visible') { @@ -29,6 +30,13 @@ const checkOverflow = (el: any) => { const isOverflowing = el.clientWidth < el.scrollWidth; + console.log( + 'Checking overflow', + el.clientWidth, + el.scrollWidth, + isOverflowing + ); + el.style.overflow = curOverflow; return isOverflowing; @@ -40,18 +48,24 @@ const TagCell = (props: TagCellProps) => { const [hasOverflow, setOverflow] = React.useState(false); const [addingTag, setAddingTag] = React.useState(false); const [loading, setLoading] = React.useState(false); - const overflowRef = React.useCallback( - (node) => { - if (node !== null) { - setOverflow(checkOverflow(node)); + + const elRef = React.useRef(null); + + const windowDimensions = useWindowDimensions(); + + React.useLayoutEffect(() => { + const interval = setInterval(checkOverflow, 5, elRef.current); + return clearInterval(interval); + }, []); + + React.useEffect(() => { + const timeout = setTimeout(() => { + if (elRef.current) { + setOverflow(checkOverflow(elRef.current)); } - }, - // The function doesn't care about tags directly, - // but if the tags list changes we want to check to see if - // the overflow state has changed. - // eslint-disable-next-line - [tags] - ); + }, 10); + return () => clearTimeout(timeout); + }); const handleAddTag = async (tag: string) => { await updateTags([...tags, tag]); @@ -86,7 +100,7 @@ const TagCell = (props: TagCellProps) => { /> ) : ( <> - + {tags.map((thisTag) => ( Date: Mon, 29 Jan 2024 16:11:46 -0500 Subject: [PATCH 02/21] Introduce new MutationObserver hook --- .../manager/src/hooks/useMutationObserver.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/manager/src/hooks/useMutationObserver.ts diff --git a/packages/manager/src/hooks/useMutationObserver.ts b/packages/manager/src/hooks/useMutationObserver.ts new file mode 100644 index 00000000000..dea4b727a58 --- /dev/null +++ b/packages/manager/src/hooks/useMutationObserver.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +/** + * Attaches a MutationObserver to the provided element. + */ +export const useMutationObserver = ( + el: HTMLElement | null, + options: MutationObserverInit +) => { + const [lastMutations, setLastMutations] = useState(); + useEffect(() => { + if (el == null) { + return; + } + const observer = new MutationObserver((mutations) => { + setLastMutations(mutations); + }); + observer.observe(el, options); + return () => { + observer.disconnect(); + }; + }, [el, options]); + return lastMutations; +}; From 92d5fc757d45d58b0621a8be53014903b694d3b0 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 29 Jan 2024 16:12:50 -0500 Subject: [PATCH 03/21] Use new MutationObserver hook to listen for parent becoming visible --- .../src/components/TagCell/TagCell.tsx | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index 88859380c65..b748feb575f 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -8,6 +8,7 @@ import Plus from 'src/assets/icons/plusSign.svg'; import { CircleProgress } from 'src/components/CircleProgress'; import { IconButton } from 'src/components/IconButton'; import { Tag } from 'src/components/Tag/Tag'; +import { useMutationObserver } from 'src/hooks/useMutationObserver'; import { useWindowDimensions } from 'src/hooks/useWindowDimensions'; import { omittedProps } from 'src/utilities/omittedProps'; @@ -30,13 +31,6 @@ const checkOverflow = (el: HTMLElement) => { const isOverflowing = el.clientWidth < el.scrollWidth; - console.log( - 'Checking overflow', - el.clientWidth, - el.scrollWidth, - isOverflowing - ); - el.style.overflow = curOverflow; return isOverflowing; @@ -45,27 +39,32 @@ const checkOverflow = (el: HTMLElement) => { const TagCell = (props: TagCellProps) => { const { sx, tags, updateTags } = props; - const [hasOverflow, setOverflow] = React.useState(false); const [addingTag, setAddingTag] = React.useState(false); const [loading, setLoading] = React.useState(false); - const elRef = React.useRef(null); + const [elRef, setElRef] = React.useState(null); - const windowDimensions = useWindowDimensions(); + // In production, a parent element sometimes is briefly + // set to display: none, breaking overflow detection. + // On mount, find the parent and listen for changes. + const renderParent = React.useMemo(() => { + let parent: HTMLElement | null = elRef; + while (parent != null && parent.style.display != 'none') { + parent = parent.parentElement; + } + return parent; + }, [elRef]); - React.useLayoutEffect(() => { - const interval = setInterval(checkOverflow, 5, elRef.current); - return clearInterval(interval); - }, []); + const windowDimensions = useWindowDimensions(); + const lastMutations = useMutationObserver(renderParent, { attributes: true }); - React.useEffect(() => { - const timeout = setTimeout(() => { - if (elRef.current) { - setOverflow(checkOverflow(elRef.current)); - } - }, 10); - return () => clearTimeout(timeout); - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + const hasOverflow = React.useMemo(() => !!elRef && checkOverflow(elRef), [ + windowDimensions, + lastMutations, + tags, + elRef, + ]); const handleAddTag = async (tag: string) => { await updateTags([...tags, tag]); @@ -100,7 +99,7 @@ const TagCell = (props: TagCellProps) => { /> ) : ( <> - + {tags.map((thisTag) => ( Date: Mon, 29 Jan 2024 16:23:01 -0500 Subject: [PATCH 04/21] Clean up --- packages/manager/src/components/TagCell/TagCell.tsx | 3 ++- packages/manager/src/hooks/useMutationObserver.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index b748feb575f..5bd9f3c476b 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -41,7 +41,6 @@ const TagCell = (props: TagCellProps) => { const [addingTag, setAddingTag] = React.useState(false); const [loading, setLoading] = React.useState(false); - const [elRef, setElRef] = React.useState(null); // In production, a parent element sometimes is briefly @@ -58,6 +57,8 @@ const TagCell = (props: TagCellProps) => { const windowDimensions = useWindowDimensions(); const lastMutations = useMutationObserver(renderParent, { attributes: true }); + // Dependencies are not used explicitly for overflow detection but + // changes in these values may indicate the overflow state has changed. // eslint-disable-next-line react-hooks/exhaustive-deps const hasOverflow = React.useMemo(() => !!elRef && checkOverflow(elRef), [ windowDimensions, diff --git a/packages/manager/src/hooks/useMutationObserver.ts b/packages/manager/src/hooks/useMutationObserver.ts index dea4b727a58..09d834940b4 100644 --- a/packages/manager/src/hooks/useMutationObserver.ts +++ b/packages/manager/src/hooks/useMutationObserver.ts @@ -12,9 +12,7 @@ export const useMutationObserver = ( if (el == null) { return; } - const observer = new MutationObserver((mutations) => { - setLastMutations(mutations); - }); + const observer = new MutationObserver(setLastMutations); observer.observe(el, options); return () => { observer.disconnect(); From c2ead659b4148aaf55a02a98cfc394b847644e06 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 29 Jan 2024 16:44:00 -0500 Subject: [PATCH 05/21] Improve TagCell styles for small displays --- .../src/features/Linodes/LinodeEntityDetailFooter.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx index 203d50752ce..984a72aabf9 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx @@ -153,12 +153,7 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => { > button': { - marginRight: theme.spacing(0.5), - }, - flexDirection: 'row-reverse', - }, + width: '100%', }} listAllTags={openTagDrawer} tags={linodeTags} From 9babcf09eba8cec143401afcea806f2d226a7e25 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 29 Jan 2024 17:24:17 -0500 Subject: [PATCH 06/21] Added changeset: Tag overflow detection --- packages/manager/.changeset/pr-10122-fixed-1706567057589.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10122-fixed-1706567057589.md diff --git a/packages/manager/.changeset/pr-10122-fixed-1706567057589.md b/packages/manager/.changeset/pr-10122-fixed-1706567057589.md new file mode 100644 index 00000000000..31affdd9fdd --- /dev/null +++ b/packages/manager/.changeset/pr-10122-fixed-1706567057589.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Tag overflow detection ([#10122](https://github.com/linode/manager/pull/10122)) From 64e7b7d0dc5b6861eeaa8371677947ae9c8ff5a9 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 27 Feb 2024 17:45:31 -0500 Subject: [PATCH 07/21] Improve tags experience and replace deprecated Select with Autocomplete component --- .../components/Autocomplete/Autocomplete.tsx | 18 ++- .../src/components/Button/StyledTagButton.ts | 2 + .../manager/src/components/TagCell/AddTag.tsx | 117 +++++++-------- .../src/components/TagCell/TagCell.tsx | 53 ++----- .../src/components/TagsPanel/TagsPanel.tsx | 140 ++++-------------- .../manager/src/foundations/themes/light.ts | 1 + 6 files changed, 117 insertions(+), 214 deletions(-) diff --git a/packages/manager/src/components/Autocomplete/Autocomplete.tsx b/packages/manager/src/components/Autocomplete/Autocomplete.tsx index 013314d5235..6705dcd7541 100644 --- a/packages/manager/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/manager/src/components/Autocomplete/Autocomplete.tsx @@ -1,11 +1,13 @@ import CloseIcon from '@mui/icons-material/Close'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import MuiAutocomplete from '@mui/material/Autocomplete'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box } from 'src/components/Box'; import { TextField, TextFieldProps } from 'src/components/TextField'; +import { CircleProgress } from '../CircleProgress'; +import { InputAdornment } from '../InputAdornment'; import { CustomPopper, SelectedIcon, @@ -31,8 +33,8 @@ export interface EnhancedAutocompleteProps< label: string; /** Removes the top margin from the input label, if desired. */ noMarginTop?: boolean; - /** Text to show when the Autocomplete search yields no results. */ - noOptionsText?: string; + /** Element to show when the Autocomplete search yields no results. */ + noOptionsText?: ReactNode; /** Label for the "select all" option. */ selectAllLabel?: string; textFieldProps?: Partial; @@ -112,6 +114,16 @@ export const Autocomplete = < InputProps={{ ...params.InputProps, ...textFieldProps?.InputProps, + endAdornment: ( + <> + {loading && ( + + + + )} + {params.InputProps.endAdornment} + + ), }} /> )} diff --git a/packages/manager/src/components/Button/StyledTagButton.ts b/packages/manager/src/components/Button/StyledTagButton.ts index a35a6ef302a..363681cb572 100644 --- a/packages/manager/src/components/Button/StyledTagButton.ts +++ b/packages/manager/src/components/Button/StyledTagButton.ts @@ -15,6 +15,8 @@ export const StyledTagButton = styled(Button, { })(({ theme, ...props }) => ({ border: 'none', fontSize: '0.875rem', + minHeight: 30, + whiteSpace: 'nowrap', ...(!props.disabled && { '&:hover, &:focus': { backgroundColor: theme.color.tagButton, diff --git a/packages/manager/src/components/TagCell/AddTag.tsx b/packages/manager/src/components/TagCell/AddTag.tsx index 18558ebe91e..ce871bb9ff4 100644 --- a/packages/manager/src/components/TagCell/AddTag.tsx +++ b/packages/manager/src/components/TagCell/AddTag.tsx @@ -1,23 +1,19 @@ -import { styled } from '@mui/material/styles'; import * as React from 'react'; import { useQueryClient } from 'react-query'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { useProfile } from 'src/queries/profile'; import { updateTagsSuggestionsData, useTagSuggestions } from 'src/queries/tags'; -import { omittedProps } from 'src/utilities/omittedProps'; + +import { Autocomplete } from '../Autocomplete/Autocomplete'; interface AddTagProps { addTag: (tag: string) => Promise; - fixedMenu?: boolean; - inDetailsContext?: boolean; - label?: string; + existingTags: string[]; onClose?: () => void; - tags: string[]; } -const AddTag = (props: AddTagProps) => { - const { addTag, fixedMenu, label, onClose, tags } = props; +export const AddTag = (props: AddTagProps) => { + const { addTag, existingTags, onClose } = props; const queryClient = useQueryClient(); const [isLoading, setIsLoading] = React.useState(false); @@ -30,67 +26,64 @@ const AddTag = (props: AddTagProps) => { // thing we lose is preexisting tabs as options; the add tag flow // should still work. - const tagOptions = accountTags - ?.filter((tag) => !tags.includes(tag.label)) - .map((tag) => ({ label: tag.label, value: tag.label })); + const [inputValue, setInputValue] = React.useState(''); + + const createTag = + !!accountTags && + !!inputValue && + !accountTags.some( + (tag) => tag.label.toLowerCase() == inputValue.toLowerCase() + ); + + const tagOptions: { displayLabel?: string; label: string }[] = [ + ...(createTag + ? [{ displayLabel: `Create "${inputValue}"`, label: inputValue }] + : []), + ...(accountTags?.filter((tag) => !existingTags.includes(tag.label)) ?? []), + ]; - const handleAddTag = (newTag: Item) => { - if (newTag?.value) { - setIsLoading(true); - addTag(newTag.value) - .then(() => { - if (accountTags) { - updateTagsSuggestionsData([...accountTags, newTag], queryClient); - } - if (onClose) { - onClose(); - } - }) - .finally(() => setIsLoading(false)); - } + const handleAddTag = (newTag: string) => { + setIsLoading(true); + addTag(newTag) + .then(() => { + if (accountTags) { + updateTagsSuggestionsData( + [...accountTags, { label: newTag }], + queryClient + ); + } + if (onClose) { + onClose(); + } + }) + .finally(() => setIsLoading(false)); }; const loading = accountTagsLoading || isLoading; return ( - { + if (value) { + handleAddTag(typeof value == 'string' ? value : value.label); + } + }} + renderOption={(props, option) => ( +
  • {option.displayLabel ?? option.label}
  • + )} + disableClearable + disabled={isLoading} + forcePopupIcon + label={'Add a tag'} + loading={loading} + noOptionsText={{`"${inputValue}" already added`}} // Will display create option unless that tag is already added onBlur={onClose} - onChange={handleAddTag} - options={tagOptions} + onInputChange={(_, value) => setInputValue(value)} + openOnFocus + options={tagOptions ?? []} placeholder="Create or Select a Tag" - small + sx={{ width: '100%' }} + textFieldProps={{ autoFocus: true, hideLabel: true }} /> ); }; - -export { AddTag }; - -const StyledSelect = styled(Select, { - shouldForwardProp: omittedProps(['fixedMenu', 'inDetailsContext']), -})<{ - fixedMenu?: boolean; - inDetailsContext?: boolean; -}>(({ ...props }) => ({ - padding: '0px', - width: '100%', - ...(props.fixedMenu && { - '& .react-select__menu': { - margin: '2px 0 0 0', - }, - }), - ...(props.inDetailsContext && { - display: 'flex', - flexBasis: '100%', - justifyContent: 'flex-end', - width: '415px', - }), -})); diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index 67070aa778a..18de0e4f1c1 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -1,10 +1,9 @@ import MoreHoriz from '@mui/icons-material/MoreHoriz'; -import Grid from '@mui/material/Unstable_Grid2'; import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import { SxProps } from '@mui/system'; import * as React from 'react'; -import { CircleProgress } from 'src/components/CircleProgress'; import { IconButton } from 'src/components/IconButton'; import { Tag } from 'src/components/Tag/Tag'; import { useMutationObserver } from 'src/hooks/useMutationObserver'; @@ -36,11 +35,13 @@ const checkOverflow = (el: HTMLElement) => { return isOverflowing; }; -const TagCell = (props: TagCellProps) => { +export const TagCell = (props: TagCellProps) => { const { sx, tags, updateTags } = props; const [addingTag, setAddingTag] = React.useState(false); - const [loading, setLoading] = React.useState(false); + const [deletingTags, setDeletingTags] = React.useState( + () => new Set() + ); const [elRef, setElRef] = React.useState(null); // In production, a parent element sometimes is briefly @@ -57,25 +58,21 @@ const TagCell = (props: TagCellProps) => { const windowDimensions = useWindowDimensions(); const lastMutations = useMutationObserver(renderParent, { attributes: true }); - // Dependencies are not used explicitly for overflow detection but - // changes in these values may indicate the overflow state has changed. - // eslint-disable-next-line react-hooks/exhaustive-deps - const hasOverflow = React.useMemo(() => !!elRef && checkOverflow(elRef), [ - windowDimensions, - lastMutations, - tags, - elRef, - ]); + const [hasOverflow, setHasOverflow] = React.useState(false); + React.useLayoutEffect(() => { + setHasOverflow(!!elRef && checkOverflow(elRef)); + }, [windowDimensions, lastMutations, tags, elRef]); const handleAddTag = async (tag: string) => { await updateTags([...tags, tag]); }; const handleDeleteTag = (tagToDelete: string) => { - setLoading(true); - updateTags(tags.filter((tag) => tag !== tagToDelete)).finally(() => - setLoading(false) - ); + setDeletingTags(new Set(deletingTags.add(tagToDelete))); + updateTags(tags.filter((tag) => tag !== tagToDelete)).finally(() => { + deletingTags.delete(tagToDelete); + setDeletingTags(new Set(deletingTags)); + }); }; return ( @@ -86,17 +83,11 @@ const TagCell = (props: TagCellProps) => { sx={sx} wrap="nowrap" > - {loading ? ( - - - - ) : null} {addingTag ? ( setAddingTag(false)} - tags={tags} /> ) : ( <> @@ -106,7 +97,7 @@ const TagCell = (props: TagCellProps) => { colorVariant="lightBlue" key={`tag-item-${thisTag}`} label={thisTag} - loading={loading} + loading={deletingTags.has(thisTag)} onDelete={() => handleDeleteTag(thisTag)} /> ))} @@ -136,24 +127,12 @@ const TagCell = (props: TagCellProps) => { ); }; -export { TagCell }; - const StyledGrid = styled(Grid)({ justifyContent: 'flex-end', minHeight: 40, position: 'relative', }); -const StyledCircleDiv = styled('div')({ - alignItems: 'center', - display: 'flex', - height: '100%', - justifyContent: 'center', - position: 'absolute', - width: '100%', - zIndex: 2, -}); - const StyledTagListDiv = styled('div', { shouldForwardProp: omittedProps(['hasOverflow']), })<{ diff --git a/packages/manager/src/components/TagsPanel/TagsPanel.tsx b/packages/manager/src/components/TagsPanel/TagsPanel.tsx index be5b9e4388a..931b4258312 100644 --- a/packages/manager/src/components/TagsPanel/TagsPanel.tsx +++ b/packages/manager/src/components/TagsPanel/TagsPanel.tsx @@ -1,33 +1,16 @@ import * as React from 'react'; -import { useQueryClient } from 'react-query'; import { StyledPlusIcon, StyledTagButton, } from 'src/components/Button/StyledTagButton'; -import { CircleProgress } from 'src/components/CircleProgress'; -import Select from 'src/components/EnhancedSelect/Select'; import { Tag } from 'src/components/Tag/Tag'; import { Typography } from 'src/components/Typography'; -import { useProfile } from 'src/queries/profile'; -import { updateTagsSuggestionsData, useTagSuggestions } from 'src/queries/tags'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; +import { AddTag } from '../TagCell/AddTag'; import { useStyles } from './TagsPanel.styles'; -interface Item { - label: string; - value: string; -} - -interface Tag { - label: string; -} - -interface ActionMeta { - action: string; -} - export interface TagsPanelProps { /** * If true, the input will be disabled and no tags can be added or removed. @@ -47,33 +30,16 @@ export const TagsPanel = (props: TagsPanelProps) => { const { classes, cx } = useStyles(); const { disabled, tags, updateTags } = props; - const queryClient = useQueryClient(); - const [tagError, setTagError] = React.useState(''); const [isCreatingTag, setIsCreatingTag] = React.useState(false); - const [tagsLoading, setTagsLoading] = React.useState(false); - - const { data: profile } = useProfile(); - - const { - data: userTags, - error: userTagsError, - isLoading: userTagsLoading, - } = useTagSuggestions(!profile?.restricted); - - const tagsToSuggest = React.useMemo( - () => - userTags - ?.filter((tag) => !tags.some((appliedTag) => appliedTag === tag.label)) - .map((tag) => ({ - label: tag.label, - value: tag.label, - })), - [userTags, tags] + const [deletingTags, setDeletingTags] = React.useState( + () => new Set() ); React.useEffect(() => { - setTagError(''); + if (isCreatingTag) { + setTagError(''); + } }, [isCreatingTag]); const toggleTagInput = () => { @@ -82,17 +48,11 @@ export const TagsPanel = (props: TagsPanelProps) => { } }; - const userTagsErrorDisplay = userTagsError - ? 'There was an error retrieving your tags.' - : ''; - const handleDeleteTag = (label: string) => { - setTagsLoading(true); - const tagsWithoutDeletedTag = tags.filter( (thisTag: string) => thisTag !== label ); - + setDeletingTags(new Set(deletingTags.add(label))); updateTags(tagsWithoutDeletedTag) .then(() => { setTagError(''); @@ -102,78 +62,40 @@ export const TagsPanel = (props: TagsPanelProps) => { setTagError(tagError); }) .finally(() => { - setTagsLoading(false); + deletingTags.delete(label); + setDeletingTags(new Set(deletingTags)); }); }; - const handleCreateTag = (value: Item, actionMeta: ActionMeta) => { - const inputValue = value && value.value; - - /* - * This comes from the react-select API - * basically, we only want to make a request if the user is either - * hitting the enter button or choosing a selection from the dropdown - */ - if ( - actionMeta.action !== 'select-option' && - actionMeta.action !== 'create-option' - ) { - return; - } - + const handleCreateTag = async (tag: string) => { const tagExists = (tag: string) => { return tags.some((el) => { return el === tag; }); }; - toggleTagInput(); - - if (inputValue.length < 3 || inputValue.length > 50) { - setTagError(`Tag "${inputValue}" length must be 3-50 characters`); - } else if (tagExists(inputValue)) { - setTagError(`Tag "${inputValue}" is a duplicate`); + if (tag.length < 3 || tag.length > 50) { + setTagError(`Tag "${tag}" length must be 3-50 characters`); + } else if (tagExists(tag)) { + setTagError(`Tag "${tag}" is a duplicate`); } else { setTagError(''); - setTagsLoading(true); - updateTags([...tags, value.label].sort()) - .then(() => { - if (userTags) { - updateTagsSuggestionsData([...userTags, value], queryClient); - } - }) - .catch((e) => { - const tagError = getErrorStringOrDefault( - e, - 'Error while creating tag' - ); - setTagError(tagError); - }) - .finally(() => { - setTagsLoading(false); - }); + try { + await updateTags([...tags, tag].sort()); + } catch (e) { + const tagError = getErrorStringOrDefault(e, 'Error while creating tag'); + setTagError(tagError); + } } }; return ( <> {isCreatingTag ? ( -