1
0
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:
Youssef Khedher 2021-10-20 12:12:48 +01:00 committed by GitHub
parent 9a383967dc
commit 2bce93a51b
9 changed files with 108 additions and 66 deletions

View File

@ -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,

View File

@ -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 => {

View File

@ -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]}

View File

@ -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 &nbsp; does not exist. Do you want to &nbsp;
<Link to={getCreateTogglePath(projectId)}>create it</Link> <Link
to={getCreateTogglePath(
projectId,
uiConfig.flags.E,
{name: featureId}
)}
>
create it
</Link>
&nbsp;? &nbsp;?
</p> </p>
</div> </div>

View File

@ -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&nbsp; configuration of feature toggle&nbsp;
<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={

View File

@ -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>

View File

@ -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 => {

View File

@ -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"

View File

@ -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) => {