Fix: Misc issues for adding Tags, Teams and Service (#2149)

* Fix: Misc issues for adding Tags, Teams and Service

* Addressing comments
This commit is contained in:
darth-coder00 2022-01-11 15:06:46 +05:30 committed by GitHub
parent f379b35279
commit cbb0b837c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 69 deletions

View File

@ -24,6 +24,7 @@ import {
import { DatabaseService } from '../../../generated/entity/services/databaseService'; import { DatabaseService } from '../../../generated/entity/services/databaseService';
import { MessagingService } from '../../../generated/entity/services/messagingService'; import { MessagingService } from '../../../generated/entity/services/messagingService';
import { PipelineService } from '../../../generated/entity/services/pipelineService'; import { PipelineService } from '../../../generated/entity/services/pipelineService';
import { errorMsg } from '../../../utils/CommonUtils';
// import { fromISOString } from '../../../utils/ServiceUtils'; // import { fromISOString } from '../../../utils/ServiceUtils';
import { Button } from '../../buttons/Button/Button'; import { Button } from '../../buttons/Button/Button';
import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview'; import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview';
@ -161,14 +162,6 @@ const seprateUrl = (url?: string) => {
return {}; return {};
}; };
const errorMsg = (value: string) => {
return (
<div className="tw-mt-1">
<strong className="tw-text-red-500 tw-text-xs tw-italic">{value}</strong>
</div>
);
};
export const AddServiceModal: FunctionComponent<Props> = ({ export const AddServiceModal: FunctionComponent<Props> = ({
header, header,
serviceName, serviceName,
@ -338,7 +331,7 @@ export const AddServiceModal: FunctionComponent<Props> = ({
const handleSave = () => { const handleSave = () => {
let setMsg: ErrorMsg = { let setMsg: ErrorMsg = {
selectService: !selectService, selectService: !selectService,
name: !name, name: !name.trim(),
}; };
switch (serviceName) { switch (serviceName) {
case ServiceCategory.DATABASE_SERVICES: case ServiceCategory.DATABASE_SERVICES:

View File

@ -11,27 +11,31 @@
* limitations under the License. * limitations under the License.
*/ */
import { Team } from 'Models'; import { FormErrorData, Team } from 'Models';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { TagsCategory } from '../../../pages/tags/tagsTypes'; import { TagsCategory } from '../../../pages/tags/tagsTypes';
import { Button } from '../../buttons/Button/Button'; import { Button } from '../../buttons/Button/Button';
type FormData = TagsCategory | Team; type FormData = TagsCategory | Team;
type FormModalProp = { type FormModalProp = {
onCancel: () => void; onCancel: () => void;
onSave: (data: TagsCategory) => void; onChange?: (data: TagsCategory | Team) => void;
onSave: (data: TagsCategory | Team) => void;
form: React.ElementType; form: React.ElementType;
header: string; header: string;
initialData: FormData; initialData: FormData;
errorData?: FormErrorData;
}; };
type FormRef = { type FormRef = {
fetchMarkDownData: () => string; fetchMarkDownData: () => string;
}; };
const FormModal = ({ const FormModal = ({
onCancel, onCancel,
onChange,
onSave, onSave,
form: Form, form: Form,
header, header,
initialData, initialData,
errorData,
}: FormModalProp) => { }: FormModalProp) => {
const formRef = useRef<FormRef>(); const formRef = useRef<FormRef>();
const [data, setData] = useState<FormData>(initialData); const [data, setData] = useState<FormData>(initialData);
@ -57,7 +61,15 @@ const FormModal = ({
</p> </p>
</div> </div>
<div className="tw-modal-body"> <div className="tw-modal-body">
<Form initialData={initialData} ref={formRef} saveData={setData} /> <Form
errorData={errorData}
initialData={initialData}
ref={formRef}
saveData={(data: TagsCategory | Team) => {
setData(data);
onChange && onChange(data);
}}
/>
</div> </div>
<div className="tw-modal-footer" data-testid="cta-container"> <div className="tw-modal-footer" data-testid="cta-container">
<Button <Button

View File

@ -422,4 +422,8 @@ declare module 'Models' {
openInNewTab?: boolean; openInNewTab?: boolean;
showLabel?: boolean; showLabel?: boolean;
}; };
export interface FormErrorData {
[key: string]: string | undefined;
}
} }

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import PropTypes from 'prop-types'; import { FormErrorData } from 'Models';
import React, { import React, {
forwardRef, forwardRef,
useEffect, useEffect,
@ -21,6 +21,7 @@ import React, {
} from 'react'; } from 'react';
import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview'; import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview';
import { CreateTagCategory } from '../../generated/api/tags/createTagCategory'; import { CreateTagCategory } from '../../generated/api/tags/createTagCategory';
import { errorMsg } from '../../utils/CommonUtils';
type CustomTagCategory = { type CustomTagCategory = {
categoryType: string; categoryType: string;
@ -31,18 +32,22 @@ type CustomTagCategory = {
type FormProp = { type FormProp = {
saveData: (value: CreateTagCategory) => void; saveData: (value: CreateTagCategory) => void;
initialData: CustomTagCategory; initialData: CustomTagCategory;
errorData?: FormErrorData;
}; };
type EditorContentRef = { type EditorContentRef = {
getEditorContent: () => string; getEditorContent: () => string;
}; };
const Form: React.FC<FormProp> = forwardRef( const Form: React.FC<FormProp> = forwardRef(
({ saveData, initialData }, ref): JSX.Element => { ({ saveData, initialData, errorData }: FormProp, ref): JSX.Element => {
const [data, setData] = useState<CustomTagCategory>({ const [data, setData] = useState<CustomTagCategory>({
name: initialData.name, name: initialData.name,
description: initialData.description, description: initialData.description,
categoryType: initialData.categoryType, categoryType: initialData.categoryType,
}); });
const isMounting = useRef<boolean>(true);
const markdownRef = useRef<EditorContentRef>(); const markdownRef = useRef<EditorContentRef>();
const onChangeHadler = ( const onChangeHadler = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => { ) => {
@ -62,11 +67,18 @@ const Form: React.FC<FormProp> = forwardRef(
})); }));
useEffect(() => { useEffect(() => {
saveData({ if (!isMounting.current) {
...(data as CreateTagCategory), saveData({
}); ...(data as CreateTagCategory),
});
}
}, [data]); }, [data]);
// alwyas Keep this useEffect at the end...
useEffect(() => {
isMounting.current = false;
}, []);
return ( return (
<div className="tw-w-full tw-flex "> <div className="tw-w-full tw-flex ">
<div className="tw-flex tw-w-full"> <div className="tw-flex tw-w-full">
@ -92,7 +104,6 @@ const Form: React.FC<FormProp> = forwardRef(
<div className="tw-mb-4"> <div className="tw-mb-4">
<label className="tw-form-label required-field">Name</label> <label className="tw-form-label required-field">Name</label>
<input <input
required
autoComplete="off" autoComplete="off"
className="tw-text-sm tw-appearance-none tw-border tw-border-main className="tw-text-sm tw-appearance-none tw-border tw-border-main
tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight
@ -103,6 +114,7 @@ const Form: React.FC<FormProp> = forwardRef(
value={data.name} value={data.name}
onChange={onChangeHadler} onChange={onChangeHadler}
/> />
{errorData?.name && errorMsg(errorData.name)}
</div> </div>
<div> <div>
<label className="tw-form-label required-field"> <label className="tw-form-label required-field">
@ -117,13 +129,4 @@ const Form: React.FC<FormProp> = forwardRef(
} }
); );
Form.propTypes = {
saveData: PropTypes.func.isRequired,
initialData: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
categoryType: PropTypes.string.isRequired,
}).isRequired,
};
export default Form; export default Form;

View File

@ -13,7 +13,8 @@
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
import { EntityTags } from 'Models'; import { isUndefined } from 'lodash';
import { EntityTags, FormErrorData } from 'Models';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
@ -64,6 +65,8 @@ const TagsPage = () => {
const [editTag, setEditTag] = useState<TagClass>(); const [editTag, setEditTag] = useState<TagClass>();
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [errorDataCategory, setErrorDataCategory] = useState<FormErrorData>();
const [errorDataTag, setErrorDataTag] = useState<FormErrorData>();
const fetchCategories = () => { const fetchCategories = () => {
setIsLoading(true); setIsLoading(true);
@ -97,13 +100,36 @@ const TagsPage = () => {
} }
}; };
const createCategory = (data: CreateTagCategory) => { const onNewCategoryChange = (data: CreateTagCategory, forceSet = false) => {
createTagCategory(data).then((res: AxiosResponse) => { if (errorDataCategory || forceSet) {
if (res.data) { const errData: { [key: string]: string } = {};
fetchCategories(); if (!data.name.trim()) {
errData['name'] = 'Name is required';
} else if (/\s/g.test(data.name)) {
errData['name'] = 'Name with space is not allowed';
} else if (
!isUndefined(categories.find((item) => item.name === data.name))
) {
errData['name'] = 'Name already exists';
} }
}); setErrorDataCategory(errData);
setIsAddingCategory(false);
return errData;
}
return {};
};
const createCategory = (data: CreateTagCategory) => {
const errData = onNewCategoryChange(data, true);
if (!Object.values(errData).length) {
createTagCategory(data).then((res: AxiosResponse) => {
if (res.data) {
fetchCategories();
}
});
setIsAddingCategory(false);
}
}; };
const UpdateCategory = (updatedHTML: string) => { const UpdateCategory = (updatedHTML: string) => {
@ -119,16 +145,43 @@ const TagsPage = () => {
setIsEditCategory(false); setIsEditCategory(false);
}; };
const createPrimaryTag = (data: TagCategory) => { const onNewTagChange = (data: TagCategory, forceSet = false) => {
createTag(currentCategory?.name, { if (errorDataTag || forceSet) {
name: data.name, const errData: { [key: string]: string } = {};
description: data.description, if (!data.name.trim()) {
}).then((res: AxiosResponse) => { errData['name'] = 'Name is required';
if (res.data) { } else if (/\s/g.test(data.name)) {
fetchCurrentCategory(currentCategory?.name as string, true); errData['name'] = 'Name with space is not allowed';
} else if (
!isUndefined(
currentCategory?.children?.find(
(item) => (item as TagClass)?.name === data.name
)
)
) {
errData['name'] = 'Name already exists';
} }
}); setErrorDataTag(errData);
setIsAddingTag(false);
return errData;
}
return {};
};
const createPrimaryTag = (data: TagCategory) => {
const errData = onNewTagChange(data, true);
if (!Object.values(errData).length) {
createTag(currentCategory?.name, {
name: data.name,
description: data.description,
}).then((res: AxiosResponse) => {
if (res.data) {
fetchCurrentCategory(currentCategory?.name as string, true);
}
});
setIsAddingTag(false);
}
}; };
const updatePrimaryTag = (updatedHTML: string) => { const updatePrimaryTag = (updatedHTML: string) => {
updateTag(currentCategory?.name, editTag?.name, { updateTag(currentCategory?.name, editTag?.name, {
@ -187,7 +240,10 @@ const TagsPage = () => {
size="small" size="small"
theme="primary" theme="primary"
variant="contained" variant="contained"
onClick={() => setIsAddingCategory((prevState) => !prevState)}> onClick={() => {
setIsAddingCategory((prevState) => !prevState);
setErrorDataCategory(undefined);
}}>
<i aria-hidden="true" className="fa fa-plus" /> <i aria-hidden="true" className="fa fa-plus" />
</Button> </Button>
</NonAdminAction> </NonAdminAction>
@ -250,9 +306,10 @@ const TagsPage = () => {
size="small" size="small"
theme="primary" theme="primary"
variant="contained" variant="contained"
onClick={() => onClick={() => {
setIsAddingTag((prevState) => !prevState) setIsAddingTag((prevState) => !prevState);
}> setErrorDataTag(undefined);
}}>
Add new tag Add new tag
</Button> </Button>
</NonAdminAction> </NonAdminAction>
@ -394,9 +451,8 @@ const TagsPage = () => {
/> />
</button> </button>
) : ( ) : (
<span className="tw-opacity-0 group-hover:tw-opacity-100"> <span className="tw-opacity-60 group-hover:tw-opacity-100 tw-text-grey-muted group-hover:tw-text-primary">
<Tags <Tags
className="tw-border-main"
startWith="+ " startWith="+ "
tag="Add tag" tag="Add tag"
type="outlined" type="outlined"
@ -427,6 +483,7 @@ const TagsPage = () => {
)} )}
{isAddingCategory && ( {isAddingCategory && (
<FormModal <FormModal
errorData={errorDataCategory}
form={Form} form={Form}
header="Adding new category" header="Adding new category"
initialData={{ initialData={{
@ -435,11 +492,15 @@ const TagsPage = () => {
categoryType: TagCategoryType.Descriptive, categoryType: TagCategoryType.Descriptive,
}} }}
onCancel={() => setIsAddingCategory(false)} onCancel={() => setIsAddingCategory(false)}
onChange={(data) =>
onNewCategoryChange(data as TagCategory)
}
onSave={(data) => createCategory(data as TagCategory)} onSave={(data) => createCategory(data as TagCategory)}
/> />
)} )}
{isAddingTag && ( {isAddingTag && (
<FormModal <FormModal
errorData={errorDataTag}
form={Form} form={Form}
header={`Adding new tag on ${currentCategory?.name}`} header={`Adding new tag on ${currentCategory?.name}`}
initialData={{ initialData={{
@ -448,6 +509,7 @@ const TagsPage = () => {
categoryType: '', categoryType: '',
}} }}
onCancel={() => setIsAddingTag(false)} onCancel={() => setIsAddingTag(false)}
onChange={(data) => onNewTagChange(data as TagCategory)}
onSave={(data) => createPrimaryTag(data as TagCategory)} onSave={(data) => createPrimaryTag(data as TagCategory)}
/> />
)} )}

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Team } from 'Models'; import { FormErrorData, Team } from 'Models';
import React, { import React, {
forwardRef, forwardRef,
useEffect, useEffect,
@ -20,16 +20,18 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview'; import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview';
import { errorMsg } from '../../utils/CommonUtils';
type FormProp = { type FormProp = {
saveData: (value: {}) => void; saveData: (value: {}) => void;
initialData: Team; initialData: Team;
errorData?: FormErrorData;
}; };
type EditorContentRef = { type EditorContentRef = {
getEditorContent: () => string; getEditorContent: () => string;
}; };
const Form: React.FC<FormProp> = forwardRef( const Form: React.FC<FormProp> = forwardRef(
({ saveData, initialData }: FormProp, ref): JSX.Element => { ({ saveData, initialData, errorData }: FormProp, ref): JSX.Element => {
const [data, setData] = useState<Team>({ const [data, setData] = useState<Team>({
name: initialData.name, name: initialData.name,
description: initialData.description, description: initialData.description,
@ -40,6 +42,7 @@ const Form: React.FC<FormProp> = forwardRef(
users: initialData.users || [], users: initialData.users || [],
}); });
const isMounting = useRef<boolean>(true);
const markdownRef = useRef<EditorContentRef>(); const markdownRef = useRef<EditorContentRef>();
const onChangeHadler = ( const onChangeHadler = (
@ -60,11 +63,21 @@ const Form: React.FC<FormProp> = forwardRef(
})); }));
useEffect(() => { useEffect(() => {
saveData({ if (!isMounting.current) {
...data, saveData({
}); ...data,
name: data.name.trim(),
displayName: data.displayName.trim(),
description: data.description.trim(),
});
}
}, [data]); }, [data]);
// alwyas Keep this useEffect at the end...
useEffect(() => {
isMounting.current = false;
}, []);
return ( return (
<div className="tw-w-full tw-flex "> <div className="tw-w-full tw-flex ">
<div className="tw-flex tw-w-full"> <div className="tw-flex tw-w-full">
@ -72,7 +85,6 @@ const Form: React.FC<FormProp> = forwardRef(
<div className="tw-mb-4"> <div className="tw-mb-4">
<label className="tw-form-label required-field">Name</label> <label className="tw-form-label required-field">Name</label>
<input <input
required
autoComplete="off" autoComplete="off"
className="tw-form-inputs tw-px-3 tw-py-1" className="tw-form-inputs tw-px-3 tw-py-1"
name="name" name="name"
@ -81,13 +93,13 @@ const Form: React.FC<FormProp> = forwardRef(
value={data.name} value={data.name}
onChange={onChangeHadler} onChange={onChangeHadler}
/> />
{errorData?.name && errorMsg(errorData.name)}
</div> </div>
<div className="tw-mb-4"> <div className="tw-mb-4">
<label className="tw-form-label required-field"> <label className="tw-form-label required-field">
Display name Display name
</label> </label>
<input <input
required
autoComplete="off" autoComplete="off"
className="tw-form-inputs tw-px-3 tw-py-1" className="tw-form-inputs tw-px-3 tw-py-1"
name="displayName" name="displayName"
@ -96,11 +108,10 @@ const Form: React.FC<FormProp> = forwardRef(
value={data.displayName} value={data.displayName}
onChange={onChangeHadler} onChange={onChangeHadler}
/> />
{errorData?.displayName && errorMsg(errorData.displayName)}
</div> </div>
<div> <div>
<label className="tw-form-label required-field"> <label className="tw-form-label">Description</label>
Description
</label>
<MarkdownWithPreview ref={markdownRef} value={data.description} /> <MarkdownWithPreview ref={markdownRef} value={data.description} />
</div> </div>
</div> </div>

View File

@ -14,7 +14,9 @@
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
import { compare } from 'fast-json-patch'; import { compare } from 'fast-json-patch';
import { isUndefined, toLower } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { FormErrorData } from 'Models';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom'; import { Link, useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState'; import AppState from '../../AppState';
@ -40,6 +42,7 @@ import {
import { Team } from '../../generated/entity/teams/team'; import { Team } from '../../generated/entity/teams/team';
import { User } from '../../generated/entity/teams/user'; import { User } from '../../generated/entity/teams/user';
import { useAuth } from '../../hooks/authHooks'; import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
import { UserTeam } from '../../interface/team.interface'; import { UserTeam } from '../../interface/team.interface';
import { getActiveCatClass, getCountBadge } from '../../utils/CommonUtils'; import { getActiveCatClass, getCountBadge } from '../../utils/CommonUtils';
import AddUsersModal from './AddUsersModal'; import AddUsersModal from './AddUsersModal';
@ -59,6 +62,9 @@ const TeamsPage = () => {
const [isAddingTeam, setIsAddingTeam] = useState<boolean>(false); const [isAddingTeam, setIsAddingTeam] = useState<boolean>(false);
const [isAddingUsers, setIsAddingUsers] = useState<boolean>(false); const [isAddingUsers, setIsAddingUsers] = useState<boolean>(false);
const [userList, setUserList] = useState<Array<User>>([]); const [userList, setUserList] = useState<Array<User>>([]);
const [errorData, setErrorData] = useState<FormErrorData>();
const showToast = useToastContext();
const fetchTeams = () => { const fetchTeams = () => {
setIsLoading(true); setIsLoading(true);
@ -100,16 +106,48 @@ const TeamsPage = () => {
} }
}; };
const onNewDataChange = (data: Team, forceSet = false) => {
if (errorData || forceSet) {
const errData: { [key: string]: string } = {};
if (!data.name.trim()) {
errData['name'] = 'Name is required';
} else if (
!isUndefined(
teams.find((item) => toLower(item.name) === toLower(data.name))
)
) {
errData['name'] = 'Name already exists';
}
if (!data.displayName?.trim()) {
errData['displayName'] = 'Display name is required';
}
setErrorData(errData);
return errData;
}
return {};
};
const createNewTeam = (data: Team) => { const createNewTeam = (data: Team) => {
createTeam(data) const errData = onNewDataChange(data, true);
.then((res: AxiosResponse) => { if (!Object.values(errData).length) {
if (res.data) { createTeam(data)
fetchTeams(); .then((res: AxiosResponse) => {
} if (res.data) {
}) fetchTeams();
.finally(() => { }
setIsAddingTeam(false); })
}); .catch((error: AxiosError) => {
showToast({
variant: 'error',
body: error.message ?? 'Something went wrong!',
});
})
.finally(() => {
setIsAddingTeam(false);
});
}
}; };
const createUsers = (data: Array<UserTeam>) => { const createUsers = (data: Array<UserTeam>) => {
@ -280,7 +318,10 @@ const TeamsPage = () => {
size="small" size="small"
theme="primary" theme="primary"
variant="contained" variant="contained"
onClick={() => setIsAddingTeam(true)}> onClick={() => {
setErrorData(undefined);
setIsAddingTeam(true);
}}>
<i aria-hidden="true" className="fa fa-plus" /> <i aria-hidden="true" className="fa fa-plus" />
</Button> </Button>
</NonAdminAction> </NonAdminAction>
@ -440,6 +481,7 @@ const TeamsPage = () => {
{isAddingTeam && ( {isAddingTeam && (
<FormModal <FormModal
errorData={errorData}
form={Form} form={Form}
header="Adding new team" header="Adding new team"
initialData={{ initialData={{
@ -448,6 +490,7 @@ const TeamsPage = () => {
displayName: '', displayName: '',
}} }}
onCancel={() => setIsAddingTeam(false)} onCancel={() => setIsAddingTeam(false)}
onChange={(data) => onNewDataChange(data as Team)}
onSave={(data) => createNewTeam(data as Team)} onSave={(data) => createNewTeam(data as Team)}
/> />
)} )}

View File

@ -278,3 +278,11 @@ export const getOwnerIds = (
export const getActiveCatClass = (name: string, activeName = '') => { export const getActiveCatClass = (name: string, activeName = '') => {
return activeName === name ? 'activeCategory' : ''; return activeName === name ? 'activeCategory' : '';
}; };
export const errorMsg = (value: string) => {
return (
<div className="tw-mt-1">
<strong className="tw-text-red-500 tw-text-xs tw-italic">{value}</strong>
</div>
);
};