mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
Fix: improve create new feature v2 (#441)
This commit is contained in:
parent
9a383967dc
commit
2bce93a51b
@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, FormEvent } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router';
|
import { useHistory, useParams } from 'react-router';
|
||||||
import { useStyles } from './FeatureCreate.styles';
|
import { useStyles } from './FeatureCreate.styles';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||||
import PageContent from '../../common/PageContent';
|
import PageContent from '../../common/PageContent';
|
||||||
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { CardActions, TextField } from '@material-ui/core';
|
import { CardActions } from '@material-ui/core';
|
||||||
import FeatureTypeSelect from '../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
import FeatureTypeSelect from '../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
||||||
import {
|
import {
|
||||||
CF_CREATE_BTN_ID,
|
CF_CREATE_BTN_ID,
|
||||||
@ -12,26 +12,28 @@ import {
|
|||||||
CF_NAME_ID,
|
CF_NAME_ID,
|
||||||
CF_TYPE_ID,
|
CF_TYPE_ID,
|
||||||
} from '../../../testIds';
|
} from '../../../testIds';
|
||||||
import { loadNameFromUrl, trim } from '../../common/util';
|
|
||||||
import { getTogglePath } from '../../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../utils/route-path-helpers';
|
||||||
import { IFeatureToggleDTO } from '../../../interfaces/featureToggle';
|
import { IFeatureToggleDTO } from '../../../interfaces/featureToggle';
|
||||||
import { FormEventHandler } from 'react-router/node_modules/@types/react';
|
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import { FormButtons } from '../../common';
|
import { FormButtons } from '../../common';
|
||||||
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
interface Errors {
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
name?: string;
|
import Input from '../../common/Input/Input';
|
||||||
description?: string;
|
import ProjectSelect from '../project-select-container';
|
||||||
}
|
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
||||||
|
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||||
|
import { trim } from '../../common/util';
|
||||||
|
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const FeatureCreate = () => {
|
const FeatureCreate = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const { projectId } = useParams<IFeatureViewParams>();
|
const { projectId } = useParams<IFeatureViewParams>();
|
||||||
|
const params = useQueryParams();
|
||||||
const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi();
|
const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [toggle, setToggle] = useState<IFeatureToggleDTO>({
|
const [toggle, setToggle] = useState<IFeatureToggleDTO>({
|
||||||
name: loadNameFromUrl(),
|
name: params.get('name') || '',
|
||||||
description: '',
|
description: '',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
stale: false,
|
stale: false,
|
||||||
@ -39,7 +41,10 @@ const FeatureCreate = () => {
|
|||||||
project: projectId,
|
project: projectId,
|
||||||
archived: false,
|
archived: false,
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
|
||||||
|
const [nameError, setNameError] = useState('');
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { permissions } = useUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.onbeforeunload = () =>
|
window.onbeforeunload = () =>
|
||||||
@ -54,34 +59,40 @@ const FeatureCreate = () => {
|
|||||||
const onCancel = () => history.push(`/projects/${projectId}`);
|
const onCancel = () => history.push(`/projects/${projectId}`);
|
||||||
|
|
||||||
const validateName = async (featureToggleName: string) => {
|
const validateName = async (featureToggleName: string) => {
|
||||||
const e = { ...errors };
|
if (featureToggleName.length > 0) {
|
||||||
try {
|
try {
|
||||||
await validateFeatureToggleName(featureToggleName);
|
await validateFeatureToggleName(featureToggleName);
|
||||||
e.name = undefined;
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
e.name = err && err.message ? err.message : 'Could not check name';
|
setNameError(
|
||||||
|
err && err.message ? err.message : 'Could not check name'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (evt: FormEventHandler) => {
|
const onSubmit = async (evt: FormEvent<HTMLFormElement>) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
const errorList = Object.values(errors).filter(i => i);
|
await validateName(toggle.name);
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if(!toggle.name) {
|
||||||
|
setNameError('Name is not allowed to be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createFeatureToggle(projectId, toggle).then(() =>
|
await createFeatureToggle(toggle.project, toggle)
|
||||||
history.push(getTogglePath(toggle.project, toggle.name, true))
|
history.push(getTogglePath(toggle.project, toggle.name, uiConfig.flags.E));
|
||||||
);
|
|
||||||
// Trigger
|
// Trigger
|
||||||
} catch (e: any) {
|
} catch (err) {
|
||||||
if (e.toString().includes('not allowed to be empty')) {
|
if(err instanceof Error) {
|
||||||
setErrors({ name: 'Name is not allowed to be empty' });
|
if (err.toString().includes('not allowed to be empty')) {
|
||||||
|
setNameError('Name is not allowed to be empty');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -98,11 +109,8 @@ const FeatureCreate = () => {
|
|||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<input type="hidden" name="project" value={projectId} />
|
<input type="hidden" name="project" value={projectId} />
|
||||||
<div className={styles.formContainer}>
|
<div className={styles.formContainer}>
|
||||||
<TextField
|
<Input
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
label="Name"
|
label="Name"
|
||||||
required
|
|
||||||
placeholder="Unique-name"
|
placeholder="Unique-name"
|
||||||
className={styles.nameInput}
|
className={styles.nameInput}
|
||||||
name="name"
|
name="name"
|
||||||
@ -110,13 +118,16 @@ const FeatureCreate = () => {
|
|||||||
'data-test': CF_NAME_ID,
|
'data-test': CF_NAME_ID,
|
||||||
}}
|
}}
|
||||||
value={toggle.name}
|
value={toggle.name}
|
||||||
error={errors.name !== undefined}
|
error={Boolean(nameError)}
|
||||||
helperText={errors.name}
|
helperText={nameError}
|
||||||
onBlur={v => validateName(v.target.value)}
|
onBlur={v => validateName(v.target.value)}
|
||||||
onChange={v => setValue('name', trim(v.target.value))}
|
onChange={v => {
|
||||||
|
setValue('name', trim(v.target.value));
|
||||||
|
setNameError('');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formContainer}>
|
<section className={styles.formContainer}>
|
||||||
<FeatureTypeSelect
|
<FeatureTypeSelect
|
||||||
value={toggle.type}
|
value={toggle.type}
|
||||||
onChange={v => setValue('type', v.target.value)}
|
onChange={v => setValue('type', v.target.value)}
|
||||||
@ -127,18 +138,21 @@ const FeatureCreate = () => {
|
|||||||
'data-test': CF_TYPE_ID,
|
'data-test': CF_TYPE_ID,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
<section className={styles.formContainer}>
|
<section className={styles.formContainer}>
|
||||||
<TextField
|
<ProjectSelect
|
||||||
size="small"
|
value={toggle.project}
|
||||||
variant="outlined"
|
onChange={v => setValue('project', v.target.value)}
|
||||||
|
filter={projectFilterGenerator({ permissions }, CREATE_FEATURE)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section className={styles.formContainer}>
|
||||||
|
<Input
|
||||||
className={commonStyles.fullWidth}
|
className={commonStyles.fullWidth}
|
||||||
multiline
|
multiline
|
||||||
rows={4}
|
rows={4}
|
||||||
label="Description"
|
label="Description"
|
||||||
placeholder="A short description of the feature toggle"
|
placeholder="A short description of the feature toggle"
|
||||||
error={errors.description !== undefined}
|
|
||||||
helperText={errors.description}
|
|
||||||
value={toggle.description}
|
value={toggle.description}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
'data-test': CF_DESC_ID,
|
'data-test': CF_DESC_ID,
|
||||||
|
@ -54,7 +54,7 @@ const FeatureToggleList = ({
|
|||||||
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createURL = getCreateTogglePath(currentProjectId);
|
const createURL = getCreateTogglePath(currentProjectId, flags.E);
|
||||||
|
|
||||||
const renderFeatures = () => {
|
const renderFeatures = () => {
|
||||||
features.forEach(e => {
|
features.forEach(e => {
|
||||||
|
@ -144,7 +144,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
data-test="NAVIGATE_TO_CREATE_FEATURE"
|
data-test="NAVIGATE_TO_CREATE_FEATURE"
|
||||||
href="/projects/default/create-toggle?project=default"
|
href="/projects/default/create-toggle"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
@ -344,7 +344,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
data-test="NAVIGATE_TO_CREATE_FEATURE"
|
data-test="NAVIGATE_TO_CREATE_FEATURE"
|
||||||
href="/projects/default/create-toggle?project=default"
|
href="/projects/default/create-toggle"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
|
@ -21,6 +21,7 @@ import FeatureSettings from './FeatureSettings/FeatureSettings';
|
|||||||
import useLoading from '../../../hooks/useLoading';
|
import useLoading from '../../../hooks/useLoading';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const FeatureView2 = () => {
|
const FeatureView2 = () => {
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
@ -33,6 +34,7 @@ const FeatureView2 = () => {
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
const basePath = `/projects/${projectId}/features2/${featureId}`;
|
const basePath = `/projects/${projectId}/features2/${featureId}`;
|
||||||
|
|
||||||
@ -108,7 +110,15 @@ const FeatureView2 = () => {
|
|||||||
<p>
|
<p>
|
||||||
The feature <strong>{featureId.substring(0, 30)}</strong>{' '}
|
The feature <strong>{featureId.substring(0, 30)}</strong>{' '}
|
||||||
does not exist. Do you want to
|
does not exist. Do you want to
|
||||||
<Link to={getCreateTogglePath(projectId)}>create it</Link>
|
<Link
|
||||||
|
to={getCreateTogglePath(
|
||||||
|
projectId,
|
||||||
|
uiConfig.flags.E,
|
||||||
|
{name: featureId}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
create it
|
||||||
|
</Link>
|
||||||
?
|
?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,7 @@ import { Alert } from '@material-ui/lab';
|
|||||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const CopyFeature = props => {
|
const CopyFeature = props => {
|
||||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||||
@ -32,6 +33,7 @@ const CopyFeature = props => {
|
|||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
const { name: copyToggleName, id: projectId } = useParams();
|
const { name: copyToggleName, id: projectId } = useParams();
|
||||||
const { feature } = useFeature(projectId, copyToggleName);
|
const { feature } = useFeature(projectId, copyToggleName);
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
@ -70,7 +72,7 @@ const CopyFeature = props => {
|
|||||||
{ name: newToggleName, replaceGroupId }
|
{ name: newToggleName, replaceGroupId }
|
||||||
);
|
);
|
||||||
props.history.push(
|
props.history.push(
|
||||||
getTogglePath(projectId, newToggleName)
|
getTogglePath(projectId, newToggleName, uiConfig.flags.E)
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setApiError(e);
|
setApiError(e);
|
||||||
@ -96,7 +98,7 @@ const CopyFeature = props => {
|
|||||||
You are about to create a new feature toggle by cloning the
|
You are about to create a new feature toggle by cloning the
|
||||||
configuration of feature toggle
|
configuration of feature toggle
|
||||||
<Link
|
<Link
|
||||||
to={getTogglePath(projectId, copyToggleName)}
|
to={getTogglePath(projectId, copyToggleName, uiConfig.flags.E)}
|
||||||
>
|
>
|
||||||
{copyToggleName}
|
{copyToggleName}
|
||||||
</Link>
|
</Link>
|
||||||
@ -115,6 +117,7 @@ const CopyFeature = props => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
|
@ -89,9 +89,7 @@ const CreateFeature = ({
|
|||||||
<ProjectSelect
|
<ProjectSelect
|
||||||
value={input.project}
|
value={input.project}
|
||||||
defaultValue={project}
|
defaultValue={project}
|
||||||
onChange={v => {
|
onChange={v => setValue('project', v.target.value)}
|
||||||
setValue('project', v.target.value);
|
|
||||||
}}
|
|
||||||
filter={projectFilterGenerator(user, CREATE_FEATURE)}
|
filter={projectFilterGenerator(user, CREATE_FEATURE)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
@ -52,6 +52,7 @@ class WrapperComponent extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validateName = async featureToggleName => {
|
validateName = async featureToggleName => {
|
||||||
|
if (featureToggleName.length > 0) {
|
||||||
const { errors } = { ...this.state };
|
const { errors } = { ...this.state };
|
||||||
try {
|
try {
|
||||||
await validateName(featureToggleName);
|
await validateName(featureToggleName);
|
||||||
@ -61,6 +62,7 @@ class WrapperComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
|
@ -59,10 +59,7 @@ const ProjectFeatureToggles = ({
|
|||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
history.push(
|
history.push(
|
||||||
getCreateTogglePath(
|
getCreateTogglePath(id, uiConfig.flags.E)
|
||||||
id,
|
|
||||||
uiConfig.flags.E
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
maxWidth="700px"
|
maxWidth="700px"
|
||||||
|
@ -9,8 +9,26 @@ export const getToggleCopyPath = (
|
|||||||
return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`;
|
return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCreateTogglePath = (projectId: string, newpath: boolean = false) => {
|
export const getCreateTogglePath = (
|
||||||
return newpath ? `/projects/${projectId}/create-toggle2` : `/projects/${projectId}/create-toggle?project=${projectId}`;
|
projectId: string,
|
||||||
|
newPath: boolean = false,
|
||||||
|
query?: Object
|
||||||
|
) => {
|
||||||
|
const path = newPath
|
||||||
|
? `/projects/${projectId}/create-toggle2`
|
||||||
|
: `/projects/${projectId}/create-toggle`;
|
||||||
|
|
||||||
|
let queryString;
|
||||||
|
if (query) {
|
||||||
|
queryString = Object.keys(query).reduce((acc, curr) => {
|
||||||
|
acc += `${curr}=${query[curr]}`;
|
||||||
|
return acc;
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
if (queryString) {
|
||||||
|
return `${path}?${queryString}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectEditPath = (projectId: string) => {
|
export const getProjectEditPath = (projectId: string) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user