diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx index 3dce656dc9..d370205b07 100644 --- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx +++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx @@ -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} diff --git a/frontend/src/component/project/Project/EditProject/EditProject.tsx b/frontend/src/component/project/Project/EditProject/EditProject.tsx index 9957bd815f..bece525d6f 100644 --- a/frontend/src/component/project/Project/EditProject/EditProject.tsx +++ b/frontend/src/component/project/Project/EditProject/EditProject.tsx @@ -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} diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx index 15ce22ed4d..edfa6bb305 100644 --- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx @@ -21,8 +21,10 @@ interface IProjectForm { featureCount?: number; featureNamingPattern?: string; featureNamingExample?: string; - setProjectNamingPattern?: React.Dispatch>; + featureNamingDescription?: string; + setFeatureNamingPattern?: React.Dispatch>; setFeatureNamingExample?: React.Dispatch>; + setFeatureNamingDescription?: React.Dispatch>; setProjectStickiness?: React.Dispatch>; setProjectMode?: React.Dispatch>; setProjectId: React.Dispatch>; @@ -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 = ({ featureCount, featureNamingExample, featureNamingPattern, + featureNamingDescription, setFeatureNamingExample, - setProjectNamingPattern, + setFeatureNamingPattern, + setFeatureNamingDescription, setProjectId, setProjectName, setProjectDesc, @@ -134,11 +138,11 @@ const ProjectForm: React.FC = ({ 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 = ({ } else { delete errors.namingExample; } - setFeatureNamingExample && setFeatureNamingExample(trim(example)); + setFeatureNamingExample && setFeatureNamingExample(example); } }; + const onSetFeatureNamingDescription = (description: string) => { + setFeatureNamingDescription && setFeatureNamingDescription(description); + }; + return ( @@ -283,11 +291,7 @@ const ProjectForm: React.FC = ({ = ({ = ({ } /> -

- The example will be shown to users when - they create a new feature flag in this - project. +

+ The example and description will be + shown to users when they create a new + feature flag in this project.

@@ -359,6 +363,23 @@ const ProjectForm: React.FC = ({ ) } /> + .. + +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 + ) + } + />
} diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject.tsx index 84f99e0eac..988091285d 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject.tsx @@ -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} diff --git a/frontend/src/component/project/Project/hooks/useProjectForm.ts b/frontend/src/component/project/Project/hooks/useProjectForm.ts index 1df5e55004..7683b19864 100644 --- a/frontend/src/component/project/Project/hooks/useProjectForm.ts +++ b/frontend/src/component/project/Project/hooks/useProjectForm.ts @@ -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, diff --git a/frontend/src/interfaces/project.ts b/frontend/src/interfaces/project.ts index c1bb2c618f..17dce72f4c 100644 --- a/frontend/src/interfaces/project.ts +++ b/frontend/src/interfaces/project.ts @@ -16,6 +16,7 @@ export interface IProjectCard { export type FeatureNamingType = { pattern: string; example: string; + description: string; }; export interface IProject { diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index b5446e49b2..c81997dff3 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -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, }, }; } diff --git a/src/lib/openapi/spec/create-feature-naming-pattern-schema.ts b/src/lib/openapi/spec/create-feature-naming-pattern-schema.ts index 5c5d4403a9..bbebba641c 100644 --- a/src/lib/openapi/spec/create-feature-naming-pattern-schema.ts +++ b/src/lib/openapi/spec/create-feature-naming-pattern-schema.ts @@ -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: `.. + +The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`, }, }, components: {}, diff --git a/src/lib/services/project-schema.ts b/src/lib/services/project-schema.ts index 8f694dbbd0..b11b8e667e 100644 --- a/src/lib/services/project-schema.ts +++ b/src/lib/services/project-schema.ts @@ -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 }); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 8b2f1abc20..68e271f93a 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -192,6 +192,7 @@ export type ProjectMode = 'open' | 'protected'; export interface IFeatureNaming { pattern: string | null; example: string | null; + description: string | null; } export interface IProjectOverview { diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index 3b48de6372..1a830201f4 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -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 { diff --git a/src/migrations/20230905122605-add-feature-naming-description.js b/src/migrations/20230905122605-add-feature-naming-description.js new file mode 100644 index 0000000000..d2656b15af --- /dev/null +++ b/src/migrations/20230905122605-add-feature-naming-description.js @@ -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, + ); +};