mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-19 00:15:43 +01:00
feat: command bar feedback (#7485)
 
This commit is contained in:
parent
2706a09b8b
commit
c907199d23
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
@ -13,7 +13,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
|
import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
|
||||||
import { SEARCH_INPUT } from 'utils/testIds';
|
import { SEARCH_INPUT } from 'utils/testIds';
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside';
|
import { useOnClickOutside } from 'hooks/useOnClickOutside';
|
||||||
import { useOnBlur } from 'hooks/useOnBlur';
|
|
||||||
import {
|
import {
|
||||||
CommandResultGroup,
|
CommandResultGroup,
|
||||||
type CommandResultGroupItem,
|
type CommandResultGroupItem,
|
||||||
@ -26,6 +25,7 @@ import { CommandFeatures } from './CommandFeatures';
|
|||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { CommandRecent } from './CommandRecent';
|
import { CommandRecent } from './CommandRecent';
|
||||||
import { CommandPages } from './CommandPages';
|
import { CommandPages } from './CommandPages';
|
||||||
|
import { CommandBarFeedback } from './CommandBarFeedback';
|
||||||
import { RecentlyVisitedRecorder } from './RecentlyVisitedRecorder';
|
import { RecentlyVisitedRecorder } from './RecentlyVisitedRecorder';
|
||||||
|
|
||||||
export const CommandResultsPaper = styled(Paper)(({ theme }) => ({
|
export const CommandResultsPaper = styled(Paper)(({ theme }) => ({
|
||||||
@ -35,7 +35,7 @@ export const CommandResultsPaper = styled(Paper)(({ theme }) => ({
|
|||||||
top: '39px',
|
top: '39px',
|
||||||
zIndex: 4,
|
zIndex: 4,
|
||||||
borderTop: theme.spacing(0),
|
borderTop: theme.spacing(0),
|
||||||
padding: theme.spacing(4, 0, 1.5),
|
padding: theme.spacing(1.5, 0, 1.5),
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
borderBottomLeftRadius: theme.spacing(1),
|
borderBottomLeftRadius: theme.spacing(1),
|
||||||
borderBottomRightRadius: theme.spacing(1),
|
borderBottomRightRadius: theme.spacing(1),
|
||||||
@ -109,6 +109,7 @@ export const CommandBar = () => {
|
|||||||
CommandResultGroupItem[]
|
CommandResultGroupItem[]
|
||||||
>([]);
|
>([]);
|
||||||
const [searchedFlagCount, setSearchedFlagCount] = useState(0);
|
const [searchedFlagCount, setSearchedFlagCount] = useState(0);
|
||||||
|
const [hasNoResults, setHasNoResults] = useState(false);
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const { routes } = useRoutes();
|
const { routes } = useRoutes();
|
||||||
const allRoutes: Record<string, IPageRouteInfo> = {};
|
const allRoutes: Record<string, IPageRouteInfo> = {};
|
||||||
@ -166,8 +167,13 @@ export const CommandBar = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setHasNoResults(noResultsFound);
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedSetSearchState(value);
|
||||||
|
}, [searchedFlagCount]);
|
||||||
|
|
||||||
const onSearchChange = (value: string) => {
|
const onSearchChange = (value: string) => {
|
||||||
debouncedSetSearchState(value);
|
debouncedSetSearchState(value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
@ -195,8 +201,6 @@ export const CommandBar = () => {
|
|||||||
const placeholder = `Command bar (${hotkey})`;
|
const placeholder = `Command bar (${hotkey})`;
|
||||||
|
|
||||||
useOnClickOutside([searchContainerRef], hideSuggestions);
|
useOnClickOutside([searchContainerRef], hideSuggestions);
|
||||||
useOnBlur(searchContainerRef, hideSuggestions);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer ref={searchContainerRef} active={showSuggestions}>
|
<StyledContainer ref={searchContainerRef} active={showSuggestions}>
|
||||||
<RecentlyVisitedRecorder />
|
<RecentlyVisitedRecorder />
|
||||||
@ -261,6 +265,14 @@ export const CommandBar = () => {
|
|||||||
items={searchedProjects}
|
items={searchedProjects}
|
||||||
/>
|
/>
|
||||||
<CommandPages items={searchedPages} />
|
<CommandPages items={searchedPages} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasNoResults}
|
||||||
|
show={
|
||||||
|
<CommandBarFeedback
|
||||||
|
onSubmit={hideSuggestions}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</CommandResultsPaper>
|
</CommandResultsPaper>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
|
96
frontend/src/component/commandBar/CommandBarFeedback.tsx
Normal file
96
frontend/src/component/commandBar/CommandBarFeedback.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { Button, styled, TextField } from '@mui/material';
|
||||||
|
import type React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useUserFeedbackApi } from 'hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import useUserType from '../feedbackNew/useUserType';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledText = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontSize: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ICommandBarFeedbackProps {
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandBarFeedback = ({ onSubmit }: ICommandBarFeedbackProps) => {
|
||||||
|
const userType = useUserType();
|
||||||
|
const { addFeedback } = useUserFeedbackApi();
|
||||||
|
const { setToastData } = useToast();
|
||||||
|
const [suggesting, setSuggesting] = useState(false);
|
||||||
|
const [feedback, setFeedback] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const changeFeedback = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFeedback(event.target.value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendFeedback = async () => {
|
||||||
|
await addFeedback({
|
||||||
|
areasForImprovement: feedback,
|
||||||
|
category: 'commandBar',
|
||||||
|
userType: userType,
|
||||||
|
});
|
||||||
|
onSubmit();
|
||||||
|
setToastData({
|
||||||
|
title: 'Feedback sent',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={suggesting}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledText>Describe the capability</StyledText>
|
||||||
|
<TextField
|
||||||
|
multiline={true}
|
||||||
|
minRows={2}
|
||||||
|
onChange={changeFeedback}
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
type='submit'
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
onClick={sendFeedback}
|
||||||
|
>
|
||||||
|
Send to Unleash
|
||||||
|
</StyledButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<StyledText>
|
||||||
|
We couldn’t find anything matching your search
|
||||||
|
criteria. If you think this is a missing capability,
|
||||||
|
feel free to make a suggestion.
|
||||||
|
</StyledText>
|
||||||
|
<StyledButton
|
||||||
|
type='submit'
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSuggesting(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suggest capability
|
||||||
|
</StyledButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import { Children } from 'react';
|
||||||
|
|
||||||
export const listItemButtonStyle = (theme: Theme) => ({
|
export const listItemButtonStyle = (theme: Theme) => ({
|
||||||
border: `1px solid transparent`,
|
border: `1px solid transparent`,
|
||||||
@ -26,10 +27,6 @@ export const listItemButtonStyle = (theme: Theme) => ({
|
|||||||
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
|
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledTypography = styled(Typography)(({ theme }) => ({
|
export const StyledTypography = styled(Typography)(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.bodySize,
|
fontSize: theme.fontSizes.bodySize,
|
||||||
padding: theme.spacing(0, 2.5),
|
padding: theme.spacing(0, 2.5),
|
||||||
@ -193,7 +190,10 @@ export const CommandResultGroup = ({
|
|||||||
children,
|
children,
|
||||||
}: CommandResultGroupProps) => {
|
}: CommandResultGroupProps) => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
if (!children && (!items || items.length === 0)) {
|
if (
|
||||||
|
(!children || Children.count(children) === 0) &&
|
||||||
|
(!items || items.length === 0)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ export const CommandResultGroup = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<div>
|
||||||
<StyledTypography color='textSecondary'>
|
<StyledTypography color='textSecondary'>
|
||||||
{groupName}
|
{groupName}
|
||||||
</StyledTypography>
|
</StyledTypography>
|
||||||
@ -249,6 +249,6 @@ export const CommandResultGroup = ({
|
|||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</StyledContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,12 +16,12 @@ import useToast from 'hooks/useToast';
|
|||||||
import type { ProvideFeedbackSchema } from 'openapi';
|
import type { ProvideFeedbackSchema } from 'openapi';
|
||||||
import { useUserFeedbackApi } from 'hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi';
|
import { useUserFeedbackApi } from 'hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi';
|
||||||
import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback';
|
import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import type { IToast } from 'interfaces/toast';
|
import type { IToast } from 'interfaces/toast';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import type { FeedbackData, FeedbackMode } from './FeedbackContext';
|
import type { FeedbackData, FeedbackMode } from './FeedbackContext';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import useUserType from './useUserType';
|
||||||
|
|
||||||
export const ParentContainer = styled('div')(({ theme }) => ({
|
export const ParentContainer = styled('div')(({ theme }) => ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -201,9 +201,10 @@ export const FeedbackComponent = ({
|
|||||||
feedbackMode,
|
feedbackMode,
|
||||||
}: IFeedbackComponent) => {
|
}: IFeedbackComponent) => {
|
||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
|
const userType = useUserType();
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { isPro, isOss, isEnterprise } = useUiConfig();
|
|
||||||
const { addFeedback } = useUserFeedbackApi();
|
const { addFeedback } = useUserFeedbackApi();
|
||||||
const { setHasSubmittedFeedback } = useUserSubmittedFeedback(
|
const { setHasSubmittedFeedback } = useUserSubmittedFeedback(
|
||||||
feedbackData.category,
|
feedbackData.category,
|
||||||
@ -276,22 +277,6 @@ export const FeedbackComponent = ({
|
|||||||
setSelectedScore(event.target.value);
|
setSelectedScore(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserType = () => {
|
|
||||||
if (isPro()) {
|
|
||||||
return 'pro';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOss()) {
|
|
||||||
return 'oss';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEnterprise()) {
|
|
||||||
return 'enterprise';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showFeedback}
|
condition={showFeedback}
|
||||||
@ -320,7 +305,7 @@ export const FeedbackComponent = ({
|
|||||||
<input
|
<input
|
||||||
type='hidden'
|
type='hidden'
|
||||||
name='userType'
|
name='userType'
|
||||||
value={getUserType()}
|
value={userType}
|
||||||
/>
|
/>
|
||||||
<FormTitle>{feedbackData.title}</FormTitle>
|
<FormTitle>{feedbackData.title}</FormTitle>
|
||||||
<StyledScoreContainer>
|
<StyledScoreContainer>
|
||||||
|
21
frontend/src/component/feedbackNew/useUserType.ts
Normal file
21
frontend/src/component/feedbackNew/useUserType.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
|
const useUserType = () => {
|
||||||
|
const { isPro, isOss, isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
|
if (isPro()) {
|
||||||
|
return 'pro';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOss()) {
|
||||||
|
return 'oss';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnterprise()) {
|
||||||
|
return 'enterprise';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserType;
|
Loading…
Reference in New Issue
Block a user