Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change: [M3-7540] - Improve tags experience #10122

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1c13c7e
Trying some stuff...
hkhalil-akamai Jan 26, 2024
b28ede9
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Jan 29, 2024
e253802
Introduce new MutationObserver hook
hkhalil-akamai Jan 29, 2024
92d5fc7
Use new MutationObserver hook to listen for parent becoming visible
hkhalil-akamai Jan 29, 2024
a2771ef
Merge remote-tracking branch 'upstream/develop' into M2-7540-weird-ta…
hkhalil-akamai Jan 29, 2024
bf42b4c
Clean up
hkhalil-akamai Jan 29, 2024
c2ead65
Improve TagCell styles for small displays
hkhalil-akamai Jan 29, 2024
9babcf0
Added changeset: Tag overflow detection
hkhalil-akamai Jan 29, 2024
03df9b9
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Feb 5, 2024
64e7b7d
Improve tags experience and replace deprecated Select with Autocomple…
hkhalil-akamai Feb 27, 2024
c7be0aa
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Feb 27, 2024
1f8d808
Update changeset
hkhalil-akamai Feb 27, 2024
716f640
Remove workaround and update changeset
hkhalil-akamai Feb 28, 2024
311326e
Fix test failure
hkhalil-akamai Feb 28, 2024
d1e923f
Fix race condition with new useAtomic hook
hkhalil-akamai Mar 1, 2024
9a10f32
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Mar 1, 2024
f0f0a1d
Delete TagsPanel in favor of TagCell
hkhalil-akamai Mar 1, 2024
6e87f5c
Add test for useAtomic
hkhalil-akamai Mar 2, 2024
702ab3f
Fix inconsistent TagCell margin bottom
hkhalil-akamai Mar 2, 2024
aa3c9cd
Feedback @abailly-akamai
hkhalil-akamai Mar 2, 2024
87618b3
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Mar 11, 2024
11a1854
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Mar 18, 2024
7136c61
Add debouncing to useAtomic
hkhalil-akamai Mar 19, 2024
8633402
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Mar 19, 2024
3779105
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Mar 29, 2024
6788aa6
Remove useAtomic
hkhalil-akamai Mar 29, 2024
4e86cc4
Fix errors due to forwarded prop
hkhalil-akamai Mar 29, 2024
224e6bc
Add loading state to AddTag
hkhalil-akamai Apr 1, 2024
51b117d
Merge remote-tracking branch 'upstream/develop' into M3-7540-weird-ta…
hkhalil-akamai Apr 2, 2024
fe8d957
Add storybook entry for TagCell
hkhalil-akamai Apr 2, 2024
fc85b11
Feedback @abailly-akamai
hkhalil-akamai Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Improve tags experience ([#10122](https://github.com/linode/manager/pull/10122))
15 changes: 11 additions & 4 deletions packages/manager/src/components/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import React 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,
Expand All @@ -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?: JSX.Element | string;
placeholder?: string;
/** Label for the "select all" option. */
selectAllLabel?: string;
Expand Down Expand Up @@ -115,10 +117,15 @@ export const Autocomplete = <
...params.InputProps,
...textFieldProps?.InputProps,
endAdornment: (
<React.Fragment>
<>
{loading && (
<InputAdornment position="end">
<CircleProgress mini={true} />
</InputAdornment>
)}
{textFieldProps?.InputProps?.endAdornment}
{params.InputProps.endAdornment}
</React.Fragment>
</>
),
}}
/>
Expand Down
9 changes: 8 additions & 1 deletion packages/manager/src/components/Button/StyledTagButton.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { styled } from '@mui/material/styles';

import Plus from 'src/assets/icons/plusSign.svg';
import { omittedProps } from 'src/utilities/omittedProps';

import { Button } from './Button';

Expand All @@ -12,9 +13,15 @@ import { Button } from './Button';
*/
export const StyledTagButton = styled(Button, {
label: 'StyledTagButton',
})(({ theme, ...props }) => ({
shouldForwardProp: omittedProps(['panel']),
})<{ panel?: boolean }>(({ theme, ...props }) => ({
hkhalil-akamai marked this conversation as resolved.
Show resolved Hide resolved
border: 'none',
fontSize: '0.875rem',
minHeight: 30,
whiteSpace: 'nowrap',
...(props.panel && {
height: 34,
}),
...(!props.disabled && {
'&:hover, &:focus': {
backgroundColor: theme.color.tagButton,
Expand Down
62 changes: 62 additions & 0 deletions packages/manager/src/components/TagCell.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useArgs } from '@storybook/preview-api';
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Box } from 'src/components/Box';

import { TagCell, TagCellProps } from './TagCell/TagCell';

const _tags: string[] = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];

export const Default: StoryObj<TagCellProps> = {
render: (args) => {
const TagsInputWrapper = () => {
const [{ tags }, updateArgs] = useArgs();
const handleUpdateTags = (selected: string[]) => {
return Promise.resolve(updateArgs({ tags: selected }));
};

return (
<Box sx={{ height: 300 }}>
<TagCell {...args} tags={tags} updateTags={handleUpdateTags} />
</Box>
);
};

return TagsInputWrapper();
},
};

export const InlineView: StoryObj<TagCellProps> = {
render: (args) => {
const TagsInputWrapper = () => {
const [{ tags }, updateArgs] = useArgs();
const handleUpdateTags = (selected: string[]) => {
return Promise.resolve(updateArgs({ tags: selected }));
};

return (
<Box sx={{ height: 300 }}>
<TagCell
{...args}
listAllTags={() => undefined}
tags={tags}
updateTags={handleUpdateTags}
/>
</Box>
);
};

return TagsInputWrapper();
},
};

const meta: Meta<TagCellProps> = {
args: {
disabled: false,
tags: _tags,
},
component: TagCell,
title: 'Components/Tags/Tag Cell',
};
export default meta;
128 changes: 62 additions & 66 deletions packages/manager/src/components/TagCell/AddTag.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { styled } from '@mui/material/styles';
import { useQueryClient } from '@tanstack/react-query';
import * as React from 'react';

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<void>;
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);
const { data: profile } = useProfile();
const {
data: accountTags,
Expand All @@ -30,67 +25,68 @@ 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 [loading, setLoading] = React.useState(false);

const handleAddTag = (newTag: Item<string>) => {
if (newTag?.value) {
setIsLoading(true);
addTag(newTag.value)
.then(() => {
if (accountTags) {
updateTagsSuggestionsData([...accountTags, newTag], queryClient);
}
if (onClose) {
onClose();
}
})
.finally(() => setIsLoading(false));
}
};
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 loading = accountTagsLoading || isLoading;
const handleAddTag = (newTag: string) => {
setLoading(true);
addTag(newTag)
.then(() => {
if (accountTags) {
updateTagsSuggestionsData(
[...accountTags, { label: newTag }],
queryClient
);
}
})
.finally(() => {
setLoading(false);
if (onClose) {
onClose();
}
});
};

return (
<StyledSelect
// eslint-disable-next-line
autoFocus
creatable
createOptionPosition="first"
escapeClearsValue
hideLabel={!label}
isLoading={loading}
label={label ?? 'Add a tag'}
menuPosition={fixedMenu ? 'fixed' : 'absolute'}
onBlur={onClose}
onChange={handleAddTag}
options={tagOptions}
<Autocomplete
onBlur={() => {
if (onClose) {
onClose();
}
}}
onChange={(_, value) => {
if (value) {
handleAddTag(typeof value == 'string' ? value : value.label);
}
}}
renderOption={(props, option) => (
<li {...props}>{option.displayLabel ?? option.label}</li>
)}
disableClearable
forcePopupIcon
label={'Create or Select a Tag'}
loading={accountTagsLoading || loading}
noOptionsText={<i>{`"${inputValue}" already added`}</i>} // Will display create option unless that tag is already added
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',
}),
}));
Loading
Loading