diff --git a/packages/manager/.changeset/pr-9408-tech-stories-1689609217598.md b/packages/manager/.changeset/pr-9408-tech-stories-1689609217598.md new file mode 100644 index 00000000000..918d03e40ad --- /dev/null +++ b/packages/manager/.changeset/pr-9408-tech-stories-1689609217598.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Tech Stories +--- + +MUI v5 migration Src > Features > Help ([#9408](https://github.com/linode/manager/pull/9408)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 18b2b49dfd0..0d4b56d4737 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -162,8 +162,11 @@ const SupportTicketDetail = React.lazy( ); const Longview = React.lazy(() => import('src/features/Longview')); const Managed = React.lazy(() => import('src/features/Managed')); -const Help = React.lazy(() => import('src/features/Help')); - +const Help = React.lazy(() => + import('./features/Help/index').then((module) => ({ + default: module.HelpAndSupport, + })) +); const SearchLanding = React.lazy(() => import('src/features/Search')); const EventsLanding = React.lazy( () => import('src/features/Events/EventsLanding') diff --git a/packages/manager/src/components/H1Header/H1Header.tsx b/packages/manager/src/components/H1Header/H1Header.tsx index be2a9c248db..990de603a58 100644 --- a/packages/manager/src/components/H1Header/H1Header.tsx +++ b/packages/manager/src/components/H1Header/H1Header.tsx @@ -18,12 +18,6 @@ export const H1Header = (props: H1HeaderProps) => { const h1Header = React.useRef(null); const { className, dataQaEl, renderAsSecondary, sx, title } = props; - // React.useEffect(() => { - // if (h1Header.current !== null) { - // h1Header.current.focus(); - // } - // }, []); - return ( { it('only contains a "Managed" menu link if the user has Managed services.', async () => { @@ -29,7 +30,7 @@ describe('PrimaryNav', () => { queryByTestId, rerender, } = renderWithTheme(, { queryClient }); - expect(queryByTestId('menu-item-Managed')).not.toBeInTheDocument(); + expect(queryByTestId(queryString)).not.toBeInTheDocument(); server.use( rest.get('*/account/maintenance', (req, res, ctx) => { @@ -39,9 +40,9 @@ describe('PrimaryNav', () => { rerender(wrapWithTheme(, { queryClient })); - await findByTestId('menu-item-Managed'); + await findByTestId(queryString); - getByTestId('menu-item-Managed'); + getByTestId(queryString); }); it('should have aria-current attribute for accessible links', () => { @@ -49,8 +50,6 @@ describe('PrimaryNav', () => { queryClient, }); - expect(getByTestId('menu-item-Managed').getAttribute('aria-current')).toBe( - 'false' - ); + expect(getByTestId(queryString).getAttribute('aria-current')).toBe('false'); }); }); diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 3311a74a5dd..0e64b7a6e89 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -248,6 +248,7 @@ export const PrimaryNav = (props: Props) => { }, ], ], + // eslint-disable-next-line react-hooks/exhaustive-deps [ showDatabases, _isManagedAccount, diff --git a/packages/manager/src/features/Help/HelpLanding.test.tsx b/packages/manager/src/features/Help/HelpLanding.test.tsx index d433dbece7d..fad4cd0a56d 100644 --- a/packages/manager/src/features/Help/HelpLanding.test.tsx +++ b/packages/manager/src/features/Help/HelpLanding.test.tsx @@ -6,7 +6,7 @@ import { HelpLanding } from './HelpLanding'; describe('Help Landing', () => { const component = shallow(); xit('should render search panel', () => { - expect(component.find('WithStyles(SearchPanel)')).toHaveLength(1); + expect(component.find('SearchPanel')).toHaveLength(1); }); it('should render popular posts panel', () => { @@ -14,6 +14,6 @@ describe('Help Landing', () => { }); it('should render other ways panel', () => { - expect(component.find('WithStyles(OtherWays)')).toHaveLength(1); + expect(component.find('OtherWays')).toHaveLength(1); }); }); diff --git a/packages/manager/src/features/Help/HelpLanding.tsx b/packages/manager/src/features/Help/HelpLanding.tsx index 3cf65544653..20428f2130a 100644 --- a/packages/manager/src/features/Help/HelpLanding.tsx +++ b/packages/manager/src/features/Help/HelpLanding.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import OtherWays from './Panels/OtherWays'; -import PopularPosts from './Panels/PopularPosts'; +import { OtherWays } from './Panels/OtherWays'; +import { PopularPosts } from './Panels/PopularPosts'; import { SearchPanel } from './Panels/SearchPanel'; export const HelpLanding = () => { diff --git a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx index 31d638341ba..4cd7956c246 100644 --- a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx +++ b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx @@ -1,195 +1,144 @@ import Search from '@mui/icons-material/Search'; import { Theme } from '@mui/material/styles'; -import { WithStyles, WithTheme, createStyles, withStyles } from '@mui/styles'; import { pathOr } from 'ramda'; import * as React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { compose } from 'recompose'; +import { debounce } from 'throttle-debounce'; +import { makeStyles } from 'tss-react/mui'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect'; import { Notice } from 'src/components/Notice/Notice'; import { selectStyles } from 'src/features/TopMenu/SearchBar'; -import windowIsNarrowerThan from 'src/utilities/breakpoints'; import withSearch, { AlgoliaState as AlgoliaProps } from '../SearchHOC'; -import SearchItem from './SearchItem'; +import { SearchItem } from './SearchItem'; -type ClassNames = - | 'enhancedSelectWrapper' - | 'notice' - | 'root' - | 'searchIcon' - | 'searchItem'; - -const styles = (theme: Theme) => - createStyles({ - enhancedSelectWrapper: { - '& .input': { - '& > div': { - marginRight: 0, - }, - '& p': { - color: theme.color.grey1, - paddingLeft: theme.spacing(3), - }, - maxWidth: '100%', - }, - '& .react-select__value-container': { - paddingLeft: theme.spacing(4), - }, - margin: '0 auto', - maxHeight: 500, - [theme.breakpoints.up('md')]: { - width: 500, +const useStyles = makeStyles()((theme: Theme) => ({ + enhancedSelectWrapper: { + '& .input': { + '& > div': { + marginRight: 0, }, - width: 300, - }, - notice: { '& p': { - color: theme.color.white, - fontFamily: 'LatoWeb', + color: theme.color.grey1, + paddingLeft: theme.spacing(3), }, + maxWidth: '100%', }, - root: { - position: 'relative', + '& .react-select__value-container': { + paddingLeft: theme.spacing(4), }, - searchIcon: { - color: theme.color.grey1, - left: 5, - position: 'absolute', - top: 4, - zIndex: 3, + margin: '0 auto', + maxHeight: 500, + [theme.breakpoints.up('md')]: { + width: 500, }, - searchItem: { - '& em': { - color: theme.palette.primary.main, - fontStyle: 'normal', - }, + width: 300, + }, + notice: { + '& p': { + color: theme.color.white, + fontFamily: 'LatoWeb', }, - }); + }, + root: { + position: 'relative', + }, + searchIcon: { + color: theme.color.grey1, + left: 5, + position: 'absolute', + top: 4, + zIndex: 3, + }, +})); + +type CombinedProps = AlgoliaProps & RouteComponentProps<{}>; + +const AlgoliaSearchBar = (props: CombinedProps) => { + const { classes } = useStyles(); + const [inputValue, setInputValue] = React.useState(''); + const { + history, + searchAlgolia, + searchEnabled, + searchError, + searchResults, + } = props; + + const options = React.useMemo(() => { + const [docs, community] = searchResults; + const mergedOptions = [...docs, ...community]; -interface State { - inputValue: string; -} + return [ + { data: { source: 'finalLink' }, label: inputValue, value: 'search' }, + ...mergedOptions, + ]; + }, [inputValue, searchResults]); -type CombinedProps = AlgoliaProps & - WithStyles & - WithTheme & - RouteComponentProps<{}>; -class AlgoliaSearchBar extends React.Component { - componentDidMount() { - const { theme } = this.props; - this.mounted = true; - if (theme) { - this.isMobile = windowIsNarrowerThan(theme.breakpoints.values.sm); - } - } - componentWillUnmount() { - this.mounted = false; - } - render() { - const { classes, searchEnabled, searchError } = this.props; - const { inputValue } = this.state; - const options = this.getOptionsFromResults(); + const onInputValueChange = (inputValue: string) => { + setInputValue(inputValue); + debouncedSearchAlgolia(inputValue); + }; + + const debouncedSearchAlgolia = React.useCallback( + debounce(200, false, (inputValue: string) => { + searchAlgolia(inputValue); + }), + [searchAlgolia] + ); - return ( - - {searchError && ( - - {searchError} - - )} -
- - null, Option: SearchItem } as any - } - className={classes.enhancedSelectWrapper} - disabled={!searchEnabled} - hideLabel - inputValue={inputValue} - isClearable={true} - isMulti={false} - label="Search for answers" - onChange={this.handleSelect} - onInputChange={this.onInputValueChange} - options={options} - placeholder="Search for answers..." - styles={selectStyles} - /> -
-
- ); - } - getLinkTarget = (inputValue: string) => { + const getLinkTarget = (inputValue: string) => { return inputValue ? `/support/search/?query=${inputValue}` : '/support/search/'; }; - getOptionsFromResults = () => { - const [docs, community] = this.props.searchResults; - const { inputValue } = this.state; - const options = [...docs, ...community]; - return [ - { data: { source: 'finalLink' }, label: inputValue, value: 'search' }, - ...options, - ]; - }; - - handleSelect = (selected: Item) => { - if (!selected) { + const handleSelect = (selected: Item) => { + if (!selected || !inputValue) { return; } - const { history } = this.props; - const { inputValue } = this.state; - if (!inputValue) { - return; - } - const href = pathOr('', ['data', 'href'], selected); + if (selected.value === 'search') { - const link = this.getLinkTarget(inputValue); + const link = getLinkTarget(inputValue); history.push(link); } else { + const href = pathOr('', ['data', 'href'], selected); window.open(href, '_blank', 'noopener'); } }; - handleSubmit = () => { - const { inputValue } = this.state; - if (!inputValue) { - return; - } - const { history } = this.props; - const link = this.getLinkTarget(inputValue); - history.push(link); - }; - - isMobile: boolean = false; - - mounted: boolean = false; - - onInputValueChange = (inputValue: string) => { - if (!this.mounted) { - return; - } - this.setState({ inputValue }); - this.props.searchAlgolia(inputValue); - }; - - searchIndex: any = null; - - state: State = { - inputValue: '', - }; -} - -const styled = withStyles(styles, { withTheme: true }); -const search = withSearch({ highlight: false, hitsPerPage: 10 }); - -export default compose( - styled, - search, - withRouter -)(AlgoliaSearchBar); + return ( + + {searchError && ( + + {searchError} + + )} +
+ + null, Option: SearchItem } as any + } + className={classes.enhancedSelectWrapper} + disabled={!searchEnabled} + hideLabel + inputValue={inputValue} + isClearable={false} + isMulti={false} + label="Search for answers" + onChange={handleSelect} + onInputChange={onInputValueChange} + options={options} + placeholder="Search for answers..." + styles={selectStyles} + /> +
+
+ ); +}; + +export default withSearch({ highlight: true, hitsPerPage: 10 })( + withRouter(AlgoliaSearchBar) +); diff --git a/packages/manager/src/features/Help/Panels/OtherWays.tsx b/packages/manager/src/features/Help/Panels/OtherWays.tsx index c8fa673a6e9..17b7bf7c433 100644 --- a/packages/manager/src/features/Help/Panels/OtherWays.tsx +++ b/packages/manager/src/features/Help/Panels/OtherWays.tsx @@ -1,6 +1,5 @@ import Grid from '@mui/material/Unstable_Grid2'; -import { Theme } from '@mui/material/styles'; -import { WithStyles, createStyles, withStyles } from '@mui/styles'; +import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import Community from 'src/assets/icons/community.svg'; @@ -10,77 +9,54 @@ import Support from 'src/assets/icons/support.svg'; import { Tile } from 'src/components/Tile/Tile'; import { Typography } from 'src/components/Typography'; -type ClassNames = 'heading' | 'root' | 'wrapper'; - -const styles = (theme: Theme) => - createStyles({ - heading: { - marginBottom: theme.spacing(4), - textAlign: 'center', - }, - root: {}, - wrapper: { - marginTop: theme.spacing(2), - }, - }); - -interface State { - error?: string; -} - -type CombinedProps = WithStyles; - -export class OtherWays extends React.Component { - render() { - const { classes } = this.props; - - return ( - - - Other Ways to Get Help - - - - } - link="https://linode.com/docs/" - title="Guides and Tutorials" - /> - - - } - link="https://linode.com/community/questions" - title="Community Q&A" - /> - - - } - link="https://status.linode.com" - title="Linode Status Page" - /> - - - } - link="/support/tickets" - title="Customer Support" - /> - +export const OtherWays = () => { + const theme = useTheme(); + + return ( + + + Other Ways to Get Help + + + + } + link="https://linode.com/docs/" + title="Guides and Tutorials" + /> - - ); - } - - state: State = {}; -} - -const styled = withStyles(styles); - -export default styled(OtherWays); + + } + link="https://linode.com/community/questions" + title="Community Q&A" + /> + + + } + link="https://status.linode.com" + title="Linode Status Page" + /> + + + } + link="/support/tickets" + title="Customer Support" + /> + + + + ); +}; diff --git a/packages/manager/src/features/Help/Panels/PopularPosts.tsx b/packages/manager/src/features/Help/Panels/PopularPosts.tsx index 1fb566e6f69..fcd469d1c70 100644 --- a/packages/manager/src/features/Help/Panels/PopularPosts.tsx +++ b/packages/manager/src/features/Help/Panels/PopularPosts.tsx @@ -4,8 +4,8 @@ import { makeStyles } from '@mui/styles'; import * as React from 'react'; import { Link } from 'src/components/Link'; -import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; +import { Typography } from 'src/components/Typography'; const useStyles = makeStyles((theme: Theme) => ({ post: { @@ -30,7 +30,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -const PopularPosts: React.FC = () => { +export const PopularPosts = () => { const classes = useStyles(); const renderPopularDocs = () => { @@ -101,5 +101,3 @@ const PopularPosts: React.FC = () => { ); }; - -export default PopularPosts; diff --git a/packages/manager/src/features/Help/Panels/SearchItem.tsx b/packages/manager/src/features/Help/Panels/SearchItem.tsx index 1259027bf57..4a91b2bf30d 100644 --- a/packages/manager/src/features/Help/Panels/SearchItem.tsx +++ b/packages/manager/src/features/Help/Panels/SearchItem.tsx @@ -15,7 +15,7 @@ interface Props extends OptionProps { searchText: string; } -const SearchItem: React.FC = (props) => { +export const SearchItem = (props: Props) => { const getLabel = () => { if (isFinal) { return props.label ? `Search for "${props.label}"` : 'Search'; @@ -62,5 +62,3 @@ const SearchItem: React.FC = (props) => { ); }; - -export default SearchItem; diff --git a/packages/manager/src/features/Help/Panels/SearchPanel.tsx b/packages/manager/src/features/Help/Panels/SearchPanel.tsx index 154d8383396..3a2150403b3 100644 --- a/packages/manager/src/features/Help/Panels/SearchPanel.tsx +++ b/packages/manager/src/features/Help/Panels/SearchPanel.tsx @@ -36,7 +36,7 @@ const StyledRootContainer = styled(Paper, { const StyledH1Header = styled(H1Header, { label: 'StyledH1Header', })(({ theme }) => ({ - color: theme.color.white, + color: theme.name === 'dark' ? theme.color.black : theme.color.white, marginBottom: theme.spacing(), position: 'relative', textAlign: 'center', diff --git a/packages/manager/src/features/Help/SearchHOC.tsx b/packages/manager/src/features/Help/SearchHOC.tsx index dfb06255df4..ec2bfffcc56 100644 --- a/packages/manager/src/features/Help/SearchHOC.tsx +++ b/packages/manager/src/features/Help/SearchHOC.tsx @@ -37,7 +37,6 @@ interface AlgoliaContent { } // Functional helper methods - export const convertDocsToItems = ( highlight: boolean, hits: SearchHit[] = [] @@ -221,5 +220,6 @@ export default (options: SearchOptions) => ( searchResults: [[], []], }; } + return WrappedComponent; }; diff --git a/packages/manager/src/features/Help/StatusBanners.test.tsx b/packages/manager/src/features/Help/StatusBanners.test.tsx index 6d442663c8f..54768bf999b 100644 --- a/packages/manager/src/features/Help/StatusBanners.test.tsx +++ b/packages/manager/src/features/Help/StatusBanners.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import StatusBanners, { IncidentBanner, IncidentProps } from './StatusBanners'; +import { IncidentBanner, IncidentProps, StatusBanners } from './StatusBanners'; const props: IncidentProps = { href: 'https://www.example.com', diff --git a/packages/manager/src/features/Help/StatusBanners.tsx b/packages/manager/src/features/Help/StatusBanners.tsx index f3a70eaf524..302740f9e09 100644 --- a/packages/manager/src/features/Help/StatusBanners.tsx +++ b/packages/manager/src/features/Help/StatusBanners.tsx @@ -1,6 +1,5 @@ import Box from '@mui/material/Box'; -import { Theme } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; +import { useTheme } from '@mui/material/styles'; import { DateTime } from 'luxon'; import * as React from 'react'; @@ -16,25 +15,7 @@ import { capitalize } from 'src/utilities/capitalize'; import { sanitizeHTML } from 'src/utilities/sanitize-html'; import { truncateEnd } from 'src/utilities/truncate'; -const useStyles = makeStyles((theme: Theme) => ({ - button: { - ...theme.applyLinkStyles, - display: 'flex', - }, - header: { - fontSize: '1rem', - marginBottom: theme.spacing(), - }, - root: { - marginBottom: theme.spacing(), - }, - text: { - fontSize: '0.875rem', - lineHeight: '1.25rem', - }, -})); - -export const StatusBanners: React.FC<{}> = (_) => { +export const StatusBanners = () => { const { data: incidentsData } = useIncidentQuery(); const incidents = incidentsData?.incidents ?? []; @@ -73,10 +54,10 @@ export interface IncidentProps { title: string; } -export const IncidentBanner: React.FC = React.memo((props) => { +export const IncidentBanner = React.memo((props: IncidentProps) => { const { href, impact, message, status: _status, title } = props; const status = _status ?? ''; - const classes = useStyles(); + const theme = useTheme(); const preferenceKey = `${href}-${status}`; @@ -93,12 +74,18 @@ export const IncidentBanner: React.FC = React.memo((props) => { ['major', 'minor', 'none'].includes(impact) || ['monitoring', 'resolved'].includes(status) } - className={classes.root} important preferenceKey={preferenceKey} + sx={{ marginBottom: theme.spacing() }} > - + {title} @@ -110,11 +97,12 @@ export const IncidentBanner: React.FC = React.memo((props) => { dangerouslySetInnerHTML={{ __html: sanitizeHTML(truncateEnd(message, 500)), }} - className={classes.text} + sx={{ + fontSize: '0.875rem', + lineHeight: '1.25rem', + }} /> ); }); - -export default React.memo(StatusBanners); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/DocumentationResults.tsx b/packages/manager/src/features/Help/SupportSearchLanding/DocumentationResults.tsx index e7fea290c2f..4f57694aeca 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/DocumentationResults.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/DocumentationResults.tsx @@ -1,13 +1,13 @@ import { Theme } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; import { Link } from 'src/components/Link'; import { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; import ListItem from 'src/components/core/ListItem'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ header: { marginBottom: theme.spacing(2), marginTop: theme.spacing(3), @@ -52,10 +52,8 @@ interface Props { target: string; } -type CombinedProps = Props; - -const DocumentationResults: React.FC = (props) => { - const classes = useStyles(); +export const DocumentationResults = (props: Props) => { + const { classes } = useStyles(); const { results, sectionTitle, target } = props; const renderResults = () => { @@ -72,10 +70,15 @@ const DocumentationResults: React.FC = (props) => { )); }; - const renderEmptyState = () => { + const renderEmptyState = (): JSX.Element => { return ( - No results + + No results + ); }; @@ -100,5 +103,3 @@ const DocumentationResults: React.FC = (props) => { ); }; - -export default DocumentationResults; diff --git a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx index ecfd73f3f85..95045c82275 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx @@ -1,9 +1,9 @@ import Grid from '@mui/material/Unstable_Grid2'; import { Theme } from '@mui/material/styles'; -import { WithStyles, createStyles, withStyles } from '@mui/styles'; import { compose } from 'ramda'; import * as React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { makeStyles } from 'tss-react/mui'; import Community from 'src/assets/icons/community.svg'; import Support from 'src/assets/icons/support.svg'; @@ -12,132 +12,89 @@ import { Typography } from 'src/components/Typography'; import { AttachmentError } from 'src/features/Support/SupportTicketDetail/SupportTicketDetail'; import { SupportTicketDialog } from 'src/features/Support/SupportTickets/SupportTicketDialog'; -type ClassNames = - | 'card' - | 'heading' - | 'icon' - | 'root' - | 'tileTitle' - | 'wrapper'; +const useStyles = makeStyles()((theme: Theme) => ({ + heading: { + marginBottom: theme.spacing(1), + textAlign: 'center', + }, + icon: { + border: `2px solid ${theme.palette.divider}`, + borderRadius: '50%', + color: theme.palette.primary.main, + display: 'block', + height: 66, + margin: '0 auto 16px', + padding: 16, + width: 66, + }, + wrapper: { + marginTop: theme.spacing(4), + }, +})); -const styles = (theme: Theme) => - createStyles({ - card: { - alignItems: 'center', - backgroundColor: theme.color.white, - border: `1px solid ${theme.color.grey2}`, - display: 'flex', - flexDirection: 'column', - height: '100%', - padding: theme.spacing(4), - }, - heading: { - marginBottom: theme.spacing(1), - textAlign: 'center', - }, - icon: { - border: `2px solid ${theme.palette.divider}`, - borderRadius: '50%', - color: theme.palette.primary.main, - display: 'block', - height: 66, - margin: '0 auto 16px', - padding: 16, - width: 66, - }, - root: {}, - tileTitle: { - fontSize: '1.2rem', - marginBottom: theme.spacing(1), - marginTop: theme.spacing(1), - }, - wrapper: { - marginTop: theme.spacing(4), - }, - }); - -interface State { - drawerOpen: boolean; - error?: string; -} - -type CombinedProps = RouteComponentProps<{}> & WithStyles; - -export class OtherWays extends React.Component { - render() { - const { classes } = this.props; - const { drawerOpen } = this.state; - - return ( - <> - - - - Didn’t find what you need? Get help. - - - - - } - link="https://linode.com/community/" - title="Create a Community Post" - /> - - - } - link={this.openTicketDrawer} - title="Open a ticket" - /> - - - - - - ); - } +export const HelpResources = (props: RouteComponentProps) => { + const { classes } = useStyles(); + const [drawerOpen, setDrawerOpen] = React.useState(false); + const openTicketDrawer = () => { + setDrawerOpen(true); + }; - closeTicketDrawer = () => { - this.setState({ drawerOpen: false }); + const closeTicketDrawer = () => { + setDrawerOpen(false); }; - onTicketCreated = ( + const onTicketCreated = ( ticketId: number, attachmentErrors: AttachmentError[] = [] ) => { - const { history } = this.props; + const { history } = props; history.push({ pathname: `/support/tickets/${ticketId}`, state: { attachmentErrors }, }); - this.setState({ - drawerOpen: false, - }); + setDrawerOpen(false); }; - openTicketDrawer = () => { - this.setState({ drawerOpen: true }); - }; - - state: State = { - drawerOpen: false, - }; -} - -const styled = withStyles(styles); - -export default compose(styled, withRouter)(OtherWays); + return ( + <> + + + + Didn’t find what you need? Get help. + + + + + } + link="https://linode.com/community/" + title="Create a Community Post" + /> + + + } + link={openTicketDrawer} + title="Open a ticket" + /> + + + + + + ); +}; +export default compose(withRouter)(HelpResources); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx index 5a54526de33..f0938bad6b3 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx @@ -1,24 +1,14 @@ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/react'; import { assocPath } from 'ramda'; import * as React from 'react'; import { reactRouterProps } from 'src/__data__/reactRouterProps'; import { H1Header } from 'src/components/H1Header/H1Header'; +import { renderWithTheme } from 'src/utilities/testHelpers'; -import { CombinedProps, SupportSearchLanding } from './SupportSearchLanding'; - -const classes = { - backButton: '', - root: '', - searchBar: '', - searchBoxInner: '', - searchField: '', - searchHeading: '', - searchIcon: '', -}; +import SupportSearchLanding, { CombinedProps } from './SupportSearchLanding'; const props: CombinedProps = { - classes, searchAlgolia: jest.fn(), searchEnabled: true, searchResults: [[], []], @@ -27,51 +17,41 @@ const props: CombinedProps = { const propsWithMultiWordURLQuery = assocPath( ['location', 'search'], - '?query=search%20two%20words', + '?query=two%20words', props ); -const component = shallow( - -); -// Query is read on mount so we have to mount twice. -const component2 = shallow( - -); -describe('Component', () => { +describe('SupportSearchLanding Component', () => { it('should render', () => { - expect(component).toBeDefined(); + renderWithTheme(); + expect(screen.getByTestId('support-search-landing')).toBeInTheDocument(); }); - it('should set the query from the URL param to state', () => { - expect(component.state().query).toMatch('search'); + + it('should display generic text if no query string is provided', () => { + renderWithTheme(); + expect(screen.getByText('Search')).toBeInTheDocument(); }); - it('should read multi-word queries correctly', () => { - expect(component2.state().query).toMatch('search two words'); + + it('should display query string in the header', () => { + renderWithTheme(); + expect(screen.getByText(props.location.search)).toBeInTheDocument(); }); - it('should display the query text in the header', () => { + + it('should display multi-word query string in the header', () => { + renderWithTheme( + + ); expect( - component.containsMatchingElement( - - ) - ).toBeTruthy(); + screen.getByText(propsWithMultiWordURLQuery.location.search) + ).toBeInTheDocument(); }); - it('should display generic text if no query is provided', () => { - component.setState({ query: '' }); - expect( - component.containsMatchingElement( - - ) - ).toBeTruthy(); + + it('should display empty DocumentationResults components with empty query string', () => { + const newProps = assocPath(['location', 'search'], '?query=', props); + + renderWithTheme(); expect( - component.containsMatchingElement( - - ) - ).toBeFalsy(); + screen.getAllByTestId('data-qa-documentation-no-results') + ).toHaveLength(2); }); }); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx index c2715847ebe..67908aed7ff 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx @@ -2,10 +2,9 @@ import Search from '@mui/icons-material/Search'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Unstable_Grid2'; import { Theme } from '@mui/material/styles'; -import { WithStyles, createStyles, withStyles } from '@mui/styles'; -import { compose } from 'ramda'; import * as React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { makeStyles } from 'tss-react/mui'; import { H1Header } from 'src/components/H1Header/H1Header'; import { Notice } from 'src/components/Notice/Notice'; @@ -15,163 +14,118 @@ import { COMMUNITY_SEARCH_URL, DOCS_SEARCH_URL } from 'src/constants'; import { getQueryParamFromQueryString } from 'src/utilities/queryParams'; import withSearch, { AlgoliaState as AlgoliaProps } from '../SearchHOC'; -import DocumentationResults, { SearchResult } from './DocumentationResults'; +import { DocumentationResults, SearchResult } from './DocumentationResults'; import HelpResources from './HelpResources'; -type ClassNames = - | 'root' - | 'searchBar' - | 'searchBoxInner' - | 'searchField' - | 'searchHeading' - | 'searchIcon'; - -const styles = (theme: Theme) => - createStyles({ - root: { - alignItems: 'center', - display: 'flex', - justifyContent: 'flex-start', - maxWidth: '100%', - position: 'relative', - }, - searchBar: { +const useStyles = makeStyles()((theme: Theme) => ({ + searchBar: { + maxWidth: '100%', + }, + searchBoxInner: { + '& > div': { maxWidth: '100%', }, - searchBoxInner: { - '& > div': { - maxWidth: '100%', - }, - backgroundColor: theme.color.grey2, - marginTop: 0, - padding: theme.spacing(3), - }, - searchField: { - padding: theme.spacing(3), - }, - searchHeading: { - color: theme.color.black, - fontSize: '175%', - marginBottom: theme.spacing(2), + backgroundColor: theme.color.grey2, + marginTop: 0, + padding: theme.spacing(3), + }, + searchIcon: { + '& svg': { + color: theme.palette.text.primary, }, - searchIcon: { - '& svg': { - color: theme.palette.text.primary, - }, - marginRight: 0, - }, - }); - -interface State { - query: string; -} - -export type CombinedProps = AlgoliaProps & - WithStyles & - RouteComponentProps<{}>; - -export class SupportSearchLanding extends React.Component< - CombinedProps, - State -> { - componentDidMount() { - this.searchFromParams(); - } - componentDidUpdate(prevProps: CombinedProps) { - if (!prevProps.searchEnabled && this.props.searchEnabled) { - this.searchFromParams(); - } - } - - render() { - const { classes, searchEnabled, searchError, searchResults } = this.props; - const { query } = this.state; - - const [docs, community] = searchResults; - - return ( - - - 1 ? `Search results for "${query}"` : 'Search' - } - data-qa-support-search-landing-title - /> - - - {searchError && {searchError}} - - - - ), - }} - className={classes.searchBoxInner} - data-qa-search-landing-input - disabled={!Boolean(searchEnabled)} - hideLabel - label="Search Linode documentation and community questions" - onChange={this.onInputChange} - placeholder="Search Linode documentation and community questions" - value={query} - /> - - - - - - - - - - ); - } - - searchFromParams() { - const query = getQueryParamFromQueryString( - this.props.location.search, - 'query' - ); - this.setState({ query }); - this.props.searchAlgolia(query); - } - - onInputChange = (e: React.ChangeEvent) => { - const newQuery = e.target.value ?? ''; - this.setState({ query: newQuery }); - this.props.history.replace({ search: `?query=${newQuery}` }); - this.props.searchAlgolia(newQuery); + marginRight: 0, + }, +})); + +export type CombinedProps = AlgoliaProps & RouteComponentProps<{}>; + +const SupportSearchLanding = (props: CombinedProps) => { + const { + history, + searchAlgolia, + searchEnabled, + searchError, + searchResults, + } = props; + const [docs, community] = searchResults; + const { classes } = useStyles(); + + const [queryString, setQueryString] = React.useState(''); + + React.useEffect(() => { + searchFromParams(); + }, []); + + const searchFromParams = () => { + const query = getQueryParamFromQueryString(location.search, 'query'); + setQueryString(query); + searchAlgolia(query); }; - searchIndex: any = null; - - state: State = { - query: '', + const onInputChange = (e: React.ChangeEvent) => { + const newQuery = e.target.value ?? ''; + setQueryString(newQuery); + history.replace({ search: `?query=${newQuery}` }); + searchAlgolia(newQuery); }; -} -const styled = withStyles(styles); -const searchable = withSearch({ highlight: false, hitsPerPage: 5 }); -const enhanced: any = compose( - styled, - searchable, - withRouter -)(SupportSearchLanding); - -export default enhanced; + return ( + + + 1 + ? `Search results for "${queryString}"` + : 'Search' + } + data-qa-support-search-landing-title + dataQaEl={queryString} + /> + + + {searchError && {searchError}} + + + + ), + }} + className={classes.searchBoxInner} + data-qa-search-landing-input + disabled={!Boolean(searchEnabled)} + hideLabel + label="Search Linode documentation and community questions" + onChange={onInputChange} + placeholder="Search Linode documentation and community questions" + value={queryString} + /> + + + + + + + + + + ); +}; + +export default withSearch({ highlight: false, hitsPerPage: 5 })( + withRouter(SupportSearchLanding) +); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/index.tsx b/packages/manager/src/features/Help/SupportSearchLanding/index.tsx deleted file mode 100644 index cb15cb5f8f3..00000000000 --- a/packages/manager/src/features/Help/SupportSearchLanding/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import SupportSearchLanding from './SupportSearchLanding'; -export default SupportSearchLanding; diff --git a/packages/manager/src/features/Help/index.tsx b/packages/manager/src/features/Help/index.tsx index d6a1c40d5bf..a8d600f140f 100644 --- a/packages/manager/src/features/Help/index.tsx +++ b/packages/manager/src/features/Help/index.tsx @@ -1,24 +1,25 @@ import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; -import StatusBanners from './StatusBanners'; +import { StatusBanners } from './StatusBanners'; const HelpLanding = React.lazy(() => - import('./HelpLanding').then((module) => ({ - default: module.HelpLanding, - })) + import('./HelpLanding').then((module) => ({ default: module.HelpLanding })) ); + const SupportSearchLanding = React.lazy( - () => import('src/features/Help/SupportSearchLanding') + () => import('src/features/Help/SupportSearchLanding/SupportSearchLanding') ); + const SupportTickets = React.lazy( () => import('src/features/Support/SupportTickets') ); + const SupportTicketDetail = React.lazy( () => import('src/features/Support/SupportTicketDetail') ); -const HelpAndSupport = () => { +export const HelpAndSupport = () => { return ( <> @@ -45,5 +46,3 @@ const HelpAndSupport = () => { ); }; - -export default HelpAndSupport; diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index 720aceb44c7..f3fd61c0a78 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -178,7 +178,6 @@ class UserPermissions extends React.Component { if (updateFns.length) { this.setState((compose as any)(...updateFns)); } - return; } };