1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

Feat/add feedback to new strategy form (#5745)

This PR adds the feedback form to the new create / edit strategy form
behind a feature flag.

* Add feedback form
* Minor refactor to useFeedback
This commit is contained in:
Fredrik Strand Oseberg 2024-01-03 15:43:22 +01:00 committed by GitHub
parent 31124e4a90
commit 70600552d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 109 additions and 44 deletions

View File

@ -44,6 +44,9 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { formatStrategyName } from 'utils/strategyNames'; import { formatStrategyName } from 'utils/strategyNames';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import { useFeedback } from 'component/feedbackNew/useFeedback';
import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback';
import { useUiFlag } from 'hooks/useUiFlag';
interface IFeatureStrategyFormProps { interface IFeatureStrategyFormProps {
feature: IFeatureToggle; feature: IFeatureToggle;
@ -167,6 +170,8 @@ const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
})); }));
const feedbackCategory = 'newStrategyForm';
export const NewFeatureStrategyForm = ({ export const NewFeatureStrategyForm = ({
projectId, projectId,
feature, feature,
@ -185,6 +190,8 @@ export const NewFeatureStrategyForm = ({
setTab, setTab,
StrategyVariants, StrategyVariants,
}: IFeatureStrategyFormProps) => { }: IFeatureStrategyFormProps) => {
const { openFeedback, hasSubmittedFeedback } =
useFeedback(feedbackCategory);
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const [showProdGuard, setShowProdGuard] = useState(false); const [showProdGuard, setShowProdGuard] = useState(false);
const hasValidConstraints = useConstraintsValidation(strategy.constraints); const hasValidConstraints = useConstraintsValidation(strategy.constraints);
@ -195,6 +202,9 @@ export const NewFeatureStrategyForm = ({
environmentId, environmentId,
); );
const { strategyDefinition } = useStrategy(strategy?.name); const { strategyDefinition } = useStrategy(strategy?.name);
const newStrategyConfigurationFeedback = useUiFlag(
'newStrategyConfigurationFeedback',
);
useEffect(() => { useEffect(() => {
trackEvent('new-strategy-form', { trackEvent('new-strategy-form', {
@ -265,6 +275,15 @@ export const NewFeatureStrategyForm = ({
navigate(formatFeaturePath(feature.project, feature.name)); navigate(formatFeaturePath(feature.project, feature.name));
}; };
const createFeedbackContext = () => {
openFeedback({
title: 'How easy was it to work with the new strategy form?',
positiveLabel: 'What do you like most about the new strategy form?',
areasForImprovementsLabel:
'What should be improved the new strategy form?',
});
};
const onSubmitWithValidation = async (event: React.FormEvent) => { const onSubmitWithValidation = async (event: React.FormEvent) => {
if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) { if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) {
trackEvent('strategy-variants', { trackEvent('strategy-variants', {
@ -287,7 +306,15 @@ export const NewFeatureStrategyForm = ({
if (enableProdGuard && !isChangeRequest) { if (enableProdGuard && !isChangeRequest) {
setShowProdGuard(true); setShowProdGuard(true);
} else { } else {
onSubmit(); await onSubmitWithFeedback();
}
};
const onSubmitWithFeedback = async () => {
await onSubmit();
if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) {
createFeedbackContext();
} }
}; };
@ -488,7 +515,7 @@ export const NewFeatureStrategyForm = ({
<FeatureStrategyProdGuard <FeatureStrategyProdGuard
open={showProdGuard} open={showProdGuard}
onClose={() => setShowProdGuard(false)} onClose={() => setShowProdGuard(false)}
onClick={onSubmit} onClick={onSubmitWithFeedback}
loading={loading} loading={loading}
label='Save strategy' label='Save strategy'
/> />

View File

@ -71,9 +71,8 @@ const feedbackCategory = 'search';
const FeatureToggleListTableComponent: VFC = () => { const FeatureToggleListTableComponent: VFC = () => {
const theme = useTheme(); const theme = useTheme();
const { openFeedback } = useFeedback(); const { openFeedback, hasSubmittedFeedback } =
const { hasSubmittedFeedback, setHasSubmittedFeedback } = useFeedback(feedbackCategory);
useUserSubmittedFeedback(feedbackCategory);
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const { environments } = useEnvironments(); const { environments } = useEnvironments();
const enabledEnvironments = environments const enabledEnvironments = environments
@ -276,7 +275,6 @@ const FeatureToggleListTableComponent: VFC = () => {
const createFeedbackContext = () => { const createFeedbackContext = () => {
openFeedback({ openFeedback({
category: feedbackCategory,
title: 'How easy was it to use search and filters?', title: 'How easy was it to use search and filters?',
positiveLabel: 'What do you like most about search and filters?', positiveLabel: 'What do you like most about search and filters?',
areasForImprovementsLabel: areasForImprovementsLabel:

View File

@ -7,7 +7,7 @@ import {
Tooltip, Tooltip,
} from '@mui/material'; } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFeedback } from './useFeedback'; import { useFeedbackContext } from './useFeedback';
import React, { useState } from 'react'; import React, { useState } from 'react';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
@ -93,6 +93,7 @@ export const FormSubTitle = styled(Box)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontSize: theme.spacing(1.75), fontSize: theme.spacing(1.75),
lineHeight: theme.spacing(2.5), lineHeight: theme.spacing(2.5),
marginBottom: theme.spacing(0.5),
})); }));
export const StyledButton = styled(Button)(() => ({ export const StyledButton = styled(Button)(() => ({
@ -160,7 +161,7 @@ const StyledCloseButton = styled(IconButton)(({ theme }) => ({
})); }));
export const FeedbackComponentWrapper = () => { export const FeedbackComponentWrapper = () => {
const { feedbackData, showFeedback, closeFeedback } = useFeedback(); const { feedbackData, showFeedback, closeFeedback } = useFeedbackContext();
if (!feedbackData) return null; if (!feedbackData) return null;
@ -309,7 +310,7 @@ export const FeedbackComponent = ({
{feedbackData.positiveLabel} {feedbackData.positiveLabel}
</FormSubTitle> </FormSubTitle>
<TextField <TextField
label='Your answer here' placeholder='Your answer here'
style={{ width: '100%' }} style={{ width: '100%' }}
name='positive' name='positive'
multiline multiline
@ -329,7 +330,7 @@ export const FeedbackComponent = ({
{feedbackData.areasForImprovementsLabel} {feedbackData.areasForImprovementsLabel}
</FormSubTitle> </FormSubTitle>
<TextField <TextField
label='Your answer here' placeholder='Your answer here'
style={{ width: '100%' }} style={{ width: '100%' }}
multiline multiline
name='areasForImprovement' name='areasForImprovement'
@ -344,18 +345,15 @@ export const FeedbackComponent = ({
size='small' size='small'
/> />
</Box> </Box>
<ConditionallyRender
condition={Boolean(selectedScore)}
show={
<StyledButton <StyledButton
disabled={!selectedScore}
variant='contained' variant='contained'
color='primary' color='primary'
type='submit' type='submit'
> >
Send Feedback Send Feedback
</StyledButton> </StyledButton>
}
/>
</StyledForm> </StyledForm>
</StyledContent> </StyledContent>
</StyledContainer> </StyledContainer>

View File

@ -1,8 +1,7 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { ProvideFeedbackSchema } from '../../openapi';
import { IFeedbackCategory } from 'hooks/useSubmittedFeedback'; import { IFeedbackCategory } from 'hooks/useSubmittedFeedback';
interface IFeedbackContext { export interface IFeedbackContext {
feedbackData: FeedbackData | undefined; feedbackData: FeedbackData | undefined;
openFeedback: (data: FeedbackData) => void; openFeedback: (data: FeedbackData) => void;
closeFeedback: () => void; closeFeedback: () => void;

View File

@ -1,10 +1,41 @@
import { FeedbackContext } from './FeedbackContext'; import {
IFeedbackCategory,
useUserSubmittedFeedback,
} from 'hooks/useSubmittedFeedback';
import { FeedbackContext, IFeedbackContext } from './FeedbackContext';
import { useContext } from 'react'; import { useContext } from 'react';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const useFeedback = () => { type OpenFeedbackParams = {
title: string;
positiveLabel: string;
areasForImprovementsLabel: string;
};
export const useFeedbackContext = (): IFeedbackContext => {
const context = useContext(FeedbackContext); const context = useContext(FeedbackContext);
if (!context) { if (!context) {
throw new Error('useFeedback must be used within a FeedbackProvider'); throw new Error(
'useFeedbackContext must be used within a FeedbackProvider',
);
} }
return context; return context;
}; };
export const useFeedback = (feedbackCategory: IFeedbackCategory) => {
const context = useFeedbackContext();
const { hasSubmittedFeedback } = useUserSubmittedFeedback(feedbackCategory);
return {
...context,
hasSubmittedFeedback,
openFeedback: (parameters: OpenFeedbackParams) => {
context.openFeedback({
...parameters,
category: feedbackCategory,
});
},
};
};

View File

@ -3,7 +3,7 @@ import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
import { basePath } from 'utils/formatPath'; import { basePath } from 'utils/formatPath';
import { createLocalStorage } from '../utils/createLocalStorage'; import { createLocalStorage } from '../utils/createLocalStorage';
export type IFeedbackCategory = 'search'; export type IFeedbackCategory = 'search' | 'newStrategyForm';
export const useUserSubmittedFeedback = (category: IFeedbackCategory) => { export const useUserSubmittedFeedback = (category: IFeedbackCategory) => {
const key = `${basePath}:unleash-userSubmittedFeedback:${category}`; const key = `${basePath}:unleash-userSubmittedFeedback:${category}`;

View File

@ -72,6 +72,7 @@ export type UiFlags = {
increaseUnleashWidth?: boolean; increaseUnleashWidth?: boolean;
featureSearchFeedback?: boolean; featureSearchFeedback?: boolean;
enableLicense?: boolean; enableLicense?: boolean;
newStrategyConfigurationFeedback?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -9,6 +9,7 @@ import { AccessProviderMock } from 'component/providers/AccessProvider/AccessPro
import { UIProviderContainer } from '../component/providers/UIProvider/UIProviderContainer'; import { UIProviderContainer } from '../component/providers/UIProvider/UIProviderContainer';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { QueryParamProvider } from 'use-query-params'; import { QueryParamProvider } from 'use-query-params';
import { FeedbackProvider } from 'component/feedbackNew/FeedbackProvider';
export const render = ( export const render = (
ui: JSX.Element, ui: JSX.Element,
@ -29,6 +30,7 @@ export const render = (
const Wrapper: FC = ({ children }) => ( const Wrapper: FC = ({ children }) => (
<UIProviderContainer> <UIProviderContainer>
<FeedbackProvider>
<SWRConfig <SWRConfig
value={{ provider: () => new Map(), dedupingInterval: 0 }} value={{ provider: () => new Map(), dedupingInterval: 0 }}
> >
@ -44,6 +46,7 @@ export const render = (
</BrowserRouter> </BrowserRouter>
</AccessProviderMock> </AccessProviderMock>
</SWRConfig> </SWRConfig>
</FeedbackProvider>
</UIProviderContainer> </UIProviderContainer>
); );

View File

@ -104,6 +104,7 @@ exports[`should create default config 1`] = `
}, },
"migrationLock": true, "migrationLock": true,
"newStrategyConfiguration": false, "newStrategyConfiguration": false,
"newStrategyConfigurationFeedback": false,
"personalAccessTokensKillSwitch": false, "personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,

View File

@ -36,7 +36,8 @@ export type IFlagKey =
| 'celebrateUnleash' | 'celebrateUnleash'
| 'increaseUnleashWidth' | 'increaseUnleashWidth'
| 'featureSearchFeedback' | 'featureSearchFeedback'
| 'featureSearchFeedbackPosting'; | 'featureSearchFeedbackPosting'
| 'newStrategyConfigurationFeedback';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -163,6 +164,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FEEDBACK_POSTING, process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FEEDBACK_POSTING,
false, false,
), ),
newStrategyConfigurationFeedback: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_NEW_STRATEGY_CONFIGURATION_FEEDBACK,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -47,6 +47,8 @@ process.nextTick(async () => {
stripHeadersOnAPI: true, stripHeadersOnAPI: true,
celebrateUnleash: true, celebrateUnleash: true,
increaseUnleashWidth: true, increaseUnleashWidth: true,
featureSearchFeedback: true,
newStrategyConfigurationFeedback: true,
}, },
}, },
authentication: { authentication: {