mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	1-1319: add feature naming pattern descriptions (#4612)
This PR adds a feature naming pattern description to the project form. It's rendered as a multi-line input field. The description is also stored in the db. This adapts most of @andreas-unleash's PR #4599 with some minor changes (using description instead of prompt). Actually displaying this data to the users will come in a later PR. 
This commit is contained in:
		
							parent
							
								
									31df85a3f5
								
							
						
					
					
						commit
						73b7cc0b5a
					
				| @ -35,8 +35,10 @@ const CreateProject = () => { | ||||
|         featureLimit, | ||||
|         featureNamingPattern, | ||||
|         featureNamingExample, | ||||
|         featureNamingDescription, | ||||
|         setFeatureNamingExample, | ||||
|         setFeatureNamingPattern, | ||||
|         setFeatureNamingDescription, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
| @ -114,7 +116,9 @@ const CreateProject = () => { | ||||
|                 featureLimit={featureLimit} | ||||
|                 featureNamingExample={featureNamingExample} | ||||
|                 featureNamingPattern={featureNamingPattern} | ||||
|                 setProjectNamingPattern={setFeatureNamingPattern} | ||||
|                 setFeatureNamingPattern={setFeatureNamingPattern} | ||||
|                 featureNamingDescription={featureNamingDescription} | ||||
|                 setFeatureNamingDescription={setFeatureNamingDescription} | ||||
|                 setFeatureNamingExample={setFeatureNamingExample} | ||||
|                 setProjectStickiness={setProjectStickiness} | ||||
|                 setFeatureLimit={setFeatureLimit} | ||||
|  | ||||
| @ -43,6 +43,7 @@ const EditProject = () => { | ||||
|         projectMode, | ||||
|         featureNamingPattern, | ||||
|         featureNamingExample, | ||||
|         featureNamingDescription, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
| @ -50,6 +51,7 @@ const EditProject = () => { | ||||
|         setProjectMode, | ||||
|         setFeatureNamingExample, | ||||
|         setFeatureNamingPattern, | ||||
|         setFeatureNamingDescription, | ||||
|         getProjectPayload, | ||||
|         clearErrors, | ||||
|         validateProjectId, | ||||
| @ -63,7 +65,8 @@ const EditProject = () => { | ||||
|         project.mode, | ||||
|         String(project.featureLimit), | ||||
|         project?.featureNaming?.pattern || '', | ||||
|         project?.featureNaming?.example || '' | ||||
|         project?.featureNaming?.example || '', | ||||
|         project?.featureNaming?.description || '' | ||||
|     ); | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
| @ -131,13 +134,15 @@ const EditProject = () => { | ||||
|                 projectMode={projectMode} | ||||
|                 featureNamingPattern={featureNamingPattern} | ||||
|                 featureNamingExample={featureNamingExample} | ||||
|                 featureNamingDescription={featureNamingDescription} | ||||
|                 setProjectName={setProjectName} | ||||
|                 projectStickiness={projectStickiness} | ||||
|                 setProjectStickiness={setProjectStickiness} | ||||
|                 setProjectMode={setProjectMode} | ||||
|                 setFeatureLimit={() => {}} | ||||
|                 setFeatureNamingExample={setFeatureNamingExample} | ||||
|                 setProjectNamingPattern={setFeatureNamingPattern} | ||||
|                 setFeatureNamingPattern={setFeatureNamingPattern} | ||||
|                 setFeatureNamingDescription={setFeatureNamingDescription} | ||||
|                 featureLimit={''} | ||||
|                 projectDesc={projectDesc} | ||||
|                 setProjectDesc={setProjectDesc} | ||||
|  | ||||
| @ -21,8 +21,10 @@ interface IProjectForm { | ||||
|     featureCount?: number; | ||||
|     featureNamingPattern?: string; | ||||
|     featureNamingExample?: string; | ||||
|     setProjectNamingPattern?: React.Dispatch<React.SetStateAction<string>>; | ||||
|     featureNamingDescription?: string; | ||||
|     setFeatureNamingPattern?: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setFeatureNamingDescription?: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>; | ||||
|     setProjectId: React.Dispatch<React.SetStateAction<string>>; | ||||
| @ -100,7 +102,7 @@ const StyledFlagNamingContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     alignItems: 'flex-start', | ||||
|     mt: theme.spacing(1), | ||||
|     gap: theme.spacing(1), | ||||
|     '& > *': { width: '100%' }, | ||||
| })); | ||||
| 
 | ||||
| @ -116,8 +118,10 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|     featureCount, | ||||
|     featureNamingExample, | ||||
|     featureNamingPattern, | ||||
|     featureNamingDescription, | ||||
|     setFeatureNamingExample, | ||||
|     setProjectNamingPattern, | ||||
|     setFeatureNamingPattern, | ||||
|     setFeatureNamingDescription, | ||||
|     setProjectId, | ||||
|     setProjectName, | ||||
|     setProjectDesc, | ||||
| @ -134,11 +138,11 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|     const onSetFeatureNamingPattern = (regex: string) => { | ||||
|         try { | ||||
|             new RegExp(regex); | ||||
|             setProjectNamingPattern && setProjectNamingPattern(regex); | ||||
|             setFeatureNamingPattern && setFeatureNamingPattern(regex); | ||||
|             clearErrors(); | ||||
|         } catch (e) { | ||||
|             errors.featureNamingPattern = 'Invalid regular expression'; | ||||
|             setProjectNamingPattern && setProjectNamingPattern(regex); | ||||
|             setFeatureNamingPattern && setFeatureNamingPattern(regex); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
| @ -151,10 +155,14 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|             } else { | ||||
|                 delete errors.namingExample; | ||||
|             } | ||||
|             setFeatureNamingExample && setFeatureNamingExample(trim(example)); | ||||
|             setFeatureNamingExample && setFeatureNamingExample(example); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onSetFeatureNamingDescription = (description: string) => { | ||||
|         setFeatureNamingDescription && setFeatureNamingDescription(description); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledForm onSubmit={handleSubmit}> | ||||
|             <StyledContainer> | ||||
| @ -283,11 +291,7 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|                     </StyledInputContainer> | ||||
|                 </> | ||||
|                 <ConditionallyRender | ||||
|                     condition={ | ||||
|                         Boolean(shouldShowFlagNaming) && | ||||
|                         setProjectNamingPattern != null && | ||||
|                         setFeatureNamingExample != null | ||||
|                     } | ||||
|                     condition={Boolean(shouldShowFlagNaming)} | ||||
|                     show={ | ||||
|                         <StyledFieldset> | ||||
|                             <Box | ||||
| @ -322,9 +326,9 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|                             <StyledFlagNamingContainer> | ||||
|                                 <StyledInput | ||||
|                                     label={'Naming Pattern'} | ||||
|                                     name="pattern" | ||||
|                                     name="feature flag naming pattern" | ||||
|                                     aria-describedby="pattern-naming-description" | ||||
|                                     placeholder="^[A-Za-z]+-[A-Za-z0-9]+$" | ||||
|                                     placeholder="^[A-Za-z]+\.[A-Za-z]+\.[A-Za-z0-9-]+$" | ||||
|                                     type={'text'} | ||||
|                                     value={featureNamingPattern || ''} | ||||
|                                     error={Boolean(errors.featureNamingPattern)} | ||||
| @ -337,20 +341,20 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <StyledSubtitle> | ||||
|                                     <p id="pattern-example-description"> | ||||
|                                         The example will be shown to users when | ||||
|                                         they create a new feature flag in this | ||||
|                                         project. | ||||
|                                     <p id="pattern-additional-description"> | ||||
|                                         The example and description will be | ||||
|                                         shown to users when they create a new | ||||
|                                         feature flag in this project. | ||||
|                                     </p> | ||||
|                                 </StyledSubtitle> | ||||
| 
 | ||||
|                                 <StyledInput | ||||
|                                     label={'Naming Example'} | ||||
|                                     name="example" | ||||
|                                     name="feature flag naming example" | ||||
|                                     type={'text'} | ||||
|                                     aria-describedBy="pattern-example-description" | ||||
|                                     aria-describedBy="pattern-additional-description" | ||||
|                                     value={featureNamingExample || ''} | ||||
|                                     placeholder="dx-feature1" | ||||
|                                     placeholder="dx.feature1.1-135" | ||||
|                                     error={Boolean(errors.namingExample)} | ||||
|                                     errorText={errors.namingExample} | ||||
|                                     onChange={e => | ||||
| @ -359,6 +363,23 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|                                         ) | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <StyledTextField | ||||
|                                     label={'Naming pattern description'} | ||||
|                                     name="feature flag naming description" | ||||
|                                     type={'text'} | ||||
|                                     aria-describedBy="pattern-additional-description" | ||||
|                                     placeholder={`<project>.<featureName>.<ticket>
 | ||||
| 
 | ||||
| The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`}
 | ||||
|                                     multiline | ||||
|                                     minRows={5} | ||||
|                                     value={featureNamingDescription || ''} | ||||
|                                     onChange={e => | ||||
|                                         onSetFeatureNamingDescription( | ||||
|                                             e.target.value | ||||
|                                         ) | ||||
|                                     } | ||||
|                                 /> | ||||
|                             </StyledFlagNamingContainer> | ||||
|                         </StyledFieldset> | ||||
|                     } | ||||
|  | ||||
| @ -38,6 +38,7 @@ const EditProject = () => { | ||||
|         featureLimit, | ||||
|         featureNamingPattern, | ||||
|         featureNamingExample, | ||||
|         featureNamingDescription, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
| @ -46,6 +47,7 @@ const EditProject = () => { | ||||
|         setFeatureLimit, | ||||
|         setFeatureNamingPattern, | ||||
|         setFeatureNamingExample, | ||||
|         setFeatureNamingDescription, | ||||
|         getProjectPayload, | ||||
|         clearErrors, | ||||
|         validateProjectId, | ||||
| @ -59,7 +61,8 @@ const EditProject = () => { | ||||
|         project.mode, | ||||
|         project.featureLimit ? String(project.featureLimit) : '', | ||||
|         project.featureNaming?.pattern || '', | ||||
|         project.featureNaming?.example || '' | ||||
|         project.featureNaming?.example || '', | ||||
|         project.featureNaming?.description || '' | ||||
|     ); | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
| @ -123,12 +126,14 @@ const EditProject = () => { | ||||
|                     featureCount={project.features.length} | ||||
|                     featureNamingPattern={featureNamingPattern} | ||||
|                     featureNamingExample={featureNamingExample} | ||||
|                     featureNamingDescription={featureNamingDescription} | ||||
|                     setProjectName={setProjectName} | ||||
|                     projectStickiness={projectStickiness} | ||||
|                     setProjectStickiness={setProjectStickiness} | ||||
|                     setProjectMode={setProjectMode} | ||||
|                     setProjectNamingPattern={setFeatureNamingPattern} | ||||
|                     setFeatureNamingPattern={setFeatureNamingPattern} | ||||
|                     setFeatureNamingExample={setFeatureNamingExample} | ||||
|                     setFeatureNamingDescription={setFeatureNamingDescription} | ||||
|                     projectDesc={projectDesc} | ||||
|                     mode="Edit" | ||||
|                     setProjectDesc={setProjectDesc} | ||||
|  | ||||
| @ -12,7 +12,8 @@ const useProjectForm = ( | ||||
|     initialProjectMode: ProjectMode = 'open', | ||||
|     initialFeatureLimit = '', | ||||
|     initialFeatureNamingPattern = '', | ||||
|     initialFeatureNamingExample = '' | ||||
|     initialFeatureNamingExample = '', | ||||
|     initialFeatureNamingDescription = '' | ||||
| ) => { | ||||
|     const [projectId, setProjectId] = useState(initialProjectId); | ||||
| 
 | ||||
| @ -31,6 +32,11 @@ const useProjectForm = ( | ||||
|     const [featureNamingExample, setFeatureNamingExample] = useState( | ||||
|         initialFeatureNamingExample | ||||
|     ); | ||||
| 
 | ||||
|     const [featureNamingDescription, setFeatureNamingDescription] = useState( | ||||
|         initialFeatureNamingDescription | ||||
|     ); | ||||
| 
 | ||||
|     const [errors, setErrors] = useState({}); | ||||
| 
 | ||||
|     const { validateId } = useProjectApi(); | ||||
| @ -63,6 +69,10 @@ const useProjectForm = ( | ||||
|         setFeatureNamingExample(initialFeatureNamingExample); | ||||
|     }, [initialFeatureNamingExample]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setFeatureNamingDescription(initialFeatureNamingDescription); | ||||
|     }, [initialFeatureNamingDescription]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setProjectStickiness(initialProjectStickiness); | ||||
|     }, [initialProjectStickiness]); | ||||
| @ -78,6 +88,7 @@ const useProjectForm = ( | ||||
|             featureNaming: { | ||||
|                 pattern: featureNamingPattern, | ||||
|                 example: featureNamingExample, | ||||
|                 description: featureNamingDescription, | ||||
|             }, | ||||
|         }; | ||||
|     }; | ||||
| @ -125,8 +136,10 @@ const useProjectForm = ( | ||||
|         featureLimit, | ||||
|         featureNamingPattern, | ||||
|         featureNamingExample, | ||||
|         featureNamingDescription, | ||||
|         setFeatureNamingPattern, | ||||
|         setFeatureNamingExample, | ||||
|         setFeatureNamingDescription, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
|  | ||||
| @ -16,6 +16,7 @@ export interface IProjectCard { | ||||
| export type FeatureNamingType = { | ||||
|     pattern: string; | ||||
|     example: string; | ||||
|     description: string; | ||||
| }; | ||||
| 
 | ||||
| export interface IProject { | ||||
|  | ||||
| @ -39,6 +39,7 @@ const SETTINGS_COLUMNS = [ | ||||
|     'feature_limit', | ||||
|     'feature_naming_pattern', | ||||
|     'feature_naming_example', | ||||
|     'feature_naming_description', | ||||
| ]; | ||||
| const SETTINGS_TABLE = 'project_settings'; | ||||
| const PROJECT_ENVIRONMENTS = 'project_environments'; | ||||
| @ -240,6 +241,7 @@ class ProjectStore implements IProjectStore { | ||||
|                 feature_limit: project.featureLimit, | ||||
|                 feature_naming_pattern: project.featureNamingPattern, | ||||
|                 feature_naming_example: project.featureNamingExample, | ||||
|                 feature_naming_description: project.featureNamingDescription, | ||||
|             }) | ||||
|             .returning('*'); | ||||
|         return this.mapRow({ ...row[0], ...settingsRow[0] }); | ||||
| @ -269,6 +271,8 @@ class ProjectStore implements IProjectStore { | ||||
|                         feature_limit: data.featureLimit, | ||||
|                         feature_naming_pattern: data.featureNaming?.pattern, | ||||
|                         feature_naming_example: data.featureNaming?.example, | ||||
|                         feature_naming_description: | ||||
|                             data.featureNaming?.description, | ||||
|                     }); | ||||
|             } else { | ||||
|                 await this.db(SETTINGS_TABLE).insert({ | ||||
| @ -278,6 +282,7 @@ class ProjectStore implements IProjectStore { | ||||
|                     feature_limit: data.featureLimit, | ||||
|                     feature_naming_pattern: data.featureNaming?.pattern, | ||||
|                     feature_naming_example: data.featureNaming?.example, | ||||
|                     feature_naming_description: data.featureNaming?.description, | ||||
|                 }); | ||||
|             } | ||||
|         } catch (err) { | ||||
| @ -573,6 +578,7 @@ class ProjectStore implements IProjectStore { | ||||
|             featureNaming: { | ||||
|                 pattern: row.feature_naming_pattern, | ||||
|                 example: row.feature_naming_example, | ||||
|                 description: row.feature_naming_description, | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -11,14 +11,23 @@ export const createFeatureNamingPatternSchema = { | ||||
|             nullable: true, | ||||
|             description: | ||||
|                 'A JavaScript regular expression pattern, without the start and end delimiters. Optional flags are not allowed.', | ||||
|             example: '[a-z]{2,5}.team-[a-z]+.[a-z-]+', | ||||
|             example: '^[A-Za-z]+\\.[A-Za-z]+\\.[A-Za-z0-9-]+$', | ||||
|         }, | ||||
|         example: { | ||||
|             type: 'string', | ||||
|             nullable: true, | ||||
|             description: | ||||
|                 'An example of a feature name that matches the pattern. Must itself match the pattern supplied.', | ||||
|             example: 'new-project.team-red.feature-1', | ||||
|             example: 'dx.feature1.1-135', | ||||
|         }, | ||||
|         description: { | ||||
|             type: 'string', | ||||
|             nullable: true, | ||||
|             description: | ||||
|                 'A description of the pattern in a human-readable format. Will be shown to users when they create a new feature flag.', | ||||
|             example: `<project>.<featureName>.<ticket>
 | ||||
| 
 | ||||
| The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`,
 | ||||
|         }, | ||||
|     }, | ||||
|     components: {}, | ||||
|  | ||||
| @ -13,6 +13,7 @@ export const projectSchema = joi | ||||
|         featureNaming: joi.object().keys({ | ||||
|             pattern: joi.string().allow(null).allow('').optional(), | ||||
|             example: joi.string().allow(null).allow('').optional(), | ||||
|             description: joi.string().allow(null).allow('').optional(), | ||||
|         }), | ||||
|     }) | ||||
|     .options({ allowUnknown: false, stripUnknown: true }); | ||||
|  | ||||
| @ -192,6 +192,7 @@ export type ProjectMode = 'open' | 'protected'; | ||||
| export interface IFeatureNaming { | ||||
|     pattern: string | null; | ||||
|     example: string | null; | ||||
|     description: string | null; | ||||
| } | ||||
| 
 | ||||
| export interface IProjectOverview { | ||||
|  | ||||
| @ -21,6 +21,7 @@ export interface IProjectInsert { | ||||
|     featureLimit?: number; | ||||
|     featureNamingPattern?: string; | ||||
|     featureNamingExample?: string; | ||||
|     featureNamingDescription?: string; | ||||
| } | ||||
| 
 | ||||
| export interface IProjectSettings { | ||||
| @ -29,6 +30,7 @@ export interface IProjectSettings { | ||||
|     featureLimit?: number; | ||||
|     featureNamingPattern?: string; | ||||
|     featureNamingExample?: string; | ||||
|     featureNamingDescription?: string; | ||||
| } | ||||
| 
 | ||||
| export interface IProjectSettingsRow { | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| exports.up = function (db, cb) { | ||||
|     db.runSql( | ||||
|         ` | ||||
|         ALTER TABLE project_settings | ||||
|             ADD COLUMN IF NOT EXISTS "feature_naming_description" text; | ||||
|         `,
 | ||||
|         cb(), | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| exports.down = function (db, cb) { | ||||
|     db.runSql( | ||||
|         ` | ||||
|         ALTER TABLE project_settings DROP COLUMN IF EXISTS "feature_naming_description"; | ||||
|         `,
 | ||||
|         cb, | ||||
|     ); | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user