feat: Ability to tag multiple keywords at once

fixes #54
This commit is contained in:
towfiqi
2023-01-11 21:29:52 +06:00
parent 8139e399c1
commit 9e9dad7631
4 changed files with 101 additions and 45 deletions

View File

@@ -0,0 +1,60 @@
import { useState } from 'react';
import { useUpdateKeywordTags } from '../../services/keywords';
import Icon from '../common/Icon';
import Modal from '../common/Modal';
type AddTagsProps = {
keywords: KeywordType[],
closeModal: Function
}
const AddTags = ({ keywords = [], closeModal }: AddTagsProps) => {
const [tagInput, setTagInput] = useState('');
const [inputError, setInputError] = useState('');
const { mutate: updateMutate } = useUpdateKeywordTags(() => { setTagInput(''); });
const addTag = () => {
if (keywords.length === 0) { return; }
if (!tagInput) {
setInputError('Please Insert a Tag!');
setTimeout(() => { setInputError(''); }, 3000);
return;
}
const tagsArray = tagInput.split(',').map((t) => t.trim());
const tagsPayload:any = {};
keywords.forEach((keyword:KeywordType) => {
tagsPayload[keyword.ID] = [...keyword.tags, ...tagsArray];
});
updateMutate({ tags: tagsPayload });
};
return (
<Modal closeModal={() => { closeModal(false); }} title={`Add New Tags to ${keywords.length} Selected Keyword`}>
<div className="relative">
{inputError && <span className="absolute top-[-24px] text-red-400 text-sm font-semibold">{inputError}</span>}
<span className='absolute text-gray-400 top-3 left-2'><Icon type="tags" size={16} /></span>
<input
className='w-full border rounded border-gray-200 py-3 px-4 pl-8 outline-none focus:border-indigo-300'
placeholder='Insert Tags. eg: tag1, tag2'
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.code === 'Enter') {
e.preventDefault();
addTag();
}
}}
/>
<button
className=" absolute right-2 top-2 cursor-pointer rounded p-2 px-4 bg-indigo-600 text-white font-semibold text-sm"
onClick={addTag}>
Apply
</button>
</div>
</Modal>
);
};
export default AddTags;

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useUpdateKeywordTags } from '../../services/keywords';
import Icon from '../common/Icon';
import Modal from '../common/Modal';
import AddTags from './AddTags';
type keywordTagManagerProps = {
keyword: KeywordType|undefined,
@@ -10,9 +11,8 @@ type keywordTagManagerProps = {
}
const KeywordTagManager = ({ keyword, closeModal }: keywordTagManagerProps) => {
const [tagInput, setTagInput] = useState('');
const [inputError, setInputError] = useState('');
const { mutate: updateMutate } = useUpdateKeywordTags(() => { setTagInput(''); });
const [showAddTag, setShowAddTag] = useState<boolean>(false);
const { mutate: updateMutate } = useUpdateKeywordTags(() => { });
const removeTag = (tag:String) => {
if (!keyword) { return; }
@@ -20,24 +20,6 @@ const KeywordTagManager = ({ keyword, closeModal }: keywordTagManagerProps) => {
updateMutate({ tags: { [keyword.ID]: newTags } });
};
const addTag = () => {
if (!keyword) { return; }
if (!tagInput) {
setInputError('Please Insert a Tag!');
setTimeout(() => { setInputError(''); }, 3000);
return;
}
if (keyword.tags.includes(tagInput)) {
setInputError('Tag Exist!');
setTimeout(() => { setInputError(''); }, 3000);
return;
}
console.log('New Tag: ', tagInput);
const newTags = [...keyword.tags, tagInput.trim()];
updateMutate({ tags: { [keyword.ID]: newTags } });
};
return (
<Modal closeModal={() => { closeModal(false); }} title={`Tags for Keyword "${keyword && keyword.keyword}"`}>
<div className="text-sm my-8 ">
@@ -53,31 +35,27 @@ const KeywordTagManager = ({ keyword, closeModal }: keywordTagManagerProps) => {
</button>
</li>;
})}
<li className='inline-block py-1 px-1'>
<button
title='Add New Tag'
className="cursor-pointer rounded p-1 px-3 bg-indigo-600 text-white font-semibold text-sm"
onClick={() => setShowAddTag(true)}>+</button>
</li>
</ul>
)}
{keyword && keyword.tags.length === 0 && (
<div className="text-center w-full text-gray-500">No Tags Added to this Keyword.</div>
<div className="text-center w-full text-gray-500">
No Tags Added to this Keyword. <button className=' text-indigo-600' onClick={() => setShowAddTag(true)}>+ Add Tag</button>
</div>
)}
</div>
<div className="relative">
{inputError && <span className="absolute top-[-24px] text-red-400 text-sm font-semibold">{inputError}</span>}
<span className='absolute text-gray-400 top-3 left-2'><Icon type="tags" size={16} /></span>
<input
className='w-full border rounded border-gray-200 py-3 px-4 pl-8 outline-none focus:border-indigo-300'
placeholder='Insert Tags'
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.code === 'Enter') {
e.preventDefault();
addTag();
}
}}
/>
<button className=" absolute right-2 top-2 cursor-pointer rounded p-1 px-4 bg-blue-600 text-white font-bold" onClick={addTag}>+</button>
</div>
{showAddTag && keyword && (
<AddTags
keywords={[keyword]}
closeModal={() => setShowAddTag(false)}
/>
)}
</Modal>
);
};

View File

@@ -10,6 +10,7 @@ import KeywordFilters from './KeywordFilter';
import Modal from '../common/Modal';
import { useDeleteKeywords, useFavKeywords, useRefreshKeywords } from '../../services/keywords';
import KeywordTagManager from './KeywordTagManager';
import AddTags from './AddTags';
type KeywordsTableProps = {
domain: DomainType | null,
@@ -28,6 +29,7 @@ const KeywordsTable = (props: KeywordsTableProps) => {
const [showKeyDetails, setShowKeyDetails] = useState<KeywordType|null>(null);
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
const [showTagManager, setShowTagManager] = useState<null|number>(null);
const [showAddTags, setShowAddTags] = useState<boolean>(false);
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
const [sortBy, setSortBy] = useState<string>('date_asc');
const [scDataType, setScDataType] = useState<string>('threeDays');
@@ -79,7 +81,7 @@ const KeywordsTable = (props: KeywordsTableProps) => {
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => { refreshMutate({ ids: selectedKeywords }); setSelectedKeywords([]); }}
>
<span className=' bg-indigo-100 text-blue-700 px-1 rounded'><Icon type="reload" size={11} /></span> Refresh Keyword
<span className=' bg-indigo-100 text-blue-700 px-1 rounded'><Icon type="reload" size={11} /></span> Refresh Keywords
</a>
</li>
<li className='inline-block mr-4'>
@@ -87,7 +89,14 @@ const KeywordsTable = (props: KeywordsTableProps) => {
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => setShowRemoveModal(true)}
>
<span className=' bg-red-100 text-red-600 px-1 rounded'><Icon type="trash" size={14} /></span> Remove Keyword</a>
<span className=' bg-red-100 text-red-600 px-1 rounded'><Icon type="trash" size={14} /></span> Remove Keywords</a>
</li>
<li className='inline-block mr-4'>
<a
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => setShowAddTags(true)}
>
<span className=' bg-green-100 text-green-500 px-1 rounded'><Icon type="tags" size={14} /></span> Tag Keywords</a>
</li>
</ul>
</div>
@@ -222,6 +231,12 @@ const KeywordsTable = (props: KeywordsTableProps) => {
closeModal={() => setShowTagManager(null)}
/>
)}
{showAddTags && (
<AddTags
keywords={keywords.filter((k) => selectedKeywords.includes(k.ID))}
closeModal={() => setShowAddTags(false)}
/>
)}
<Toaster position='bottom-center' containerClassName="react_toaster" />
</div>
);

View File

@@ -153,10 +153,13 @@ const updateKeywords = async (req: NextApiRequest, res: NextApiResponse<Keywords
}
if (tags) {
const tagsKeywordIDs = Object.keys(tags);
const multipleKeywords = tagsKeywordIDs.length > 1;
for (const keywordID of tagsKeywordIDs) {
const response = await Keyword.findOne({ where: { ID: keywordID } });
if (response) {
await response.update({ tags: JSON.stringify(tags[keywordID]) });
const selectedKeyword = await Keyword.findOne({ where: { ID: keywordID } });
const currentTags = selectedKeyword && selectedKeyword.tags ? JSON.parse(selectedKeyword.tags) : [];
const mergedTags = Array.from(new Set([...currentTags, ...tags[keywordID]]));
if (selectedKeyword) {
await selectedKeyword.update({ tags: JSON.stringify(multipleKeywords ? mergedTags : tags[keywordID]) });
}
}
return res.status(200).json({ keywords });