From edd6706ffe88a08df57f5ea5c78406e61584db2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 15 Oct 2021 14:16:08 +0200 Subject: [PATCH] fix: new create toggle page --- .../FeatureCreate/FeatureCreate.styles.ts | 17 ++ .../feature/FeatureCreate/FeatureCreate.tsx | 165 ++++++++++++++++++ .../__snapshots__/routes-test.jsx.snap | 9 + frontend/src/component/menu/routes.js | 10 ++ .../ProjectFeatureToggles.tsx | 4 +- .../actions/useFeatureApi/useFeatureApi.ts | 41 +++++ frontend/src/interfaces/featureToggle.ts | 10 ++ frontend/src/utils/route-path-helpers.ts | 4 +- 8 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 frontend/src/component/feature/FeatureCreate/FeatureCreate.styles.ts create mode 100644 frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx diff --git a/frontend/src/component/feature/FeatureCreate/FeatureCreate.styles.ts b/frontend/src/component/feature/FeatureCreate/FeatureCreate.styles.ts new file mode 100644 index 0000000000..7174f7e034 --- /dev/null +++ b/frontend/src/component/feature/FeatureCreate/FeatureCreate.styles.ts @@ -0,0 +1,17 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + bodyContainer: { + borderRadius: '12.5px', + backgroundColor: '#fff', + padding: '2rem', + }, + formContainer: { + marginBottom: '1.5rem', + maxWidth: '350px', + }, + nameInput: { + marginRight: `1.5rem`, + minWidth: `250px` + } +})); diff --git a/frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx b/frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx new file mode 100644 index 0000000000..757c66b7e9 --- /dev/null +++ b/frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from 'react'; +import { useHistory, useParams } from 'react-router'; +import { useStyles } from './FeatureCreate.styles'; +import { IFeatureViewParams } from '../../../interfaces/params'; +import PageContent from '../../common/PageContent'; +import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi'; +import { CardActions, TextField } from '@material-ui/core'; +import FeatureTypeSelect from '../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect'; +import { + CF_CREATE_BTN_ID, + CF_DESC_ID, + CF_NAME_ID, + CF_TYPE_ID, +} from '../../../testIds'; +import { loadNameFromUrl, trim } from '../../common/util'; +import { getTogglePath } from '../../../utils/route-path-helpers'; +import { IFeatureToggleDTO } from '../../../interfaces/featureToggle'; +import { FormEventHandler } from 'react-router/node_modules/@types/react'; +import { useCommonStyles } from '../../../common.styles'; +import { FormButtons } from '../../common'; + +interface Errors { + name?: string; + description?: string; +} + +const FeatureCreate = () => { + const styles = useStyles(); + const commonStyles = useCommonStyles(); + const { projectId } = useParams(); + const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi(); + const history = useHistory(); + const [ toggle, setToggle ] = useState({ + name: loadNameFromUrl(), + description: '', + type: 'release', + stale: false, + variants: [], + project: projectId, + archived: false, + }); + const [errors, setErrors] = useState({}); + + + useEffect(() => { + window.onbeforeunload = () => + 'Data will be lost if you leave the page, are you sure?'; + + return () => { + //@ts-ignore + window.onbeforeunload = false; + }; + }, []); + + const onCancel = () => history.push( + `/projects/${projectId}` + ); + + + const validateName = async (featureToggleName: string) => { + const e = { ...errors }; + try { + await validateFeatureToggleName(featureToggleName); + e.name = undefined; + } catch (err: any) { + e.name = err && err.message ? err.message : 'Could not check name'; + } + + setErrors(e); + }; + + const onSubmit = async (evt: FormEventHandler) => { + evt.preventDefault(); + + const errorList = Object.values(errors).filter(i => i); + + if (errorList.length > 0) { + return; + } + + try { + await createFeatureToggle(projectId, toggle).then(() => + history.push( + getTogglePath(toggle.project, toggle.name, true) + ) + ); + // Trigger + } catch (e: any) { + if (e.toString().includes('not allowed to be empty')) { + setErrors({ name: 'Name is not allowed to be empty' }) + } + } + }; + + const setValue = (field:string, value:string) => { + setToggle({...toggle, [field]: value}) + } + + + return ( + +
+ +
+ validateName(v.target.value)} + onChange={v => setValue('name', trim(v.target.value))} + /> +
+
+ setValue('type', v.target.value)} + label={'Toggle type'} + id="feature-type-select" + editable + inputProps={{ + 'data-test': CF_TYPE_ID, + }} + /> +
+
+ setValue('description', v.target.value)} + /> +
+ + + +
+
+ ); +}; + +export default FeatureCreate; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap index 2a74416658..7a41bc7ef1 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap @@ -57,6 +57,15 @@ Array [ "title": ":name", "type": "protected", }, + Object { + "component": [Function], + "layout": "main", + "menu": Object {}, + "parent": "/projects/:id/features", + "path": "/projects/:projectId/create-toggle2", + "title": "Create feature toggle", + "type": "protected", + }, Object { "component": [Function], "layout": "main", diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 9150ec7e61..9228f8ebb9 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -40,6 +40,7 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive'; import EnvironmentList from '../environments/EnvironmentList/EnvironmentList'; import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment'; import FeatureView2 from '../feature/FeatureView2/FeatureView2'; +import FeatureCreate from '../feature/FeatureCreate/FeatureCreate' export const routes = [ // Project @@ -98,6 +99,15 @@ export const routes = [ layout: 'main', menu: {}, }, + { + path: '/projects/:projectId/create-toggle2', + parent: '/projects/:id/features', + title: 'Create feature toggle', + component: FeatureCreate, + type: 'protected', + layout: 'main', + menu: {}, + }, { path: '/projects/:id/create-toggle', parent: '/projects', diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 3864493944..4d395d7583 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -15,6 +15,7 @@ import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton' import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew'; import { useStyles } from './ProjectFeatureToggles.styles'; import { CREATE_FEATURE } from '../../../AccessProvider/permissions'; +import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; interface IProjectFeatureToggles { features: IFeatureToggleListItem[]; @@ -29,6 +30,7 @@ const ProjectFeatureToggles = ({ const { id } = useParams(); const history = useHistory(); const { hasAccess } = useContext(AccessContext); + const { uiConfig } = useUiConfig(); return ( history.push( - getCreateTogglePath(id) + getCreateTogglePath(id, uiConfig.flags.E) ) } maxWidth="700px" diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts index d8e521ca25..0921a49599 100644 --- a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts @@ -1,3 +1,4 @@ +import { IFeatureToggleDTO } from '../../../../interfaces/featureToggle'; import { ITag } from '../../../../interfaces/tags'; import useAPI from '../useApi/useApi'; @@ -6,6 +7,44 @@ const useFeatureApi = () => { propagateErrors: true, }); + const validateFeatureToggleName = async ( + name: string, + ) => { + const path = `api/admin/features/validate`; + const req = createRequest(path, { + method: 'POST', + body: JSON.stringify({ name }), + }); + + try { + const res = await makeRequest(req.caller, req.id); + + return res; + } catch (e) { + throw e; + } + }; + + + const createFeatureToggle = async ( + projectId: string, + featureToggle: IFeatureToggleDTO, + ) => { + const path = `api/admin/projects/${projectId}/features`; + const req = createRequest(path, { + method: 'POST', + body: JSON.stringify(featureToggle), + }); + + try { + const res = await makeRequest(req.caller, req.id); + + return res; + } catch (e) { + throw e; + } + }; + const toggleFeatureEnvironmentOn = async ( projectId: string, featureId: string, @@ -164,6 +203,8 @@ const useFeatureApi = () => { }; return { + validateFeatureToggleName, + createFeatureToggle, changeFeatureProject, errors, toggleFeatureEnvironmentOn, diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 0532830889..f810792e1b 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -11,6 +11,16 @@ export interface IEnvironments { enabled: boolean; } +export interface IFeatureToggleDTO { + stale: boolean; + archived: boolean; + description: string; + name: string; + project: string; + type: string; + variants: IFeatureVariant[]; +} + export interface IFeatureToggle { stale: boolean; archived: boolean; diff --git a/frontend/src/utils/route-path-helpers.ts b/frontend/src/utils/route-path-helpers.ts index 2a4966a0e3..0f5a35ecf1 100644 --- a/frontend/src/utils/route-path-helpers.ts +++ b/frontend/src/utils/route-path-helpers.ts @@ -9,8 +9,8 @@ export const getToggleCopyPath = ( return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`; }; -export const getCreateTogglePath = (projectId: string) => { - return `/projects/${projectId}/create-toggle?project=${projectId}`; +export const getCreateTogglePath = (projectId: string, newpath: boolean = false) => { + return newpath ? `/projects/${projectId}/create-toggle2` : `/projects/${projectId}/create-toggle?project=${projectId}`; }; export const getProjectEditPath = (projectId: string) => {