diff --git a/client/src/components/Snippets/SnippetDetails.tsx b/client/src/components/Snippets/SnippetDetails.tsx index a3cb22e..514f343 100644 --- a/client/src/components/Snippets/SnippetDetails.tsx +++ b/client/src/components/Snippets/SnippetDetails.tsx @@ -64,8 +64,8 @@ export const SnippetDetails = (props: Props): JSX.Element => { {/* TAGS */}
- {tags.map(tag => ( - + {tags.map((tag, idx) => ( + ))} diff --git a/client/src/components/Snippets/SnippetForm.tsx b/client/src/components/Snippets/SnippetForm.tsx index d839b25..257e9fc 100644 --- a/client/src/components/Snippets/SnippetForm.tsx +++ b/client/src/components/Snippets/SnippetForm.tsx @@ -26,7 +26,7 @@ export const SnippetForm = (props: Props): JSX.Element => { code: '', docs: '', isPinned: false, - tags: '' + tags: [] }); useEffect(() => { @@ -46,6 +46,14 @@ export const SnippetForm = (props: Props): JSX.Element => { }); }; + const stringToTags = (e: ChangeEvent) => { + const tags = e.target.value.split(','); + setFormData({ + ...formData, + tags + }); + }; + const formHandler = (e: FormEvent) => { e.preventDefault(); @@ -126,13 +134,13 @@ export const SnippetForm = (props: Props): JSX.Element => { className='form-control' id='tags' name='tags' - value={formData.tags} + // value={formData.tags} placeholder='automation, files, loop' - required - onChange={e => inputHandler(e)} + onChange={e => stringToTags(e)} />
- Language tag will be added automatically + Tags should be separate with a comma. Language tag will be added + automatically

diff --git a/src/controllers/snippets.ts b/src/controllers/snippets.ts index 65fac50..01dce23 100644 --- a/src/controllers/snippets.ts +++ b/src/controllers/snippets.ts @@ -2,8 +2,13 @@ import { Request, Response, NextFunction } from 'express'; import { QueryTypes } from 'sequelize'; import { sequelize } from '../db'; import { asyncWrapper } from '../middleware'; -import { SnippetInstance, SnippetModel } from '../models'; -import { ErrorResponse, getTags, tagsParser, Logger } from '../utils'; +import { + SnippetInstance, + SnippetModel, + Snippet_TagModel, + TagModel +} from '../models'; +import { ErrorResponse, getTags, tagParser, Logger } from '../utils'; const logger = new Logger('snippets-controller'); @@ -14,20 +19,65 @@ const logger = new Logger('snippets-controller'); */ export const createSnippet = asyncWrapper( async (req: Request, res: Response, next: NextFunction): Promise => { - const { language, tags } = <{ language: string; tags: string }>req.body; - const parsedTags = tagsParser(tags); - - if (!parsedTags.includes(language.toLowerCase())) { - parsedTags.push(language.toLowerCase()); + interface Body { + language: string; + tags: string[]; } + // Get tags from request body + const { language, tags: requestTags } = req.body; + const parsedRequestTags = tagParser([ + ...requestTags, + language.toLowerCase() + ]); + + // Create snippet const snippet = await SnippetModel.create({ ...req.body, - tags: parsedTags.join(',') + tags: [...parsedRequestTags].join(',') }); + // Get all tags + const rawAllTags = await sequelize.query<{ id: number; name: string }>( + `SELECT * FROM tags`, + { type: QueryTypes.SELECT } + ); + + const parsedAllTags = rawAllTags.map(tag => tag.name); + + // Create array of new tags + const newTags = [...parsedRequestTags].filter( + tag => !parsedAllTags.includes(tag) + ); + + // Create new tags + if (newTags.length > 0) { + for (const tag of newTags) { + const { id, name } = await TagModel.create({ name: tag }); + rawAllTags.push({ id, name }); + } + } + + // Associate tags with snippet + for (const tag of parsedRequestTags) { + const tagObj = rawAllTags.find(t => t.name == tag); + + if (tagObj) { + await Snippet_TagModel.create({ + snippet_id: snippet.id, + tag_id: tagObj.id + }); + } + } + + // Get raw snippet values + const rawSnippet = snippet.get({ plain: true }); + res.status(201).json({ - data: snippet + data: { + ...rawSnippet, + tags: [...parsedRequestTags] + } }); } ); @@ -50,7 +100,7 @@ export const getAllSnippets = asyncWrapper( snippet.tags = tags; } } catch (err) { - logger.log('Error while fetching tags'); + logger.log('Error while fetching tags', 'ERROR'); } finally { resolve(); } @@ -112,23 +162,7 @@ export const updateSnippet = asyncWrapper( ); } - // Check if language was changed. Edit tags if so - const { language: oldLanguage } = snippet; - const { language, tags } = <{ language: string; tags: string }>req.body; - let parsedTags = tagsParser(tags); - - if (oldLanguage != language) { - parsedTags = parsedTags.filter(tag => tag != oldLanguage); - - if (!parsedTags.includes(language)) { - parsedTags.push(language.toLowerCase()); - } - } - - snippet = await snippet.update({ - ...req.body, - tags: parsedTags.join(',') - }); + snippet = await snippet.update(req.body); res.status(200).json({ data: snippet @@ -156,6 +190,7 @@ export const deleteSnippet = asyncWrapper( ); } + await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } }); await snippet.destroy(); res.status(200).json({ diff --git a/src/db/migrations/02_tags.ts b/src/db/migrations/02_tags.ts index 558a144..02d09a9 100644 --- a/src/db/migrations/02_tags.ts +++ b/src/db/migrations/02_tags.ts @@ -20,7 +20,8 @@ export const up = async (queryInterface: QueryInterface): Promise => { }, name: { type: STRING, - allowNull: false + allowNull: false, + unique: true } }); @@ -47,40 +48,42 @@ export const up = async (queryInterface: QueryInterface): Promise => { const uniqueLanguages = [...new Set(languages)]; const tags: TagInstance[] = []; - await new Promise(resolve => { - uniqueLanguages.forEach(async language => { - try { - const tag = await TagModel.create({ name: language }); - tags.push(tag); - } catch (err) { - logger.log('Error while creating new tags'); - } finally { - if (uniqueLanguages.length == tags.length) { + if (snippets.length > 0) { + await new Promise(resolve => { + uniqueLanguages.forEach(async language => { + try { + const tag = await TagModel.create({ name: language }); + tags.push(tag); + } catch (err) { + logger.log('Error while creating new tags'); + } finally { + if (uniqueLanguages.length == tags.length) { + resolve(); + } + } + }); + }); + + // Assign tag to snippet + await new Promise(resolve => { + snippets.forEach(async snippet => { + try { + const tag = tags.find(tag => tag.name == snippet.language); + + if (tag) { + await Snippet_TagModel.create({ + snippet_id: snippet.id, + tag_id: tag.id + }); + } + } catch (err) { + logger.log('Error while assigning tags to snippets'); + } finally { resolve(); } - } + }); }); - }); - - // Assign tag to snippet - await new Promise(resolve => { - snippets.forEach(async snippet => { - try { - const tag = tags.find(tag => tag.name == snippet.language); - - if (tag) { - await Snippet_TagModel.create({ - snippet_id: snippet.id, - tag_id: tag.id - }); - } - } catch (err) { - logger.log('Error while assigning tags to snippets'); - } finally { - resolve(); - } - }); - }); + } }; export const down = async (queryInterface: QueryInterface): Promise => { diff --git a/src/models/Tag.ts b/src/models/Tag.ts index 9c2e2de..759bd06 100644 --- a/src/models/Tag.ts +++ b/src/models/Tag.ts @@ -16,7 +16,8 @@ export const TagModel = sequelize.define( }, name: { type: STRING, - allowNull: false + allowNull: false, + unique: true } }, { diff --git a/src/utils/index.ts b/src/utils/index.ts index f05b365..96fabc9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ export * from './Logger'; export * from './ErrorResponse'; -export * from './tagsParser'; +export * from './tagParser'; export * from './getTags'; diff --git a/src/utils/tagParser.ts b/src/utils/tagParser.ts new file mode 100644 index 0000000..9e66bcc --- /dev/null +++ b/src/utils/tagParser.ts @@ -0,0 +1,6 @@ +export const tagParser = (tags: string[]): Set => { + const parsedTags = tags.map(tag => tag.trim().toLowerCase()).filter(String); + const uniqueTags = new Set([...parsedTags]); + + return uniqueTags; +}; diff --git a/src/utils/tagsParser.ts b/src/utils/tagsParser.ts deleted file mode 100644 index 14fa27e..0000000 --- a/src/utils/tagsParser.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const tagsParser = (tags: string): string[] => { - const parsedTags = tags - .split(',') - .map(tag => tag.trim().toLowerCase()) - .filter(String); - - return parsedTags; -};