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:
parent
31124e4a90
commit
70600552d2
@ -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'
|
||||||
/>
|
/>
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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}`;
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user