mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into refactor/create-token
This commit is contained in:
		
						commit
						05f395f638
					
				| @ -48,10 +48,10 @@ | ||||
|     "@types/enzyme": "3.10.11", | ||||
|     "@types/enzyme-adapter-react-16": "1.0.6", | ||||
|     "@types/jest": "27.4.0", | ||||
|     "@types/node": "14.18.7", | ||||
|     "@types/node": "14.18.9", | ||||
|     "@types/react": "17.0.38", | ||||
|     "@types/react-dom": "17.0.11", | ||||
|     "@types/react-router-dom": "5.3.2", | ||||
|     "@types/react-router-dom": "5.3.3", | ||||
|     "@types/react-timeago": "4.1.3", | ||||
|     "@welldone-software/why-did-you-render": "6.2.3", | ||||
|     "array-move": "3.0.1", | ||||
| @ -87,7 +87,7 @@ | ||||
|     "redux-devtools-extension": "2.13.9", | ||||
|     "redux-mock-store": "1.5.4", | ||||
|     "redux-thunk": "2.4.1", | ||||
|     "sass": "1.48.0", | ||||
|     "sass": "1.49.0", | ||||
|     "swr": "1.0.1", | ||||
|     "typescript": "4.5.4", | ||||
|     "web-vitals": "2.1.3" | ||||
|  | ||||
| @ -73,7 +73,6 @@ const FormTemplate: React.FC<ICreateProps> = ({ | ||||
|                             <h3 className={styles.subtitle}> | ||||
|                                 API Command{' '} | ||||
|                                 <IconButton | ||||
|                                     className={styles.iconButton} | ||||
|                                     onClick={copyCommand} | ||||
|                                 > | ||||
|                                     <FileCopy className={styles.icon} /> | ||||
|  | ||||
| @ -11,6 +11,15 @@ Array [ | ||||
|     "title": "Create", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "layout": "main", | ||||
|     "menu": Object {}, | ||||
|     "parent": "/projects", | ||||
|     "path": "/projects/:id/edit", | ||||
|     "title": ":id", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "layout": "main", | ||||
| @ -263,15 +272,6 @@ Array [ | ||||
|     "title": "Tag types", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "layout": "main", | ||||
|     "menu": Object {}, | ||||
|     "parent": "/tags", | ||||
|     "path": "/tags/create", | ||||
|     "title": "Create", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "layout": "main", | ||||
|  | ||||
| @ -13,12 +13,7 @@ import ApplicationView from '../../page/applications/view'; | ||||
| import ContextFields from '../../page/context'; | ||||
| import CreateContextField from '../../page/context/create'; | ||||
| import EditContextField from '../../page/context/edit'; | ||||
| import CreateProject from '../../page/project/create'; | ||||
| import ListTagTypes from '../../page/tag-types'; | ||||
| import CreateTagType from '../../page/tag-types/create'; | ||||
| import EditTagType from '../../page/tag-types/edit'; | ||||
| import ListTags from '../../page/tags'; | ||||
| import CreateTag from '../../page/tags/create'; | ||||
| import Addons from '../../page/addons'; | ||||
| import AddonsCreate from '../../page/addons/create'; | ||||
| import AddonsEdit from '../../page/addons/edit'; | ||||
| @ -48,6 +43,10 @@ import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken'; | ||||
| import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment'; | ||||
| import EditEnvironment from '../environments/EditEnvironment/EditEnvironment'; | ||||
| 
 | ||||
| import EditTagType from '../tagTypes/EditTagType/EditTagType'; | ||||
| import CreateTagType from '../tagTypes/CreateTagType/CreateTagType'; | ||||
| import EditProject from '../project/Project/EditProject/EditProject'; | ||||
| import CreateProject from '../project/Project/CreateProject/CreateProject'; | ||||
| 
 | ||||
| export const routes = [ | ||||
|     // Project
 | ||||
| @ -61,6 +60,15 @@ export const routes = [ | ||||
|         layout: 'main', | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/projects/:id/edit', | ||||
|         parent: '/projects', | ||||
|         title: ':id', | ||||
|         component: EditProject, | ||||
|         type: 'protected', | ||||
|         layout: 'main', | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/projects/:id/archived', | ||||
|         title: ':name', | ||||
| @ -304,24 +312,6 @@ export const routes = [ | ||||
|         layout: 'main', | ||||
|         menu: { mobile: true, advanced: true }, | ||||
|     }, | ||||
|     { | ||||
|         path: '/tags/create', | ||||
|         parent: '/tags', | ||||
|         title: 'Create', | ||||
|         component: CreateTag, | ||||
|         type: 'protected', | ||||
|         layout: 'main', | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/tags', | ||||
|         title: 'Tags', | ||||
|         component: ListTags, | ||||
|         hidden: true, | ||||
|         type: 'protected', | ||||
|         layout: 'main', | ||||
|         menu: {}, | ||||
|     }, | ||||
| 
 | ||||
|     // Addons
 | ||||
|     { | ||||
|  | ||||
| @ -1,31 +0,0 @@ | ||||
| .header { | ||||
|     padding: var(--card-header-padding); | ||||
|     margin-bottom: var(--card-margin-y); | ||||
|     word-break: break-all; | ||||
|     border-bottom: var(--default-border); | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
| } | ||||
| 
 | ||||
| .header h1 { | ||||
|     font-size: var(--h1-size); | ||||
| } | ||||
| 
 | ||||
| .supporting { | ||||
|     font-size: var(--caption-size); | ||||
|     max-width: 450px; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|     padding: var(--card-padding); | ||||
| } | ||||
| 
 | ||||
| .formButtons { | ||||
|     padding-top: 1rem; | ||||
| } | ||||
| 
 | ||||
| .formContainer { | ||||
|     margin-bottom: 1.5rem; | ||||
|     max-width: 350px; | ||||
| } | ||||
| @ -0,0 +1,94 @@ | ||||
| import FormTemplate from '../../../common/FormTemplate/FormTemplate'; | ||||
| import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import { useHistory } from 'react-router-dom'; | ||||
| import ProjectForm from '../ProjectForm/ProjectForm'; | ||||
| import useProjectForm from '../hooks/useProjectForm'; | ||||
| import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from '../../../../hooks/useToast'; | ||||
| import useUser from '../../../../hooks/api/getters/useUser/useUser'; | ||||
| 
 | ||||
| const CreateProject = () => { | ||||
|     /* @ts-ignore */ | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { refetch } = useUser(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const history = useHistory(); | ||||
|     const { | ||||
|         projectId, | ||||
|         projectName, | ||||
|         projectDesc, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
|         getProjectPayload, | ||||
|         clearErrors, | ||||
|         validateIdUniqueness, | ||||
|         validateName, | ||||
|         errors, | ||||
|     } = useProjectForm(); | ||||
| 
 | ||||
|     const { createProject, loading } = useProjectApi(); | ||||
| 
 | ||||
|     const handleSubmit = async (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         clearErrors(); | ||||
|         const validName = validateName(); | ||||
|         const validId = await validateIdUniqueness(); | ||||
|         if (validName && validId) { | ||||
|             const payload = getProjectPayload(); | ||||
|             try { | ||||
|                 await createProject(payload); | ||||
|                 refetch(); | ||||
|                 history.push(`/projects/${projectId}`); | ||||
|                 setToastData({ | ||||
|                     title: 'Project created', | ||||
|                     text: 'Now you can add toggles to this project', | ||||
|                     confetti: true, | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|             } catch (e: any) { | ||||
|                 setToastApiError(e.toString()); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
|         return `curl --location --request POST '${ | ||||
|             uiConfig.unleashUrl | ||||
|         }/api/admin/projects' \\ | ||||
| --header 'Authorization: INSERT_API_KEY' \\ | ||||
| --header 'Content-Type: application/json' \\ | ||||
| --data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
 | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         history.push('/projects'); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             loading={loading} | ||||
|             title="Create project" | ||||
|             description="Projects allows you to group feature toggles together in the management UI." | ||||
|             documentationLink="https://docs.getunleash.io/user_guide/projects" | ||||
|             formatApiCode={formatApiCode} | ||||
|         > | ||||
|             <ProjectForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 projectId={projectId} | ||||
|                 setProjectId={setProjectId} | ||||
|                 projectName={projectName} | ||||
|                 setProjectName={setProjectName} | ||||
|                 projectDesc={projectDesc} | ||||
|                 setProjectDesc={setProjectDesc} | ||||
|                 submitButtonText="Create" | ||||
|                 clearErrors={clearErrors} | ||||
|                 validateIdUniqueness={validateIdUniqueness} | ||||
|             /> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default CreateProject; | ||||
| @ -0,0 +1,93 @@ | ||||
| import FormTemplate from '../../../common/FormTemplate/FormTemplate'; | ||||
| import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import { useHistory, useParams } from 'react-router-dom'; | ||||
| import ProjectForm from '../ProjectForm/ProjectForm'; | ||||
| import useProjectForm from '../hooks/useProjectForm'; | ||||
| import useProject from '../../../../hooks/api/getters/useProject/useProject'; | ||||
| import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from '../../../../hooks/useToast'; | ||||
| 
 | ||||
| const EditProject = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { id } = useParams<{ id: string }>(); | ||||
|     const { project } = useProject(id); | ||||
|     const history = useHistory(); | ||||
|     const { | ||||
|         projectId, | ||||
|         projectName, | ||||
|         projectDesc, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
|         getProjectPayload, | ||||
|         clearErrors, | ||||
|         validateIdUniqueness, | ||||
|         validateName, | ||||
|         errors, | ||||
|     } = useProjectForm(id, project.name, project.description); | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
|         return `curl --location --request PUT '${ | ||||
|             uiConfig.unleashUrl | ||||
|         }/api/admin/projects/${id}' \\ | ||||
| --header 'Authorization: INSERT_API_KEY' \\ | ||||
| --header 'Content-Type: application/json' \\ | ||||
| --data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
 | ||||
|     }; | ||||
| 
 | ||||
|     const { refetch } = useProject(id); | ||||
|     const { editProject, loading } = useProjectApi(); | ||||
| 
 | ||||
|     const handleSubmit = async (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         const payload = getProjectPayload(); | ||||
| 
 | ||||
|         const validName = validateName(); | ||||
| 
 | ||||
|         if (validName) { | ||||
|             try { | ||||
|                 await editProject(id, payload); | ||||
|                 refetch(); | ||||
|                 history.push(`/projects/${id}`); | ||||
|                 setToastData({ | ||||
|                     title: 'Project information updated', | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|             } catch (e: any) { | ||||
|                 setToastApiError(e.toString()); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         history.goBack(); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             loading={loading} | ||||
|             title="Edit project" | ||||
|             description="Projects allows you to group feature toggles together in the management UI." | ||||
|             documentationLink="https://docs.getunleash.io/user_guide/projects" | ||||
|             formatApiCode={formatApiCode} | ||||
|         > | ||||
|             <ProjectForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 projectId={projectId} | ||||
|                 setProjectId={setProjectId} | ||||
|                 projectName={projectName} | ||||
|                 setProjectName={setProjectName} | ||||
|                 projectDesc={projectDesc} | ||||
|                 setProjectDesc={setProjectDesc} | ||||
|                 submitButtonText="Edit" | ||||
|                 clearErrors={clearErrors} | ||||
|                 validateIdUniqueness={validateIdUniqueness} | ||||
|             /> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EditProject; | ||||
| @ -13,7 +13,6 @@ import { useEffect } from 'react'; | ||||
| import useTabs from '../../../hooks/useTabs'; | ||||
| import TabPanel from '../../common/TabNav/TabPanel'; | ||||
| import ProjectAccess from '../access-container'; | ||||
| import EditProject from '../edit-project-container'; | ||||
| import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment'; | ||||
| import ProjectOverview from './ProjectOverview'; | ||||
| import ProjectHealth from './ProjectHealth/ProjectHealth'; | ||||
| @ -58,19 +57,6 @@ const Project = () => { | ||||
|             path: `${basePath}/environments`, | ||||
|             name: 'environments', | ||||
|         }, | ||||
|         { | ||||
|             title: 'Settings', | ||||
|             // @ts-ignore (fix later)
 | ||||
|             component: ( | ||||
|                 <EditProject | ||||
|                     projectId={id} | ||||
|                     history={history} | ||||
|                     title="Edit project" | ||||
|                 /> | ||||
|             ), | ||||
|             path: `${basePath}/settings`, | ||||
|             name: 'settings', | ||||
|         }, | ||||
|     ]; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
| @ -101,15 +87,6 @@ const Project = () => { | ||||
|         /* eslint-disable-next-line */ | ||||
|     }, []); | ||||
| 
 | ||||
|     const goToTabWithName = (name: string) => { | ||||
|         const index = tabData.findIndex(t => t.name === name); | ||||
|         if (index >= 0) { | ||||
|             const tab = tabData[index]; | ||||
|             history.push(tab.path); | ||||
|             setActiveTab(index); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const renderTabs = () => { | ||||
|         return tabData.map((tab, index) => { | ||||
|             return ( | ||||
| @ -150,9 +127,9 @@ const Project = () => { | ||||
|                         Project: {project?.name}{' '} | ||||
|                         <PermissionIconButton | ||||
|                             permission={UPDATE_PROJECT} | ||||
|                             tooltip={'Edit description'} | ||||
|                             tooltip="Edit" | ||||
|                             projectId={project?.id} | ||||
|                             onClick={() => goToTabWithName('settings')} | ||||
|                             onClick={() => history.push(`/projects/${id}/edit`)} | ||||
|                             data-loading | ||||
|                         > | ||||
|                             <Edit /> | ||||
|  | ||||
| @ -0,0 +1,47 @@ | ||||
| import { makeStyles } from '@material-ui/core/styles'; | ||||
| 
 | ||||
| export const useStyles = makeStyles(theme => ({ | ||||
|     form: { | ||||
|         display: 'flex', | ||||
|         flexDirection: 'column', | ||||
|         height: '100%', | ||||
|     }, | ||||
|     container: { | ||||
|         maxWidth: '400px', | ||||
|     }, | ||||
|     input: { width: '100%', marginBottom: '1rem' }, | ||||
|     label: { | ||||
|         minWidth: '300px', | ||||
|         [theme.breakpoints.down(600)]: { | ||||
|             minWidth: 'auto', | ||||
|         }, | ||||
|     }, | ||||
|     buttonContainer: { | ||||
|         marginTop: 'auto', | ||||
|         display: 'flex', | ||||
|         justifyContent: 'flex-end', | ||||
|     }, | ||||
|     cancelButton: { | ||||
|         marginRight: '1.5rem', | ||||
|     }, | ||||
|     inputDescription: { | ||||
|         marginBottom: '0.5rem', | ||||
|     }, | ||||
|     formHeader: { | ||||
|         fontWeight: 'normal', | ||||
|         marginTop: '0', | ||||
|     }, | ||||
|     header: { | ||||
|         fontWeight: 'normal', | ||||
|     }, | ||||
|     permissionErrorContainer: { | ||||
|         position: 'relative', | ||||
|     }, | ||||
|     errorMessage: { | ||||
|         //@ts-ignore
 | ||||
|         fontSize: theme.fontSizes.smallBody, | ||||
|         color: theme.palette.error.main, | ||||
|         position: 'absolute', | ||||
|         top: '-8px', | ||||
|     }, | ||||
| })); | ||||
| @ -0,0 +1,103 @@ | ||||
| import PermissionButton from '../../../common/PermissionButton/PermissionButton'; | ||||
| import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions'; | ||||
| import Input from '../../../common/Input/Input'; | ||||
| import { TextField, Button } from '@material-ui/core'; | ||||
| import { useStyles } from './ProjectForm.style'; | ||||
| import React from 'react'; | ||||
| import { trim } from '../../../common/util'; | ||||
| 
 | ||||
| interface IProjectForm { | ||||
|     projectId: string; | ||||
|     projectName: string; | ||||
|     projectDesc: string; | ||||
|     setProjectId: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setProjectName: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setProjectDesc: React.Dispatch<React.SetStateAction<string>>; | ||||
|     handleSubmit: (e: any) => void; | ||||
|     handleCancel: () => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     submitButtonText: string; | ||||
|     clearErrors: () => void; | ||||
|     validateIdUniqueness: () => void; | ||||
| } | ||||
| 
 | ||||
| const ProjectForm = ({ | ||||
|     handleSubmit, | ||||
|     handleCancel, | ||||
|     projectId, | ||||
|     projectName, | ||||
|     projectDesc, | ||||
|     setProjectId, | ||||
|     setProjectName, | ||||
|     setProjectDesc, | ||||
|     errors, | ||||
|     submitButtonText, | ||||
|     validateIdUniqueness, | ||||
|     clearErrors, | ||||
| }: IProjectForm) => { | ||||
|     const styles = useStyles(); | ||||
| 
 | ||||
|     return ( | ||||
|         <form onSubmit={handleSubmit} className={styles.form}> | ||||
|             <h3 className={styles.formHeader}>Project Information</h3> | ||||
| 
 | ||||
|             <div className={styles.container}> | ||||
|                 <p className={styles.inputDescription}> | ||||
|                     What is your project Id? | ||||
|                 </p> | ||||
|                 <Input | ||||
|                     className={styles.input} | ||||
|                     label="Project Id" | ||||
|                     value={projectId} | ||||
|                     onChange={e => setProjectId(trim(e.target.value))} | ||||
|                     error={Boolean(errors.id)} | ||||
|                     errorText={errors.id} | ||||
|                     onFocus={() => clearErrors()} | ||||
|                     onBlur={validateIdUniqueness} | ||||
|                     disabled={submitButtonText === 'Edit'} | ||||
|                 /> | ||||
| 
 | ||||
|                 <p className={styles.inputDescription}> | ||||
|                     What is your project name? | ||||
|                 </p> | ||||
|                 <Input | ||||
|                     className={styles.input} | ||||
|                     label="Project name" | ||||
|                     value={projectName} | ||||
|                     onChange={e => setProjectName(e.target.value)} | ||||
|                     error={Boolean(errors.name)} | ||||
|                     errorText={errors.name} | ||||
|                     onFocus={() => clearErrors()} | ||||
|                 /> | ||||
| 
 | ||||
|                 <p className={styles.inputDescription}> | ||||
|                     What is your project description? | ||||
|                 </p> | ||||
|                 <TextField | ||||
|                     className={styles.input} | ||||
|                     label="Project description" | ||||
|                     variant="outlined" | ||||
|                     multiline | ||||
|                     maxRows={4} | ||||
|                     value={projectDesc} | ||||
|                     onChange={e => setProjectDesc(e.target.value)} | ||||
|                 /> | ||||
|             </div> | ||||
| 
 | ||||
|             <div className={styles.buttonContainer}> | ||||
|                 <Button onClick={handleCancel} className={styles.cancelButton}> | ||||
|                     Cancel | ||||
|                 </Button> | ||||
|                 <PermissionButton | ||||
|                     onClick={handleSubmit} | ||||
|                     permission={CREATE_PROJECT} | ||||
|                     type="submit" | ||||
|                 > | ||||
|                     {submitButtonText} project | ||||
|                 </PermissionButton> | ||||
|             </div> | ||||
|         </form> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default ProjectForm; | ||||
| @ -55,7 +55,7 @@ const ProjectInfo = ({ | ||||
|             component={Link} | ||||
|             className={permissionButtonClass} | ||||
|             data-loading | ||||
|             to={`/projects/${id}/settings`} | ||||
|             to={`/projects/${id}/edit`} | ||||
|         > | ||||
|             <Edit /> | ||||
|         </PermissionIconButton> | ||||
|  | ||||
| @ -0,0 +1,83 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import { IPermission } from '../../../../interfaces/project'; | ||||
| 
 | ||||
| export interface ICheckedPermission { | ||||
|     [key: string]: IPermission; | ||||
| } | ||||
| 
 | ||||
| const useProjectForm = ( | ||||
|     initialProjectId = '', | ||||
|     initialProjectName = '', | ||||
|     initialProjectDesc = '' | ||||
| ) => { | ||||
|     const [projectId, setProjectId] = useState(initialProjectId); | ||||
|     const [projectName, setProjectName] = useState(initialProjectName); | ||||
|     const [projectDesc, setProjectDesc] = useState(initialProjectDesc); | ||||
|     const [errors, setErrors] = useState({}); | ||||
|     const { validateId } = useProjectApi(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setProjectId(initialProjectId); | ||||
|     }, [initialProjectId]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setProjectName(initialProjectName); | ||||
|     }, [initialProjectName]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setProjectDesc(initialProjectDesc); | ||||
|     }, [initialProjectDesc]); | ||||
| 
 | ||||
|     const getProjectPayload = () => { | ||||
|         return { | ||||
|             id: projectId, | ||||
|             name: projectName, | ||||
|             description: projectDesc, | ||||
|         }; | ||||
|     }; | ||||
|     const NAME_EXISTS_ERROR = 'Error: A project with this id already exists.'; | ||||
| 
 | ||||
|     const validateIdUniqueness = async () => { | ||||
|         try { | ||||
|             await validateId(getProjectPayload()); | ||||
|             return true; | ||||
|         } catch (e: any) { | ||||
|             if (e.toString().includes(NAME_EXISTS_ERROR)) { | ||||
|                 setErrors(prev => ({ | ||||
|                     ...prev, | ||||
|                     id: 'A project with this id already exists', | ||||
|                 })); | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const validateName = () => { | ||||
|         if (projectName.length === 0) { | ||||
|             setErrors(prev => ({ ...prev, name: 'Name can not be empty.' })); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     const clearErrors = () => { | ||||
|         setErrors({}); | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         projectId, | ||||
|         projectName, | ||||
|         projectDesc, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
|         getProjectPayload, | ||||
|         validateName, | ||||
|         validateIdUniqueness, | ||||
|         clearErrors, | ||||
|         errors, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useProjectForm; | ||||
| @ -1,21 +0,0 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import ProjectComponent from './form-project-component'; | ||||
| import { createProject, validateId } from './../../store/project/actions'; | ||||
| import { fetchUser } from './../../store/user/actions'; | ||||
| 
 | ||||
| const mapStateToProps = () => ({ | ||||
|     project: { id: '', name: '', description: '' }, | ||||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|     validateId, | ||||
|     submit: async project => { | ||||
|         await createProject(project)(dispatch); | ||||
|         fetchUser()(dispatch); | ||||
|     }, | ||||
|     editMode: false, | ||||
| }); | ||||
| 
 | ||||
| const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectComponent); | ||||
| 
 | ||||
| export default FormAddContainer; | ||||
| @ -1,23 +0,0 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import Component from './form-project-component'; | ||||
| import { updateProject, validateId } from './../../store/project/actions'; | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => { | ||||
|     const projectBase = { id: '', name: '', description: '' }; | ||||
|     const realProject = state.projects.toJS().find(n => n.id === props.projectId); | ||||
|     const project = Object.assign(projectBase, realProject); | ||||
| 
 | ||||
|     return { | ||||
|         project, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|     validateId, | ||||
|     submit: project => updateProject(project)(dispatch), | ||||
|     editMode: true, | ||||
| }); | ||||
| 
 | ||||
| const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component); | ||||
| 
 | ||||
| export default FormAddContainer; | ||||
| @ -1,239 +0,0 @@ | ||||
| import { useContext, useEffect, useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { TextField, Typography } from '@material-ui/core'; | ||||
| 
 | ||||
| import styles from './Project.module.scss'; | ||||
| import classnames from 'classnames'; | ||||
| import { FormButtons, styles as commonStyles } from '../common'; | ||||
| import { trim } from '../common/util'; | ||||
| import PageContent from '../common/PageContent/PageContent'; | ||||
| import AccessContext from '../../contexts/AccessContext'; | ||||
| import ConditionallyRender from '../common/ConditionallyRender'; | ||||
| import { CREATE_PROJECT } from '../providers/AccessProvider/permissions'; | ||||
| import HeaderTitle from '../common/HeaderTitle'; | ||||
| import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { Alert } from '@material-ui/lab'; | ||||
| import { FormEvent } from 'react-router/node_modules/@types/react'; | ||||
| import useLoading from '../../hooks/useLoading'; | ||||
| import PermissionButton from '../common/PermissionButton/PermissionButton'; | ||||
| import { UPDATE_PROJECT } from '../../store/project/actions'; | ||||
| import useUser from '../../hooks/api/getters/useUser/useUser'; | ||||
| 
 | ||||
| interface ProjectFormComponentProps { | ||||
|     editMode: boolean; | ||||
|     project: any; | ||||
|     validateId: (id: string) => Promise<void>; | ||||
|     history: any; | ||||
|     submit: (project: any) => Promise<void>; | ||||
| } | ||||
| 
 | ||||
| const ProjectFormComponent = (props: ProjectFormComponentProps) => { | ||||
|     const { editMode } = props; | ||||
|     const { refetch } = useUser(); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const [project, setProject] = useState(props.project || {}); | ||||
|     const [errors, setErrors] = useState<any>({}); | ||||
|     const { isOss, loading } = useUiConfig(); | ||||
|     const ref = useLoading(loading); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (!project.id && props.project.id) { | ||||
|             setProject(props.project); | ||||
|         } | ||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|     }, [props.project]); | ||||
| 
 | ||||
|     const setValue = (field: string, value: string) => { | ||||
|         const p = { ...project }; | ||||
|         p[field] = value; | ||||
|         setProject(p); | ||||
|     }; | ||||
| 
 | ||||
|     const validateId = async (id: string) => { | ||||
|         if (editMode) return true; | ||||
| 
 | ||||
|         const e = { ...errors }; | ||||
|         try { | ||||
|             await props.validateId(id); | ||||
|             e.id = undefined; | ||||
|         } catch (err: any) { | ||||
|             e.id = err.message; | ||||
|         } | ||||
| 
 | ||||
|         setErrors(e); | ||||
|         if (e.id) return false; | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     const validateName = () => { | ||||
|         if (project.name.length === 0) { | ||||
|             setErrors({ ...errors, name: 'Name can not be empty.' }); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     const validate = async (id: string) => { | ||||
|         const validId = await validateId(id); | ||||
|         const validName = validateName(); | ||||
| 
 | ||||
|         return validId && validName; | ||||
|     }; | ||||
| 
 | ||||
|     const onCancel = (evt: Event) => { | ||||
|         evt.preventDefault(); | ||||
| 
 | ||||
|         if (editMode) { | ||||
|             props.history.push(`/projects/${project.id}`); | ||||
|             return; | ||||
|         } | ||||
|         props.history.push(`/projects/`); | ||||
|     }; | ||||
| 
 | ||||
|     const onSubmit = async (evt: FormEvent<HTMLFormElement>) => { | ||||
|         evt.preventDefault(); | ||||
| 
 | ||||
|         const valid = await validate(project.id); | ||||
| 
 | ||||
|         if (valid) { | ||||
|             const query = editMode ? 'edited=true' : 'created=true'; | ||||
|             await props.submit(project); | ||||
|             refetch(); | ||||
|             props.history.push(`/projects/${project.id}?${query}`); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const submitText = editMode ? 'Update' : 'Create'; | ||||
| 
 | ||||
|     return ( | ||||
|         <div ref={ref}> | ||||
|             <PageContent | ||||
|                 headerContent={ | ||||
|                     <HeaderTitle | ||||
|                         title={`${submitText} ${props.project?.name} project`} | ||||
|                     /> | ||||
|                 } | ||||
|             > | ||||
|                 <ConditionallyRender | ||||
|                     condition={isOss()} | ||||
|                     show={ | ||||
|                         <Alert data-loading severity="error"> | ||||
|                             {submitText} project requires a paid version of | ||||
|                             Unleash. Check out{' '} | ||||
|                             <a | ||||
|                                 href="https://www.getunleash.io" | ||||
|                                 target="_blank" | ||||
|                                 rel="noreferrer" | ||||
|                             > | ||||
|                                 getunleash.io | ||||
|                             </a>{' '} | ||||
|                             to learn more. | ||||
|                         </Alert> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <> | ||||
|                             <Typography | ||||
|                                 variant="subtitle1" | ||||
|                                 style={{ marginBottom: '0.5rem' }} | ||||
|                             > | ||||
|                                 Projects allows you to group feature toggles | ||||
|                                 together in the management UI. | ||||
|                             </Typography> | ||||
|                             <form | ||||
|                                 data-loading | ||||
|                                 onSubmit={onSubmit} | ||||
|                                 className={classnames( | ||||
|                                     commonStyles.contentSpacing, | ||||
|                                     styles.formContainer | ||||
|                                 )} | ||||
|                             > | ||||
|                                 <TextField | ||||
|                                     label="Project Id" | ||||
|                                     name="id" | ||||
|                                     placeholder="A-unique-key" | ||||
|                                     value={project.id} | ||||
|                                     error={!!errors.id} | ||||
|                                     helperText={errors.id} | ||||
|                                     disabled={editMode} | ||||
|                                     variant="outlined" | ||||
|                                     size="small" | ||||
|                                     onBlur={v => validateId(v.target.value)} | ||||
|                                     onChange={v => | ||||
|                                         setValue('id', trim(v.target.value)) | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <br /> | ||||
|                                 <TextField | ||||
|                                     label="Name" | ||||
|                                     name="name" | ||||
|                                     placeholder="Project name" | ||||
|                                     value={project.name} | ||||
|                                     error={!!errors.name} | ||||
|                                     variant="outlined" | ||||
|                                     size="small" | ||||
|                                     helperText={errors.name} | ||||
|                                     onChange={v => | ||||
|                                         setValue('name', v.target.value) | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <TextField | ||||
|                                     className={commonStyles.fullwidth} | ||||
|                                     placeholder="A short description" | ||||
|                                     maxRows={2} | ||||
|                                     label="Description" | ||||
|                                     error={!!errors.description} | ||||
|                                     helperText={errors.description} | ||||
|                                     variant="outlined" | ||||
|                                     size="small" | ||||
|                                     multiline | ||||
|                                     value={project.description} | ||||
|                                     onChange={v => | ||||
|                                         setValue('description', v.target.value) | ||||
|                                     } | ||||
|                                 /> | ||||
| 
 | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={ | ||||
|                                         hasAccess(CREATE_PROJECT) && !editMode | ||||
|                                     } | ||||
|                                     show={ | ||||
|                                         <div className={styles.formButtons}> | ||||
|                                             <FormButtons | ||||
|                                                 submitText={submitText} | ||||
|                                                 onCancel={onCancel} | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                     } | ||||
|                                 /> | ||||
| 
 | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={editMode} | ||||
|                                     show={ | ||||
|                                         <PermissionButton | ||||
|                                             permission={UPDATE_PROJECT} | ||||
|                                             projectId={props.project.id} | ||||
|                                             type="submit" | ||||
|                                             style={{ marginTop: '1rem' }} | ||||
|                                         > | ||||
|                                             Update project | ||||
|                                         </PermissionButton> | ||||
|                                     } | ||||
|                                 /> | ||||
|                             </form> | ||||
|                         </> | ||||
|                     } | ||||
|                 /> | ||||
|             </PageContent> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| ProjectFormComponent.propTypes = { | ||||
|     project: PropTypes.object.isRequired, | ||||
|     validateId: PropTypes.func.isRequired, | ||||
|     submit: PropTypes.func.isRequired, | ||||
|     history: PropTypes.object.isRequired, | ||||
|     editMode: PropTypes.bool.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default ProjectFormComponent; | ||||
| @ -13,7 +13,6 @@ export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; | ||||
| export const CREATE_PROJECT = 'CREATE_PROJECT'; | ||||
| export const UPDATE_PROJECT = 'UPDATE_PROJECT'; | ||||
| export const DELETE_PROJECT = 'DELETE_PROJECT'; | ||||
| export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE'; | ||||
| export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; | ||||
| export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; | ||||
| export const CREATE_TAG = 'CREATE_TAG'; | ||||
|  | ||||
| @ -2,6 +2,10 @@ | ||||
|     min-width: 100px; | ||||
| } | ||||
| 
 | ||||
| .icon { | ||||
|     fill: #757575; | ||||
| } | ||||
| 
 | ||||
| .textfield { | ||||
|     margin-left: 15px; | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import { useContext, useEffect, useState } from 'react'; | ||||
| import { useContext, useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { Link, useHistory } from 'react-router-dom'; | ||||
| 
 | ||||
| import { | ||||
|     List, | ||||
|     ListItem, | ||||
| @ -11,38 +10,53 @@ import { | ||||
|     Button, | ||||
|     Tooltip, | ||||
| } from '@material-ui/core'; | ||||
| import { Add, Delete, Label } from '@material-ui/icons'; | ||||
| 
 | ||||
| import { Add, Delete, Edit, Label } from '@material-ui/icons'; | ||||
| import HeaderTitle from '../../common/HeaderTitle'; | ||||
| import PageContent from '../../common/PageContent/PageContent'; | ||||
| import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; | ||||
| import { | ||||
|     CREATE_TAG_TYPE, | ||||
|     DELETE_TAG_TYPE, | ||||
|     UPDATE_TAG_TYPE, | ||||
| } from '../../providers/AccessProvider/permissions'; | ||||
| import Dialogue from '../../common/Dialogue/Dialogue'; | ||||
| import useMediaQuery from '@material-ui/core/useMediaQuery'; | ||||
| 
 | ||||
| import styles from '../TagType.module.scss'; | ||||
| import AccessContext from '../../../contexts/AccessContext'; | ||||
| import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||
| import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes'; | ||||
| import useToast from '../../../hooks/useToast'; | ||||
| import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton'; | ||||
| 
 | ||||
| const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | ||||
| const TagTypeList = () => { | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const [deletion, setDeletion] = useState({ open: false }); | ||||
|     const history = useHistory(); | ||||
|     const smallScreen = useMediaQuery('(max-width:700px)'); | ||||
|     const { deleteTagType } = useTagTypesApi(); | ||||
|     const { tagTypes, refetch } = useTagTypes(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         fetchTagTypes(); | ||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps | ||||
|     }, []); | ||||
|     const deleteTag = async () => { | ||||
|         try { | ||||
|             await deleteTagType(deletion.name); | ||||
|             refetch(); | ||||
|             setDeletion({ open: false }); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 show: true, | ||||
|                 text: 'Successfully deleted tag type.', | ||||
|             }); | ||||
|         } catch (e) { | ||||
|             setToastApiError(e.toString()); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     let header = ( | ||||
|         <HeaderTitle | ||||
|             title="Tag Types" | ||||
|             actions={ | ||||
|                 <ConditionallyRender | ||||
|                     condition={hasAccess(CREATE_TAG_TYPE)} | ||||
|                     condition={hasAccess(UPDATE_TAG_TYPE)} | ||||
|                     show={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={smallScreen} | ||||
| @ -96,6 +110,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | ||||
|                 </IconButton> | ||||
|             </Tooltip> | ||||
|         ); | ||||
| 
 | ||||
|         return ( | ||||
|             <ListItem | ||||
|                 key={`${tagType.name}`} | ||||
| @ -105,6 +120,13 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | ||||
|                     <Label /> | ||||
|                 </ListItemIcon> | ||||
|                 <ListItemText primary={link} secondary={tagType.description} /> | ||||
|                 <PermissionIconButton | ||||
|                     permission={UPDATE_TAG_TYPE} | ||||
|                     component={Link} | ||||
|                     to={`/tag-types/edit/${tagType.name}`} | ||||
|                 > | ||||
|                     <Edit className={styles.icon} /> | ||||
|                 </PermissionIconButton> | ||||
|                 <ConditionallyRender | ||||
|                     condition={hasAccess(DELETE_TAG_TYPE)} | ||||
|                     show={deleteButton} | ||||
| @ -124,10 +146,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | ||||
|             <Dialogue | ||||
|                 title="Really delete Tag type?" | ||||
|                 open={deletion.open} | ||||
|                 onClick={() => { | ||||
|                     removeTagType(deletion.name); | ||||
|                     setDeletion({ open: false }); | ||||
|                 }} | ||||
|                 onClick={deleteTag} | ||||
|                 onClose={() => { | ||||
|                     setDeletion({ open: false }); | ||||
|                 }} | ||||
|  | ||||
| @ -38,44 +38,10 @@ exports[`renders a list with elements correctly 1`] = ` | ||||
|       className="MuiList-root MuiList-padding" | ||||
|     > | ||||
|       <li | ||||
|         className="MuiListItem-root tagListItem MuiListItem-gutters" | ||||
|         className="MuiListItem-root MuiListItem-gutters" | ||||
|         disabled={false} | ||||
|       > | ||||
|         <div | ||||
|           className="MuiListItemIcon-root" | ||||
|         > | ||||
|           <svg | ||||
|             aria-hidden={true} | ||||
|             className="MuiSvgIcon-root" | ||||
|             focusable="false" | ||||
|             viewBox="0 0 24 24" | ||||
|           > | ||||
|             <path | ||||
|               d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z" | ||||
|             /> | ||||
|           </svg> | ||||
|         </div> | ||||
|         <div | ||||
|           className="MuiListItemText-root MuiListItemText-multiline" | ||||
|         > | ||||
|           <span | ||||
|             className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" | ||||
|           > | ||||
|             <a | ||||
|               href="/tag-types/edit/simple" | ||||
|               onClick={[Function]} | ||||
|             > | ||||
|               <strong> | ||||
|                 simple | ||||
|               </strong> | ||||
|             </a> | ||||
|           </span> | ||||
|           <p | ||||
|             className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock" | ||||
|           > | ||||
|             Some simple description | ||||
|           </p> | ||||
|         </div> | ||||
|         No entries | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
|  | ||||
| @ -14,21 +14,24 @@ import { | ||||
|     UPDATE_TAG_TYPE, | ||||
|     DELETE_TAG_TYPE, | ||||
| } from '../../providers/AccessProvider/permissions'; | ||||
| import UIProvider from '../../providers/UIProvider/UIProvider'; | ||||
| 
 | ||||
| test('renders an empty list correctly', () => { | ||||
|     const tree = renderer.create( | ||||
|         <MemoryRouter> | ||||
|             <ThemeProvider theme={theme}> | ||||
|                 <AccessProvider | ||||
|                     store={createFakeStore([{ permission: ADMIN }])} | ||||
|                 > | ||||
|                     <TagTypesList | ||||
|                         tagTypes={[]} | ||||
|                         fetchTagTypes={jest.fn()} | ||||
|                         removeTagType={jest.fn()} | ||||
|                         history={{}} | ||||
|                     /> | ||||
|                 </AccessProvider> | ||||
|                 <UIProvider> | ||||
|                     <AccessProvider | ||||
|                         store={createFakeStore([{ permission: ADMIN }])} | ||||
|                     > | ||||
|                         <TagTypesList | ||||
|                             tagTypes={[]} | ||||
|                             fetchTagTypes={jest.fn()} | ||||
|                             removeTagType={jest.fn()} | ||||
|                             history={{}} | ||||
|                         /> | ||||
|                     </AccessProvider> | ||||
|                 </UIProvider> | ||||
|             </ThemeProvider> | ||||
|         </MemoryRouter> | ||||
|     ); | ||||
| @ -39,26 +42,28 @@ test('renders a list with elements correctly', () => { | ||||
|     const tree = renderer.create( | ||||
|         <ThemeProvider theme={theme}> | ||||
|             <MemoryRouter> | ||||
|                 <AccessProvider | ||||
|                     store={createFakeStore([ | ||||
|                         { permission: CREATE_TAG_TYPE }, | ||||
|                         { permission: UPDATE_TAG_TYPE }, | ||||
|                         { permission: DELETE_TAG_TYPE }, | ||||
|                     ])} | ||||
|                 > | ||||
|                     <TagTypesList | ||||
|                         tagTypes={[ | ||||
|                             { | ||||
|                                 name: 'simple', | ||||
|                                 description: 'Some simple description', | ||||
|                                 icon: '#', | ||||
|                             }, | ||||
|                         ]} | ||||
|                         fetchTagTypes={jest.fn()} | ||||
|                         removeTagType={jest.fn()} | ||||
|                         history={{}} | ||||
|                     /> | ||||
|                 </AccessProvider> | ||||
|                 <UIProvider> | ||||
|                     <AccessProvider | ||||
|                         store={createFakeStore([ | ||||
|                             { permission: CREATE_TAG_TYPE }, | ||||
|                             { permission: UPDATE_TAG_TYPE }, | ||||
|                             { permission: DELETE_TAG_TYPE }, | ||||
|                         ])} | ||||
|                     > | ||||
|                         <TagTypesList | ||||
|                             tagTypes={[ | ||||
|                                 { | ||||
|                                     name: 'simple', | ||||
|                                     description: 'Some simple description', | ||||
|                                     icon: '#', | ||||
|                                 }, | ||||
|                             ]} | ||||
|                             fetchTagTypes={jest.fn()} | ||||
|                             removeTagType={jest.fn()} | ||||
|                             history={{}} | ||||
|                         /> | ||||
|                     </AccessProvider> | ||||
|                 </UIProvider> | ||||
|             </MemoryRouter> | ||||
|         </ThemeProvider> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,91 @@ | ||||
| import { useHistory } from 'react-router-dom'; | ||||
| import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||
| import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from '../../../hooks/useToast'; | ||||
| import FormTemplate from '../../common/FormTemplate/FormTemplate'; | ||||
| import PermissionButton from '../../common/PermissionButton/PermissionButton'; | ||||
| import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; | ||||
| import useTagForm from '../hooks/useTagForm'; | ||||
| import TagTypeForm from '../TagTypeForm/TagTypeForm'; | ||||
| 
 | ||||
| const CreateTagType = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const history = useHistory(); | ||||
|     const { | ||||
|         tagName, | ||||
|         tagDesc, | ||||
|         setTagName, | ||||
|         setTagDesc, | ||||
|         getTagPayload, | ||||
|         validateNameUniqueness, | ||||
|         errors, | ||||
|         clearErrors, | ||||
|     } = useTagForm(); | ||||
|     const { createTag, loading } = useTagTypesApi(); | ||||
| 
 | ||||
|     const handleSubmit = async (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         clearErrors(); | ||||
|         const validName = await validateNameUniqueness(); | ||||
|         if (validName) { | ||||
|             const payload = getTagPayload(); | ||||
|             try { | ||||
|                 await createTag(payload); | ||||
|                 history.push('/tag-types'); | ||||
|                 setToastData({ | ||||
|                     title: 'Tag type created', | ||||
|                     confetti: true, | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|             } catch (e: any) { | ||||
|                 setToastApiError(e.toString()); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
|         return `curl --location --request POST '${ | ||||
|             uiConfig.unleashUrl | ||||
|         }/api/admin/tag-types' \\ | ||||
| --header 'Authorization: INSERT_API_KEY' \\ | ||||
| --header 'Content-Type: application/json' \\ | ||||
| --data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
 | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         history.goBack(); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             loading={loading} | ||||
|             title="Create tag type" | ||||
|             description="Tag types allow you to group tags together in the management UI" | ||||
|             documentationLink="https://docs.getunleash.io/advanced/tags" | ||||
|             formatApiCode={formatApiCode} | ||||
|         > | ||||
|             <TagTypeForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 tagName={tagName} | ||||
|                 setTagName={setTagName} | ||||
|                 tagDesc={tagDesc} | ||||
|                 setTagDesc={setTagDesc} | ||||
|                 mode="Create" | ||||
|                 clearErrors={clearErrors} | ||||
|             > | ||||
|                 <PermissionButton | ||||
|                     onClick={handleSubmit} | ||||
|                     permission={UPDATE_TAG_TYPE} | ||||
|                     type="submit" | ||||
|                 > | ||||
|                     Create type | ||||
|                 </PermissionButton> | ||||
|             </TagTypeForm> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default CreateTagType; | ||||
							
								
								
									
										89
									
								
								frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| import { useHistory, useParams } from 'react-router-dom'; | ||||
| import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||
| import useTagType from '../../../hooks/api/getters/useTagType/useTagType'; | ||||
| import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from '../../../hooks/useToast'; | ||||
| import FormTemplate from '../../common/FormTemplate/FormTemplate'; | ||||
| import PermissionButton from '../../common/PermissionButton/PermissionButton'; | ||||
| import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; | ||||
| import useTagForm from '../hooks/useTagForm'; | ||||
| import TagForm from '../TagTypeForm/TagTypeForm'; | ||||
| 
 | ||||
| const EditTagType = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const history = useHistory(); | ||||
|     const { name } = useParams<{ name: string }>(); | ||||
|     const { tagType } = useTagType(name); | ||||
|     const { | ||||
|         tagName, | ||||
|         tagDesc, | ||||
|         setTagName, | ||||
|         setTagDesc, | ||||
|         getTagPayload, | ||||
|         errors, | ||||
|         clearErrors, | ||||
|     } = useTagForm(tagType?.name, tagType?.description); | ||||
|     const { updateTagType, loading } = useTagTypesApi(); | ||||
| 
 | ||||
|     const handleSubmit = async (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         clearErrors(); | ||||
|         const payload = getTagPayload(); | ||||
|         try { | ||||
|             await updateTagType(tagName, payload); | ||||
|             history.push('/tag-types'); | ||||
|             setToastData({ | ||||
|                 title: 'Tag type updated', | ||||
|                 type: 'success', | ||||
|             }); | ||||
|         } catch (e: any) { | ||||
|             setToastApiError(e.toString()); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
|         return `curl --location --request PUT '${ | ||||
|             uiConfig.unleashUrl | ||||
|         }/api/admin/tag-types/${name}' \\ | ||||
| --header 'Authorization: INSERT_API_KEY' \\ | ||||
| --header 'Content-Type: application/json' \\ | ||||
| --data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
 | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         history.goBack(); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             loading={loading} | ||||
|             title="Edit tag type" | ||||
|             description="Tag types allow you to group tags together in the management UI" | ||||
|             documentationLink="https://docs.getunleash.io/" | ||||
|             formatApiCode={formatApiCode} | ||||
|         > | ||||
|             <TagForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 tagName={tagName} | ||||
|                 setTagName={setTagName} | ||||
|                 tagDesc={tagDesc} | ||||
|                 setTagDesc={setTagDesc} | ||||
|                 mode="Edit" | ||||
|                 clearErrors={clearErrors} | ||||
|             > | ||||
|                 <PermissionButton | ||||
|                     onClick={handleSubmit} | ||||
|                     permission={UPDATE_TAG_TYPE} | ||||
|                     type="submit" | ||||
|                 > | ||||
|                     Edit type | ||||
|                 </PermissionButton> | ||||
|             </TagForm> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EditTagType; | ||||
| @ -0,0 +1,47 @@ | ||||
| import { makeStyles } from '@material-ui/core/styles'; | ||||
| 
 | ||||
| export const useStyles = makeStyles(theme => ({ | ||||
|     container: { | ||||
|         maxWidth: '400px', | ||||
|     }, | ||||
|     form: { | ||||
|         display: 'flex', | ||||
|         flexDirection: 'column', | ||||
|         height: '100%', | ||||
|     }, | ||||
|     input: { width: '100%', marginBottom: '1rem' }, | ||||
|     label: { | ||||
|         minWidth: '300px', | ||||
|         [theme.breakpoints.down(600)]: { | ||||
|             minWidth: 'auto', | ||||
|         }, | ||||
|     }, | ||||
|     buttonContainer: { | ||||
|         marginTop: 'auto', | ||||
|         display: 'flex', | ||||
|         justifyContent: 'flex-end', | ||||
|     }, | ||||
|     cancelButton: { | ||||
|         marginRight: '1.5rem', | ||||
|     }, | ||||
|     inputDescription: { | ||||
|         marginBottom: '0.5rem', | ||||
|     }, | ||||
|     formHeader: { | ||||
|         fontWeight: 'normal', | ||||
|         marginTop: '0', | ||||
|     }, | ||||
|     header: { | ||||
|         fontWeight: 'normal', | ||||
|     }, | ||||
|     permissionErrorContainer: { | ||||
|         position: 'relative', | ||||
|     }, | ||||
|     errorMessage: { | ||||
|         //@ts-ignore
 | ||||
|         fontSize: theme.fontSizes.smallBody, | ||||
|         color: theme.palette.error.main, | ||||
|         position: 'absolute', | ||||
|         top: '-8px', | ||||
|     }, | ||||
| })); | ||||
							
								
								
									
										77
									
								
								frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| import Input from '../../common/Input/Input'; | ||||
| import { TextField, Button } from '@material-ui/core'; | ||||
| 
 | ||||
| import { useStyles } from './TagTypeForm.styles'; | ||||
| import React from 'react'; | ||||
| import { trim } from '../../common/util'; | ||||
| import { EDIT } from '../../../constants/misc'; | ||||
| 
 | ||||
| interface ITagTypeForm { | ||||
|     tagName: string; | ||||
|     tagDesc: string; | ||||
|     setTagName: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setTagDesc: React.Dispatch<React.SetStateAction<string>>; | ||||
|     handleSubmit: (e: any) => void; | ||||
|     handleCancel: () => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     mode: string; | ||||
|     clearErrors: () => void; | ||||
| } | ||||
| 
 | ||||
| const TagTypeForm: React.FC<ITagTypeForm> = ({ | ||||
|     children, | ||||
|     handleSubmit, | ||||
|     handleCancel, | ||||
|     tagName, | ||||
|     tagDesc, | ||||
|     setTagName, | ||||
|     setTagDesc, | ||||
|     errors, | ||||
|     mode, | ||||
|     clearErrors, | ||||
| }) => { | ||||
|     const styles = useStyles(); | ||||
| 
 | ||||
|     return ( | ||||
|         <form onSubmit={handleSubmit} className={styles.form}> | ||||
|             <h3 className={styles.formHeader}>Tag information</h3> | ||||
| 
 | ||||
|             <div className={styles.container}> | ||||
|                 <p className={styles.inputDescription}> | ||||
|                     What is your tag name? | ||||
|                 </p> | ||||
|                 <Input | ||||
|                     className={styles.input} | ||||
|                     label="Tag name" | ||||
|                     value={tagName} | ||||
|                     onChange={e => setTagName(trim(e.target.value))} | ||||
|                     error={Boolean(errors.name)} | ||||
|                     errorText={errors.name} | ||||
|                     onFocus={() => clearErrors()} | ||||
|                     disabled={mode === EDIT} | ||||
|                 /> | ||||
| 
 | ||||
|                 <p className={styles.inputDescription}> | ||||
|                     What is this role for? | ||||
|                 </p> | ||||
|                 <TextField | ||||
|                     className={styles.input} | ||||
|                     label="Tag description" | ||||
|                     variant="outlined" | ||||
|                     multiline | ||||
|                     maxRows={4} | ||||
|                     value={tagDesc} | ||||
|                     onChange={e => setTagDesc(e.target.value)} | ||||
|                 /> | ||||
|             </div> | ||||
|             <div className={styles.buttonContainer}> | ||||
|                 <Button onClick={handleCancel} className={styles.cancelButton}> | ||||
|                     Cancel | ||||
|                 </Button> | ||||
|                 {children} | ||||
|             </div> | ||||
|         </form> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default TagTypeForm; | ||||
							
								
								
									
										69
									
								
								frontend/src/component/tagTypes/hooks/useTagForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								frontend/src/component/tagTypes/hooks/useTagForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||
| 
 | ||||
| const useTagForm = (initialTagName = '', initialTagDesc = '') => { | ||||
|     const [tagName, setTagName] = useState(initialTagName); | ||||
|     const [tagDesc, setTagDesc] = useState(initialTagDesc); | ||||
|     const [errors, setErrors] = useState({}); | ||||
|     const { validateTagName } = useTagTypesApi(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setTagName(initialTagName); | ||||
|     }, [initialTagName]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setTagDesc(initialTagDesc); | ||||
|     }, [initialTagDesc]); | ||||
| 
 | ||||
|     const getTagPayload = () => { | ||||
|         return { | ||||
|             name: tagName, | ||||
|             description: tagDesc, | ||||
|         }; | ||||
|     }; | ||||
| 
 | ||||
|     const NAME_EXISTS_ERROR = | ||||
|         'There already exists a tag-type with the name simple'; | ||||
|     const validateNameUniqueness = async () => { | ||||
|         if (tagName.length === 0) { | ||||
|             setErrors(prev => ({ ...prev, name: 'Name can not be empty.' })); | ||||
|             return false; | ||||
|         } | ||||
|         if (tagName.length < 2) { | ||||
|             setErrors(prev => ({ | ||||
|                 ...prev, | ||||
|                 name: 'Tag name length must be at least 2 characters long', | ||||
|             })); | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             await validateTagName(tagName); | ||||
|             return true; | ||||
|         } catch (e: any) { | ||||
|             if (e.toString().includes(NAME_EXISTS_ERROR)) { | ||||
|                 setErrors(prev => ({ | ||||
|                     ...prev, | ||||
|                     name: NAME_EXISTS_ERROR, | ||||
|                 })); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const clearErrors = () => { | ||||
|         setErrors({}); | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         tagName, | ||||
|         tagDesc, | ||||
|         setTagName, | ||||
|         setTagDesc, | ||||
|         getTagPayload, | ||||
|         clearErrors, | ||||
|         validateNameUniqueness, | ||||
|         errors, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useTagForm; | ||||
| @ -50,6 +50,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | ||||
|                 <Label /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary={tag.value} secondary={tag.type} /> | ||||
| 
 | ||||
|             <ConditionallyRender | ||||
|                 condition={hasAccess(DELETE_TAG)} | ||||
|                 show={<DeleteButton tagType={tag.type} tagValue={tag.value} />} | ||||
| @ -81,7 +82,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | ||||
|                     show={ | ||||
|                         <IconButton | ||||
|                             aria-label="add tag" | ||||
|                             onClick={() => history.push('/tags/create')} | ||||
|                             onClick={() => history.push('/tag-types/create')} | ||||
|                         > | ||||
|                             <Add /> | ||||
|                         </IconButton> | ||||
| @ -91,7 +92,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | ||||
|                             <Button | ||||
|                                 color="primary" | ||||
|                                 startIcon={<Add />} | ||||
|                                 onClick={() => history.push('/tags/create')} | ||||
|                                 onClick={() => | ||||
|                                     history.push('/tag-types/create') | ||||
|                                 } | ||||
|                                 variant="contained" | ||||
|                             > | ||||
|                                 Add new tag | ||||
|  | ||||
| @ -1,10 +1,64 @@ | ||||
| import useAPI from '../useApi/useApi'; | ||||
| 
 | ||||
| interface ICreatePayload { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     description: string; | ||||
| } | ||||
| 
 | ||||
| const useProjectApi = () => { | ||||
|     const { makeRequest, createRequest, errors } = useAPI({ | ||||
|     const { makeRequest, createRequest, errors, loading } = useAPI({ | ||||
|         propagateErrors: true, | ||||
|     }); | ||||
| 
 | ||||
|     const createProject = async (payload: ICreatePayload) => { | ||||
|         const path = `api/admin/projects`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'POST', | ||||
|             body: JSON.stringify(payload), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const validateId = async (payload: ICreatePayload) => { | ||||
|         const path = `api/admin/projects/validate`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'POST', | ||||
|             body: JSON.stringify(payload), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const editProject = async (id: string, payload: ICreatePayload) => { | ||||
|         const path = `api/admin/projects/${id}`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'PUT', | ||||
|             body: JSON.stringify(payload), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const deleteProject = async (projectId: string) => { | ||||
|         const path = `api/admin/projects/${projectId}`; | ||||
|         const req = createRequest(path, { method: 'DELETE' }); | ||||
| @ -18,7 +72,10 @@ const useProjectApi = () => { | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const addEnvironmentToProject = async (projectId: string, environment: string) => { | ||||
|     const addEnvironmentToProject = async ( | ||||
|         projectId: string, | ||||
|         environment: string | ||||
|     ) => { | ||||
|         const path = `api/admin/projects/${projectId}/environments`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'POST', | ||||
| @ -32,9 +89,12 @@ const useProjectApi = () => { | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     const removeEnvironmentFromProject = async (projectId: string, environment: string) => { | ||||
|     const removeEnvironmentFromProject = async ( | ||||
|         projectId: string, | ||||
|         environment: string | ||||
|     ) => { | ||||
|         const path = `api/admin/projects/${projectId}/environments/${environment}`; | ||||
|         const req = createRequest(path, { method: 'DELETE' }); | ||||
| 
 | ||||
| @ -45,9 +105,18 @@ const useProjectApi = () => { | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     return { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, errors }; | ||||
|     return { | ||||
|         createProject, | ||||
|         validateId, | ||||
|         editProject, | ||||
|         deleteProject, | ||||
|         addEnvironmentToProject, | ||||
|         removeEnvironmentFromProject, | ||||
|         errors, | ||||
|         loading, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useProjectApi; | ||||
|  | ||||
| @ -0,0 +1,75 @@ | ||||
| import { ITagPayload } from '../../../../interfaces/tags'; | ||||
| import useAPI from '../useApi/useApi'; | ||||
| 
 | ||||
| const useTagTypesApi = () => { | ||||
|     const { makeRequest, createRequest, errors, loading } = useAPI({ | ||||
|         propagateErrors: true, | ||||
|     }); | ||||
| 
 | ||||
|     const createTag = async (payload: ITagPayload) => { | ||||
|         const path = `api/admin/tag-types`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'POST', | ||||
|             body: JSON.stringify(payload), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const validateTagName = async (name: string) => { | ||||
|         const path = `api/admin/tag-types/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 updateTagType = async (tagName: string, payload: ITagPayload) => { | ||||
|         const path = `api/admin/tag-types/${tagName}`; | ||||
|         const req = createRequest(path, { | ||||
|             method: 'PUT', | ||||
|             body: JSON.stringify(payload), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const deleteTagType = async (tagName: string) => { | ||||
|         const path = `api/admin/tag-types/${tagName}`; | ||||
|         const req = createRequest(path, { method: 'DELETE' }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         createTag, | ||||
|         validateTagName, | ||||
|         updateTagType, | ||||
|         deleteTagType, | ||||
|         errors, | ||||
|         loading | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useTagTypesApi; | ||||
							
								
								
									
										41
									
								
								frontend/src/hooks/api/getters/useTagType/useTagType.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/hooks/api/getters/useTagType/useTagType.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| import useSWR, { mutate, SWRConfiguration } from 'swr'; | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { formatApiPath } from '../../../../utils/format-path'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | ||||
| 
 | ||||
| const useTagType = (name: string, options: SWRConfiguration = {}) => { | ||||
|     const fetcher = async () => { | ||||
|         const path = formatApiPath(`api/admin/tag-types/${name}`); | ||||
|         return fetch(path, { | ||||
|             method: 'GET', | ||||
|         }) | ||||
|             .then(handleErrorResponses('Tag data')) | ||||
|             .then(res => res.json()); | ||||
|     }; | ||||
| 
 | ||||
|     const FEATURE_CACHE_KEY = `api/admin/tag-types/${name}`; | ||||
| 
 | ||||
|     const { data, error } = useSWR(FEATURE_CACHE_KEY, fetcher, { | ||||
|         ...options, | ||||
|     }); | ||||
| 
 | ||||
|     const [loading, setLoading] = useState(!error && !data); | ||||
| 
 | ||||
|     const refetch = () => { | ||||
|         mutate(FEATURE_CACHE_KEY); | ||||
|     }; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setLoading(!error && !data); | ||||
|     }, [data, error]); | ||||
| 
 | ||||
|     return { | ||||
|         tagType: data?.tagType || { name: '', description: '' }, | ||||
|         error, | ||||
|         loading, | ||||
|         refetch, | ||||
|         FEATURE_CACHE_KEY, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useTagType; | ||||
| @ -15,6 +15,8 @@ interface IToastOptions { | ||||
|     type: string; | ||||
|     persist?: boolean; | ||||
|     confetti?: boolean; | ||||
|     autoHideDuration?: number; | ||||
|     show?: boolean; | ||||
| } | ||||
| 
 | ||||
| const useToast = () => { | ||||
|  | ||||
| @ -8,3 +8,8 @@ export interface ITagType { | ||||
|     description: string; | ||||
|     icon: string; | ||||
| } | ||||
| 
 | ||||
| export interface ITagPayload { | ||||
|     name: string; | ||||
|     description: string; | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,6 @@ | ||||
|     "skipLibCheck": true, | ||||
|     "esModuleInterop": true, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "strict": true, | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "module": "esnext", | ||||
| @ -18,7 +17,8 @@ | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "noEmit": true, | ||||
|     "jsx": "react-jsx" | ||||
|     "jsx": "react-jsx", | ||||
|     "strict": true | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src" | ||||
|  | ||||
| @ -2037,6 +2037,11 @@ | ||||
|   resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz" | ||||
|   integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== | ||||
| 
 | ||||
| "@types/history@^4.7.11": | ||||
|   version "4.7.11" | ||||
|   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" | ||||
|   integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== | ||||
| 
 | ||||
| "@types/hoist-non-react-statics@^3.3.0": | ||||
|   version "3.3.1" | ||||
|   resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" | ||||
| @ -2112,10 +2117,10 @@ | ||||
|   resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz" | ||||
|   integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== | ||||
| 
 | ||||
| "@types/node@14.18.7": | ||||
|   version "14.18.7" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.7.tgz#bf973dbd8e156dbf860504a8811033cbd26967d1" | ||||
|   integrity sha512-UpLEO1iBG7esNPusSAjoZhWFK5Mfd8QfwWhHRrg5io13POn/stsBgTCba9suQaFflNA4tc0+6AFM3R6BZNng6A== | ||||
| "@types/node@14.18.9": | ||||
|   version "14.18.9" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.9.tgz#0e5944eefe2b287391279a19b407aa98bd14436d" | ||||
|   integrity sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q== | ||||
| 
 | ||||
| "@types/node@^14.14.31": | ||||
|   version "14.17.19" | ||||
| @ -2164,12 +2169,12 @@ | ||||
|     hoist-non-react-statics "^3.3.0" | ||||
|     redux "^4.0.0" | ||||
| 
 | ||||
| "@types/react-router-dom@5.3.2": | ||||
|   version "5.3.2" | ||||
|   resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.2.tgz" | ||||
|   integrity sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ== | ||||
| "@types/react-router-dom@5.3.3": | ||||
|   version "5.3.3" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" | ||||
|   integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== | ||||
|   dependencies: | ||||
|     "@types/history" "*" | ||||
|     "@types/history" "^4.7.11" | ||||
|     "@types/react" "*" | ||||
|     "@types/react-router" "*" | ||||
| 
 | ||||
| @ -11265,10 +11270,10 @@ sass-loader@^10.0.5: | ||||
|     schema-utils "^3.0.0" | ||||
|     semver "^7.3.2" | ||||
| 
 | ||||
| sass@1.48.0: | ||||
|   version "1.48.0" | ||||
|   resolved "https://registry.yarnpkg.com/sass/-/sass-1.48.0.tgz#b53cfccc1b8ab4be375cc54f306fda9d4711162c" | ||||
|   integrity sha512-hQi5g4DcfjcipotoHZ80l7GNJHGqQS5LwMBjVYB/TaT0vcSSpbgM8Ad7cgfsB2M0MinbkEQQPO9+sjjSiwxqmw== | ||||
| sass@1.49.0: | ||||
|   version "1.49.0" | ||||
|   resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078" | ||||
|   integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw== | ||||
|   dependencies: | ||||
|     chokidar ">=3.0.0 <4.0.0" | ||||
|     immutable "^4.0.0" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user