mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
feat: Adds ability to pick existing tags when applying keyword tags.
closes #171
This commit is contained in:
parent
2a1fc0e43d
commit
407ab8db83
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import Icon from '../common/Icon';
|
||||
import Modal from '../common/Modal';
|
||||
import SelectField from '../common/SelectField';
|
||||
@ -24,10 +24,16 @@ type KeywordsInput = {
|
||||
|
||||
const AddKeywords = ({ closeModal, domain, keywords, scraperName = '', allowsCity = false }: AddKeywordsProps) => {
|
||||
const [error, setError] = useState<string>('');
|
||||
const [showTagSuggestions, setShowTagSuggestions] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const defCountry = localStorage.getItem('default_country') || 'US';
|
||||
const [newKeywordsData, setNewKeywordsData] = useState<KeywordsInput>({ keywords: '', device: 'desktop', country: defCountry, domain, tags: '' });
|
||||
const { mutate: addMutate, isLoading: isAdding } = useAddKeywords(() => closeModal(false));
|
||||
const deviceTabStyle = 'cursor-pointer px-3 py-2 rounded mr-2';
|
||||
|
||||
const existingTags: string[] = useMemo(() => {
|
||||
const allTags = keywords.reduce((acc: string[], keyword) => [...acc, ...keyword.tags], []).filter((t) => t && t.trim() !== '');
|
||||
return [...new Set(allTags)];
|
||||
}, [keywords]);
|
||||
|
||||
const addKeywords = () => {
|
||||
if (newKeywordsData.keywords) {
|
||||
@ -50,6 +56,8 @@ const AddKeywords = ({ closeModal, domain, keywords, scraperName = '', allowsCit
|
||||
}
|
||||
};
|
||||
|
||||
const deviceTabStyle = 'cursor-pointer px-3 py-2 rounded mr-2';
|
||||
|
||||
return (
|
||||
<Modal closeModal={() => { closeModal(false); }} title={'Add New Keywords'} width="[420px]">
|
||||
<div data-testid="addkeywords_modal">
|
||||
@ -91,14 +99,37 @@ const AddKeywords = ({ closeModal, domain, keywords, scraperName = '', allowsCit
|
||||
</ul>
|
||||
</div>
|
||||
<div className='relative'>
|
||||
{/* TODO: Insert Existing Tags as Suggestions */}
|
||||
<input
|
||||
className='w-full border rounded border-gray-200 py-2 px-4 pl-8 outline-none focus:border-indigo-300'
|
||||
className='w-full border rounded border-gray-200 py-2 px-4 pl-12 outline-none focus:border-indigo-300'
|
||||
placeholder='Insert Tags (Optional)'
|
||||
value={newKeywordsData.tags}
|
||||
onChange={(e) => setNewKeywordsData({ ...newKeywordsData, tags: e.target.value })}
|
||||
/>
|
||||
<span className='absolute text-gray-400 top-2 left-2'><Icon type="tags" size={16} /></span>
|
||||
<span className='absolute text-gray-400 top-3 left-2 cursor-pointer' onClick={() => setShowTagSuggestions(!showTagSuggestions)}>
|
||||
<Icon type="tags" size={16} color={showTagSuggestions ? '#777' : '#aaa'} />
|
||||
<Icon type={showTagSuggestions ? 'caret-up' : 'caret-down'} size={14} color={showTagSuggestions ? '#666' : '#aaa'} />
|
||||
</span>
|
||||
{showTagSuggestions && (
|
||||
<ul className={`absolute z-50
|
||||
bg-white border border-t-0 border-gray-200 rounded rounded-t-none w-full`}>
|
||||
{existingTags.length > 0 && existingTags.map((tag, index) => {
|
||||
return newKeywordsData.tags.split(',').map((t) => t.trim()).includes(tag) === false && <li
|
||||
className=' p-2 cursor-pointer hover:text-indigo-600 hover:bg-indigo-50 transition'
|
||||
key={index}
|
||||
onClick={() => {
|
||||
const tagInput = newKeywordsData.tags;
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const tagToInsert = tagInput + (tagInput.trim().slice(-1) === ',' ? '' : (tagInput.trim() ? ', ' : '')) + tag;
|
||||
setNewKeywordsData({ ...newKeywordsData, tags: tagToInsert });
|
||||
setShowTagSuggestions(false);
|
||||
if (inputRef?.current) (inputRef.current as HTMLInputElement).focus();
|
||||
}}>
|
||||
<Icon type='tags' size={14} color='#bbb' /> {tag}
|
||||
</li>;
|
||||
})}
|
||||
{existingTags.length === 0 && <p>No Existing Tags Found... </p>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className='relative mt-2'>
|
||||
<input
|
||||
|
@ -1,21 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useUpdateKeywordTags } from '../../services/keywords';
|
||||
import Icon from '../common/Icon';
|
||||
import Modal from '../common/Modal';
|
||||
|
||||
type AddTagsProps = {
|
||||
keywords: KeywordType[],
|
||||
existingTags: string[],
|
||||
closeModal: Function
|
||||
}
|
||||
|
||||
const AddTags = ({ keywords = [], closeModal }: AddTagsProps) => {
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const AddTags = ({ keywords = [], existingTags = [], closeModal }: AddTagsProps) => {
|
||||
const [tagInput, setTagInput] = useState(() => (keywords.length === 1 ? keywords[0].tags.join(', ') : ''));
|
||||
const [inputError, setInputError] = useState('');
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const { mutate: updateMutate } = useUpdateKeywordTags(() => { setTagInput(''); });
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const addTag = () => {
|
||||
if (keywords.length === 0) { return; }
|
||||
if (!tagInput) {
|
||||
if (!tagInput && keywords.length > 1) {
|
||||
setInputError('Please Insert a Tag!');
|
||||
setTimeout(() => { setInputError(''); }, 3000);
|
||||
return;
|
||||
@ -24,7 +27,7 @@ const AddTags = ({ keywords = [], closeModal }: AddTagsProps) => {
|
||||
const tagsArray = tagInput.split(',').map((t) => t.trim());
|
||||
const tagsPayload:any = {};
|
||||
keywords.forEach((keyword:KeywordType) => {
|
||||
tagsPayload[keyword.ID] = [...keyword.tags, ...tagsArray];
|
||||
tagsPayload[keyword.ID] = keywords.length === 1 ? tagsArray : [...(new Set([...keyword.tags, ...tagsArray]))];
|
||||
});
|
||||
updateMutate({ tags: tagsPayload });
|
||||
};
|
||||
@ -33,9 +36,13 @@ const AddTags = ({ keywords = [], closeModal }: AddTagsProps) => {
|
||||
<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>
|
||||
<span className='absolute text-gray-400 top-3 left-2 cursor-pointer' onClick={() => setShowSuggestions(!showSuggestions)}>
|
||||
<Icon type="tags" size={16} color={showSuggestions ? '#777' : '#aaa'} />
|
||||
<Icon type={showSuggestions ? 'caret-up' : 'caret-down'} size={14} color={showSuggestions ? '#666' : '#aaa'} />
|
||||
</span>
|
||||
<input
|
||||
className='w-full border rounded border-gray-200 py-3 px-4 pl-8 outline-none focus:border-indigo-300'
|
||||
ref={inputRef}
|
||||
className='w-full border rounded border-gray-200 py-3 px-4 pl-12 outline-none focus:border-indigo-300'
|
||||
placeholder='Insert Tags. eg: tag1, tag2'
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
@ -46,6 +53,27 @@ const AddTags = ({ keywords = [], closeModal }: AddTagsProps) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<ul className={`absolute z-50
|
||||
bg-white border border-t-0 border-gray-200 rounded rounded-t-none w-full`}>
|
||||
{existingTags.length > 0 && existingTags.map((tag, index) => {
|
||||
return tagInput.split(',').map((t) => t.trim()).includes(tag) === false && <li
|
||||
className=' p-2 cursor-pointer hover:text-indigo-600 hover:bg-indigo-50 transition'
|
||||
key={index}
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const tagToInsert = tagInput + (tagInput.trim().slice(-1) === ',' ? '' : (tagInput.trim() ? ', ' : '')) + tag;
|
||||
setTagInput(tagToInsert);
|
||||
setShowSuggestions(false);
|
||||
if (inputRef?.current) (inputRef.current as HTMLInputElement).focus();
|
||||
}}>
|
||||
<Icon type='tags' size={14} color='#bbb' /> {tag}
|
||||
</li>;
|
||||
})}
|
||||
{existingTags.length === 0 && <p>No Existing Tags Found... </p>}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<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}>
|
||||
|
@ -10,7 +10,7 @@ type keywordTagManagerProps = {
|
||||
allTags: string[]
|
||||
}
|
||||
|
||||
const KeywordTagManager = ({ keyword, closeModal }: keywordTagManagerProps) => {
|
||||
const KeywordTagManager = ({ keyword, allTags = [], closeModal }: keywordTagManagerProps) => {
|
||||
const [showAddTag, setShowAddTag] = useState<boolean>(false);
|
||||
const { mutate: updateMutate } = useUpdateKeywordTags(() => { });
|
||||
|
||||
@ -51,6 +51,7 @@ const KeywordTagManager = ({ keyword, closeModal }: keywordTagManagerProps) => {
|
||||
</div>
|
||||
{showAddTag && keyword && (
|
||||
<AddTags
|
||||
existingTags={allTags}
|
||||
keywords={[keyword]}
|
||||
closeModal={() => setShowAddTag(false)}
|
||||
/>
|
||||
|
@ -59,7 +59,7 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
||||
}, [keywords, device, sortBy, filterParams, scDataType]);
|
||||
|
||||
const allDomainTags: string[] = useMemo(() => {
|
||||
const allTags = keywords.reduce((acc: string[], keyword) => [...acc, ...keyword.tags], []);
|
||||
const allTags = keywords.reduce((acc: string[], keyword) => [...acc, ...keyword.tags], []).filter((t) => t && t.trim() !== '');
|
||||
return [...new Set(allTags)];
|
||||
}, [keywords]);
|
||||
|
||||
@ -251,6 +251,7 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
||||
)}
|
||||
{showAddTags && (
|
||||
<AddTags
|
||||
existingTags={allDomainTags}
|
||||
keywords={keywords.filter((k) => selectedKeywords.includes(k.ID))}
|
||||
closeModal={() => setShowAddTags(false)}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user