diff --git a/frontend/src/component/project/Project/ProjectForm/FeatureFlagNamingTooltip.tsx b/frontend/src/component/project/Project/ProjectForm/FeatureFlagNamingTooltip.tsx index 26de63ea0c..639a7617da 100644 --- a/frontend/src/component/project/Project/ProjectForm/FeatureFlagNamingTooltip.tsx +++ b/frontend/src/component/project/Project/ProjectForm/FeatureFlagNamingTooltip.tsx @@ -14,7 +14,7 @@ export const FeatureFlagNamingTooltip: FC = () => {

Enforce a naming convention for feature flags


-

{`eg. ^[A - Za - z0 - 9]{2}[.][a-z]{4,12}$ matches 'a1.project'`}

+

{`eg. ^[A-Za-z0-9]{2}[.][a-z]{4,12}$ matches 'a1.project'`}

Brackets:

diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx index db12adf53b..dab56a1bdc 100644 --- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx @@ -106,6 +106,29 @@ const StyledFlagNamingContainer = styled('div')(({ theme }) => ({ '& > *': { width: '100%' }, })); +export const validateFeatureNamingExample = ({ + pattern, + example, + featureNamingPatternError, +}: { + pattern: string; + example: string; + featureNamingPatternError: string | undefined; +}): { state: 'valid' } | { state: 'invalid'; reason: string } => { + if (featureNamingPatternError || !example || !pattern) { + return { state: 'valid' }; + } else if (example && pattern) { + const regex = new RegExp(pattern); + const matches = regex.test(example); + if (!matches) { + return { state: 'invalid', reason: 'Example does not match regex' }; + } else { + return { state: 'valid' }; + } + } + return { state: 'valid' }; +}; + const ProjectForm: React.FC = ({ children, handleSubmit, @@ -135,28 +158,51 @@ const ProjectForm: React.FC = ({ }) => { const { uiConfig } = useUiConfig(); const shouldShowFlagNaming = uiConfig.flags.featureNamingPattern; + + const updateNamingExampleError = ({ + example, + pattern, + }: { + example: string; + pattern: string; + }) => { + const validationResult = validateFeatureNamingExample({ + pattern, + example, + featureNamingPatternError: errors.featureNamingPattern, + }); + + switch (validationResult.state) { + case 'invalid': + errors.namingExample = validationResult.reason; + break; + case 'valid': + delete errors.namingExample; + break; + } + }; + const onSetFeatureNamingPattern = (regex: string) => { try { new RegExp(regex); setFeatureNamingPattern && setFeatureNamingPattern(regex); - clearErrors(); + delete errors.featureNamingPattern; } catch (e) { errors.featureNamingPattern = 'Invalid regular expression'; setFeatureNamingPattern && setFeatureNamingPattern(regex); } + updateNamingExampleError({ + pattern: regex, + example: featureNamingExample || '', + }); }; const onSetFeatureNamingExample = (example: string) => { - if (featureNamingPattern) { - const regex = new RegExp(featureNamingPattern); - const matches = regex.test(example); - if (!matches) { - errors.namingExample = 'Example does not match regex'; - } else { - delete errors.namingExample; - } - setFeatureNamingExample && setFeatureNamingExample(example); - } + setFeatureNamingExample && setFeatureNamingExample(example); + updateNamingExampleError({ + pattern: featureNamingPattern || '', + example, + }); }; const onSetFeatureNamingDescription = (description: string) => { @@ -190,7 +236,9 @@ const ProjectForm: React.FC = ({ onChange={e => setProjectName(e.target.value)} error={Boolean(errors.name)} errorText={errors.name} - onFocus={() => clearErrors()} + onFocus={() => { + delete errors.name; + }} data-testid={PROJECT_NAME_INPUT} required /> @@ -333,7 +381,6 @@ const ProjectForm: React.FC = ({ value={featureNamingPattern || ''} error={Boolean(errors.featureNamingPattern)} errorText={errors.featureNamingPattern} - onFocus={() => clearErrors()} onChange={e => onSetFeatureNamingPattern( e.target.value diff --git a/frontend/src/component/project/Project/ProjectForm/validate-feature-naming.test.ts b/frontend/src/component/project/Project/ProjectForm/validate-feature-naming.test.ts new file mode 100644 index 0000000000..f54233fd9e --- /dev/null +++ b/frontend/src/component/project/Project/ProjectForm/validate-feature-naming.test.ts @@ -0,0 +1,50 @@ +import { validateFeatureNamingExample } from './ProjectForm'; + +describe('validateFeatureNaming', () => { + test.each(['+', 'valid regex$'])( + `if the featureNamingPatternError prop is present, it's always valid: %s`, + pattern => { + const result = validateFeatureNamingExample({ + pattern, + example: 'aohutnasoehutns', + featureNamingPatternError: 'error', + }); + + expect(result.state).toBe('valid'); + } + ); + test(`if the pattern is empty, the example is always valid`, () => { + const result = validateFeatureNamingExample({ + pattern: '', + example: 'aohutnasoehutns', + featureNamingPatternError: undefined, + }); + + expect(result.state).toBe('valid'); + }); + test(`if the example is empty, the it's always valid`, () => { + const result = validateFeatureNamingExample({ + pattern: '^dx-[a-z]{1,5}$', + example: '', + featureNamingPatternError: undefined, + }); + + expect(result.state).toBe('valid'); + }); + + test.each([ + ['valid', 'dx-logs'], + ['invalid', 'axe-battles'], + ])( + `if example is %s, the state should be be the same`, + (state, example) => { + const result = validateFeatureNamingExample({ + pattern: '^dx-[a-z]{1,5}$', + example, + featureNamingPatternError: undefined, + }); + + expect(result.state).toBe(state); + } + ); +});