mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +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. ![image](https://github.com/Unleash/unleash/assets/17786332/b96d2dbb-2b90-4adf-bc83-cdc534c507ea)
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