mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: add create and edit screen for tag-types (NEW) (#603)
* feat: add create and edit screen for tag-types * feat: update Edit and create component with permissions * refactor: add TagForm type to react FC * fix: routes * fix: add edit button * fix: update snapshot * fix: update permission * fix: permission Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
80e80805f7
commit
7baf8400ca
@ -263,15 +263,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",
|
||||
|
@ -15,10 +15,6 @@ 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 +44,8 @@ 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';
|
||||
|
||||
export const routes = [
|
||||
// Project
|
||||
@ -304,24 +302,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
|
||||
{
|
||||
|
@ -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,11 +14,13 @@ 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}>
|
||||
<UIProvider>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
@ -29,6 +31,7 @@ test('renders an empty list correctly', () => {
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -39,6 +42,7 @@ test('renders a list with elements correctly', () => {
|
||||
const tree = renderer.create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<UIProvider>
|
||||
<AccessProvider
|
||||
store={createFakeStore([
|
||||
{ permission: CREATE_TAG_TYPE },
|
||||
@ -59,6 +63,7 @@ test('renders a list with elements correctly', () => {
|
||||
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
|
||||
|
@ -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;
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user