From 70600552d20f2cdfb82e4ac914927f98d3bcaae8 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Wed, 3 Jan 2024 15:43:22 +0100 Subject: [PATCH] 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 --- .../NewFeatureStrategyForm.tsx | 31 +++++++++++++++- .../FeatureToggleListTable.tsx | 6 +-- .../feedbackNew/FeedbackComponent.tsx | 30 +++++++-------- .../component/feedbackNew/FeedbackContext.ts | 3 +- .../src/component/feedbackNew/useFeedback.tsx | 37 +++++++++++++++++-- frontend/src/hooks/useSubmittedFeedback.ts | 2 +- frontend/src/interfaces/uiConfig.ts | 1 + frontend/src/utils/testRenderer.tsx | 33 +++++++++-------- .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/types/experimental.ts | 7 +++- src/server-dev.ts | 2 + 11 files changed, 109 insertions(+), 44 deletions(-) diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx index 865ad2a296..9238c64060 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx @@ -44,6 +44,9 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { formatStrategyName } from 'utils/strategyNames'; import { Badge } from 'component/common/Badge/Badge'; 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 { feature: IFeatureToggle; @@ -167,6 +170,8 @@ const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({ color: theme.palette.text.secondary, })); +const feedbackCategory = 'newStrategyForm'; + export const NewFeatureStrategyForm = ({ projectId, feature, @@ -185,6 +190,8 @@ export const NewFeatureStrategyForm = ({ setTab, StrategyVariants, }: IFeatureStrategyFormProps) => { + const { openFeedback, hasSubmittedFeedback } = + useFeedback(feedbackCategory); const { trackEvent } = usePlausibleTracker(); const [showProdGuard, setShowProdGuard] = useState(false); const hasValidConstraints = useConstraintsValidation(strategy.constraints); @@ -195,6 +202,9 @@ export const NewFeatureStrategyForm = ({ environmentId, ); const { strategyDefinition } = useStrategy(strategy?.name); + const newStrategyConfigurationFeedback = useUiFlag( + 'newStrategyConfigurationFeedback', + ); useEffect(() => { trackEvent('new-strategy-form', { @@ -265,6 +275,15 @@ export const NewFeatureStrategyForm = ({ 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) => { if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) { trackEvent('strategy-variants', { @@ -287,7 +306,15 @@ export const NewFeatureStrategyForm = ({ if (enableProdGuard && !isChangeRequest) { setShowProdGuard(true); } else { - onSubmit(); + await onSubmitWithFeedback(); + } + }; + + const onSubmitWithFeedback = async () => { + await onSubmit(); + + if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) { + createFeedbackContext(); } }; @@ -488,7 +515,7 @@ export const NewFeatureStrategyForm = ({ setShowProdGuard(false)} - onClick={onSubmit} + onClick={onSubmitWithFeedback} loading={loading} label='Save strategy' /> diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index a8331d1141..8ac7c94bfa 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -71,9 +71,8 @@ const feedbackCategory = 'search'; const FeatureToggleListTableComponent: VFC = () => { const theme = useTheme(); - const { openFeedback } = useFeedback(); - const { hasSubmittedFeedback, setHasSubmittedFeedback } = - useUserSubmittedFeedback(feedbackCategory); + const { openFeedback, hasSubmittedFeedback } = + useFeedback(feedbackCategory); const { trackEvent } = usePlausibleTracker(); const { environments } = useEnvironments(); const enabledEnvironments = environments @@ -276,7 +275,6 @@ const FeatureToggleListTableComponent: VFC = () => { const createFeedbackContext = () => { openFeedback({ - category: feedbackCategory, title: 'How easy was it to use search and filters?', positiveLabel: 'What do you like most about search and filters?', areasForImprovementsLabel: diff --git a/frontend/src/component/feedbackNew/FeedbackComponent.tsx b/frontend/src/component/feedbackNew/FeedbackComponent.tsx index 94aac0c84d..da5018b8e2 100644 --- a/frontend/src/component/feedbackNew/FeedbackComponent.tsx +++ b/frontend/src/component/feedbackNew/FeedbackComponent.tsx @@ -7,7 +7,7 @@ import { Tooltip, } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useFeedback } from './useFeedback'; +import { useFeedbackContext } from './useFeedback'; import React, { useState } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import useToast from 'hooks/useToast'; @@ -93,6 +93,7 @@ export const FormSubTitle = styled(Box)(({ theme }) => ({ color: theme.palette.text.primary, fontSize: theme.spacing(1.75), lineHeight: theme.spacing(2.5), + marginBottom: theme.spacing(0.5), })); export const StyledButton = styled(Button)(() => ({ @@ -160,7 +161,7 @@ const StyledCloseButton = styled(IconButton)(({ theme }) => ({ })); export const FeedbackComponentWrapper = () => { - const { feedbackData, showFeedback, closeFeedback } = useFeedback(); + const { feedbackData, showFeedback, closeFeedback } = useFeedbackContext(); if (!feedbackData) return null; @@ -309,7 +310,7 @@ export const FeedbackComponent = ({ {feedbackData.positiveLabel} - - Send Feedback - - } - /> + + + Send Feedback + diff --git a/frontend/src/component/feedbackNew/FeedbackContext.ts b/frontend/src/component/feedbackNew/FeedbackContext.ts index 03fa7b991b..280662d88e 100644 --- a/frontend/src/component/feedbackNew/FeedbackContext.ts +++ b/frontend/src/component/feedbackNew/FeedbackContext.ts @@ -1,8 +1,7 @@ import { createContext } from 'react'; -import { ProvideFeedbackSchema } from '../../openapi'; import { IFeedbackCategory } from 'hooks/useSubmittedFeedback'; -interface IFeedbackContext { +export interface IFeedbackContext { feedbackData: FeedbackData | undefined; openFeedback: (data: FeedbackData) => void; closeFeedback: () => void; diff --git a/frontend/src/component/feedbackNew/useFeedback.tsx b/frontend/src/component/feedbackNew/useFeedback.tsx index baf26a9740..c8abe6095a 100644 --- a/frontend/src/component/feedbackNew/useFeedback.tsx +++ b/frontend/src/component/feedbackNew/useFeedback.tsx @@ -1,10 +1,41 @@ -import { FeedbackContext } from './FeedbackContext'; +import { + IFeedbackCategory, + useUserSubmittedFeedback, +} from 'hooks/useSubmittedFeedback'; +import { FeedbackContext, IFeedbackContext } from './FeedbackContext'; 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); + if (!context) { - throw new Error('useFeedback must be used within a FeedbackProvider'); + throw new Error( + 'useFeedbackContext must be used within a FeedbackProvider', + ); } + 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, + }); + }, + }; +}; diff --git a/frontend/src/hooks/useSubmittedFeedback.ts b/frontend/src/hooks/useSubmittedFeedback.ts index cbf48cbe3f..2f96cc93ad 100644 --- a/frontend/src/hooks/useSubmittedFeedback.ts +++ b/frontend/src/hooks/useSubmittedFeedback.ts @@ -3,7 +3,7 @@ import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage'; import { basePath } from 'utils/formatPath'; import { createLocalStorage } from '../utils/createLocalStorage'; -export type IFeedbackCategory = 'search'; +export type IFeedbackCategory = 'search' | 'newStrategyForm'; export const useUserSubmittedFeedback = (category: IFeedbackCategory) => { const key = `${basePath}:unleash-userSubmittedFeedback:${category}`; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index fe42703c5c..df75ca7733 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -72,6 +72,7 @@ export type UiFlags = { increaseUnleashWidth?: boolean; featureSearchFeedback?: boolean; enableLicense?: boolean; + newStrategyConfigurationFeedback?: boolean; }; export interface IVersionInfo { diff --git a/frontend/src/utils/testRenderer.tsx b/frontend/src/utils/testRenderer.tsx index 7d4f7efca5..10accbed7b 100644 --- a/frontend/src/utils/testRenderer.tsx +++ b/frontend/src/utils/testRenderer.tsx @@ -9,6 +9,7 @@ import { AccessProviderMock } from 'component/providers/AccessProvider/AccessPro import { UIProviderContainer } from '../component/providers/UIProvider/UIProviderContainer'; import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import { QueryParamProvider } from 'use-query-params'; +import { FeedbackProvider } from 'component/feedbackNew/FeedbackProvider'; export const render = ( ui: JSX.Element, @@ -29,21 +30,23 @@ export const render = ( const Wrapper: FC = ({ children }) => ( - new Map(), dedupingInterval: 0 }} - > - - - - - - {children} - - - - - - + + new Map(), dedupingInterval: 0 }} + > + + + + + + {children} + + + + + + + ); diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 0bc69fdda5..8ce2f89d7d 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -104,6 +104,7 @@ exports[`should create default config 1`] = ` }, "migrationLock": true, "newStrategyConfiguration": false, + "newStrategyConfigurationFeedback": false, "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, "responseTimeWithAppNameKillSwitch": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 5625f4873f..cf72291c4b 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -36,7 +36,8 @@ export type IFlagKey = | 'celebrateUnleash' | 'increaseUnleashWidth' | 'featureSearchFeedback' - | 'featureSearchFeedbackPosting'; + | 'featureSearchFeedbackPosting' + | 'newStrategyConfigurationFeedback'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -163,6 +164,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FEEDBACK_POSTING, false, ), + newStrategyConfigurationFeedback: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_NEW_STRATEGY_CONFIGURATION_FEEDBACK, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 9119ad1b2e..9f10ef8847 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -47,6 +47,8 @@ process.nextTick(async () => { stripHeadersOnAPI: true, celebrateUnleash: true, increaseUnleashWidth: true, + featureSearchFeedback: true, + newStrategyConfigurationFeedback: true, }, }, authentication: {