From 18b9f1cab370f98ba94cb7db10fa8bc505d68099 Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Thu, 21 Oct 2021 15:54:21 +0200 Subject: [PATCH 01/12] ELMU #184 Add migration to create index on tags --- migrations/2021-10-21-01-create-tags-index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 migrations/2021-10-21-01-create-tags-index.js diff --git a/migrations/2021-10-21-01-create-tags-index.js b/migrations/2021-10-21-01-create-tags-index.js new file mode 100644 index 00000000..5e03c568 --- /dev/null +++ b/migrations/2021-10-21-01-create-tags-index.js @@ -0,0 +1,15 @@ +class Migration2021102101 { + constructor(db) { + this.db = db; + } + + async up() { + await this.db.collection('documents').createIndex({ tags: 1 }, { unique: true, name: 'tagsIndex' }); + } + + async down() { + await this.db.collection('documents').dropIndex('tagsIndex'); + } +} + +export default Migration2021102101; From 089ca1f58ae8a04bf8c595097187153671c67f0b Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Thu, 21 Oct 2021 18:30:03 +0200 Subject: [PATCH 02/12] ELMU #184 Add basic tag management --- src/components/document-metadata-editor.js | 32 +++++++++++++++++---- src/components/document-metadata-editor.yml | 3 ++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/components/document-metadata-editor.js b/src/components/document-metadata-editor.js index 9641ae30..70267cbc 100644 --- a/src/components/document-metadata-editor.js +++ b/src/components/document-metadata-editor.js @@ -2,15 +2,16 @@ import React from 'react'; import urls from '../utils/urls'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; -import { Input, Radio } from 'antd'; +import { Input, Radio, Tag, Space, Select } from 'antd'; import { inject } from './container-context'; import { withTranslation } from 'react-i18next'; +import { withSettings } from './settings-context'; import { withLanguage } from './language-context'; import LanguageSelect from './localization/language-select'; import { EyeOutlined, EditOutlined } from '@ant-design/icons'; import LanguageNameProvider from '../data/language-name-provider'; import CountryFlagAndName from './localization/country-flag-and-name'; -import { documentRevisionShape, translationProps, languageProps } from '../ui/default-prop-types'; +import { documentRevisionShape, translationProps, languageProps, settingsProps } from '../ui/default-prop-types'; const RadioButton = Radio.Button; const RadioGroup = Radio.Group; @@ -52,9 +53,17 @@ class DocumentMetadataEditor extends React.Component { onChanged({ ...documentRevision, slug: event.target.value }); } + handleTagsChange(selectedValue) { + const { onChanged, documentRevision } = this.props; + onChanged({ ...documentRevision, tags: selectedValue }); + } + render() { const { mode } = this.state; - const { documentRevision, languageNameProvider, language, t } = this.props; + const { documentRevision, languageNameProvider, language, t, settings } = this.props; + + const mergedTags = new Set(settings.defaultTags); + documentRevision.tags.forEach(tag => mergedTags.add(tag)); let docLanguage; let componentToShow; @@ -68,6 +77,8 @@ class DocumentMetadataEditor extends React.Component { {t('language')}:
{t('slug')}: {documentRevision.slug ? {urls.getArticleUrl(documentRevision.slug)} : ({t('unassigned')})} +
+ {t('tags')}: {documentRevision.tags.map(item => ({item}))} ); break; @@ -80,6 +91,16 @@ class DocumentMetadataEditor extends React.Component { {t('language')}:
{t('slug')}: + {t('tags')}: + {t('tags')}: - ({ value: tag, key: tag }))} + /> + ); break; diff --git a/src/components/document-metadata-editor.yml b/src/components/document-metadata-editor.yml index 1e618ae6..885d1a72 100644 --- a/src/components/document-metadata-editor.yml +++ b/src/components/document-metadata-editor.yml @@ -16,3 +16,6 @@ unassigned: tags: en: Tags de: Tags +invalidTags: + en: At least one tag must be provided and all tags must be between 3 and 30 characters + de: Mindestens ein Tag muss angegeben werden und Tags dürfen nicht kürzer als 3 und nicht länger als 30 Zeichen sein diff --git a/src/components/pages/edit-doc.js b/src/components/pages/edit-doc.js index 5b228d2e..d5c27b12 100644 --- a/src/components/pages/edit-doc.js +++ b/src/components/pages/edit-doc.js @@ -96,7 +96,8 @@ class EditDoc extends React.Component { editedDocumentRevision: clonedRevision, isDirty: false, proposedSectionKeys, - invalidSectionKeys: [] + invalidSectionKeys: [], + invalidMetadata: false }; } @@ -147,10 +148,11 @@ class EditDoc extends React.Component { } } - handleMetadataChanged(metadata) { + handleMetadataChanged({ metadata, invalidMetadata }) { this.setState(prevState => { return { ...prevState, + invalidMetadata, editedDocumentRevision: { ...prevState.editedDocumentRevision, ...metadata }, isDirty: true }; @@ -326,7 +328,7 @@ class EditDoc extends React.Component { render() { const { t } = this.props; - const { editedDocumentRevision, isDirty, invalidSectionKeys, proposedSectionKeys } = this.state; + const { editedDocumentRevision, isDirty, invalidSectionKeys, proposedSectionKeys, invalidMetadata } = this.state; const newSectionMenu = ( @@ -345,7 +347,7 @@ class EditDoc extends React.Component { ); const headerActions = []; - if (isDirty && !invalidSectionKeys.length) { + if (isDirty && !invalidSectionKeys.length && !invalidMetadata) { headerActions.push({ key: 'save', type: 'primary', From 0cb13bcaca8c7193edcfb91e1e8e124b445f595d Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 11:16:23 +0200 Subject: [PATCH 04/12] ELMU #184 Update the migration file name --- ...reate-tags-index.js => 2021-10-22-02-create-tags-index.js} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename migrations/{2021-10-21-01-create-tags-index.js => 2021-10-22-02-create-tags-index.js} (80%) diff --git a/migrations/2021-10-21-01-create-tags-index.js b/migrations/2021-10-22-02-create-tags-index.js similarity index 80% rename from migrations/2021-10-21-01-create-tags-index.js rename to migrations/2021-10-22-02-create-tags-index.js index e87c9b6d..5935e72f 100644 --- a/migrations/2021-10-21-01-create-tags-index.js +++ b/migrations/2021-10-22-02-create-tags-index.js @@ -1,4 +1,4 @@ -class Migration2021102101 { +class Migration2021102202 { constructor(db) { this.db = db; } @@ -12,4 +12,4 @@ class Migration2021102101 { } } -export default Migration2021102101; +export default Migration2021102202; From b49ac9e534015e565a410e2c6cda9f1ae330a9a6 Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 13:49:22 +0200 Subject: [PATCH 05/12] ELMU #184 Fix tag language bug --- src/components/document-metadata-editor.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/document-metadata-editor.js b/src/components/document-metadata-editor.js index cfac3f5c..fa95945b 100644 --- a/src/components/document-metadata-editor.js +++ b/src/components/document-metadata-editor.js @@ -19,6 +19,8 @@ const RadioGroup = Radio.Group; const MODE_EDIT = 'edit'; const MODE_PREVIEW = 'preview'; +const toGermanTag = tag => `${tag.slice(0, 1).toUpperCase()}${tag.slice(1).toLowerCase()}`; + class DocumentMetadataEditor extends React.Component { constructor(props) { super(props); @@ -40,24 +42,27 @@ class DocumentMetadataEditor extends React.Component { handleTitleChange(event) { const { onChanged, documentRevision } = this.props; - onChanged({ ...documentRevision, title: event.target.value }); + onChanged({ metadata: { ...documentRevision, title: event.target.value } }); } handleLanguageChange(value) { const { onChanged, documentRevision } = this.props; - onChanged({ ...documentRevision, language: value }); + onChanged({ metadata: { ...documentRevision, language: value } }); } handleSlugChange(event) { const { onChanged, documentRevision } = this.props; - onChanged({ ...documentRevision, slug: event.target.value }); + onChanged({ metadata: { ...documentRevision, slug: event.target.value } }); } - handleTagsChange(selectedValue) { + handleTagsChange(selectedValue, language) { const { onChanged, documentRevision } = this.props; const invalidMetadata = selectedValue.length === 0 || selectedValue.some(tag => tag.length < 3 || tag.length > 30); this.setState({ tagsValidationStatus: invalidMetadata ? 'error' : '' }); - onChanged({ metadata: { ...documentRevision, tags: selectedValue }, invalidMetadata }); + + const languageMapper = language === 'de' ? toGermanTag : word => word; + + onChanged({ metadata: { ...documentRevision, tags: selectedValue.map(languageMapper) }, invalidMetadata }); } render() { @@ -97,11 +102,11 @@ class DocumentMetadataEditor extends React.Component { this.handleTagsChange(selectedValue)} From e5ee59852bea83c9df43258456627dafc1d877ab Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 14:32:21 +0200 Subject: [PATCH 09/12] ELMU #184 Add index to index checks --- src/stores/collection-specs/documents.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/collection-specs/documents.js b/src/stores/collection-specs/documents.js index 74d5c756..210e5c4e 100644 --- a/src/stores/collection-specs/documents.js +++ b/src/stores/collection-specs/documents.js @@ -8,6 +8,10 @@ export default { { name: '_idx_namespace_slug_', key: { namespace: 1, slug: 1 } + }, + { + name: 'tagsIndex', + key: { tags: 1 } } ] }; From d4d46adc4144071c6f79c228a1eae326c5f6c7fd Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 16:59:43 +0200 Subject: [PATCH 10/12] ELMU #184 Reuse input validators --- src/components/document-metadata-editor.js | 12 +++++++----- src/components/settings/default-tags-settings.js | 4 ++-- src/utils/input-validators.js | 2 +- src/utils/input-validators.spec.js | 9 ++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/document-metadata-editor.js b/src/components/document-metadata-editor.js index 0eeced85..f970bd30 100644 --- a/src/components/document-metadata-editor.js +++ b/src/components/document-metadata-editor.js @@ -9,6 +9,7 @@ import { withSettings } from './settings-context'; import { withLanguage } from './language-context'; import LanguageSelect from './localization/language-select'; import { EyeOutlined, EditOutlined } from '@ant-design/icons'; +import validators from '../utils/input-validators'; import LanguageNameProvider from '../data/language-name-provider'; import CountryFlagAndName from './localization/country-flag-and-name'; import { documentRevisionShape, translationProps, languageProps, settingsProps } from '../ui/default-prop-types'; @@ -19,6 +20,8 @@ const RadioGroup = Radio.Group; const MODE_EDIT = 'edit'; const MODE_PREVIEW = 'preview'; +const { isValidTag } = validators; + class DocumentMetadataEditor extends React.Component { constructor(props) { super(props); @@ -55,17 +58,16 @@ class DocumentMetadataEditor extends React.Component { handleTagsChange(selectedValue) { const { onChanged, documentRevision } = this.props; - const invalidMetadata = selectedValue.length === 0 || selectedValue.some(tag => tag.length < 3 || tag.length > 30); - this.setState({ tagsValidationStatus: invalidMetadata ? 'error' : '' }); - onChanged({ metadata: { ...documentRevision, tags: selectedValue }, invalidMetadata }); + const areTagsValid = selectedValue.every(tag => isValidTag({ tag })); + this.setState({ tagsValidationStatus: areTagsValid ? '' : 'error' }); + onChanged({ metadata: { ...documentRevision, tags: selectedValue }, invalidMetadata: !areTagsValid }); } render() { const { mode, tagsValidationStatus } = this.state; const { documentRevision, languageNameProvider, language, t, settings } = this.props; - const mergedTags = new Set(settings.defaultTags); - documentRevision.tags.forEach(tag => mergedTags.add(tag)); + const mergedTags = new Set([...settings.defaultTags, ...documentRevision.tags]); let docLanguage; let componentToShow; diff --git a/src/components/settings/default-tags-settings.js b/src/components/settings/default-tags-settings.js index 168bef5d..c5f4d514 100644 --- a/src/components/settings/default-tags-settings.js +++ b/src/components/settings/default-tags-settings.js @@ -8,7 +8,7 @@ import { DeleteOutlined, DownOutlined, PlusOutlined, UpOutlined } from '@ant-des const FormItem = Form.Item; -const getRequiredValidateStatus = (allTags, tag) => inputValidators.isValidTag(allTags, tag) ? 'success' : 'error'; +const getRequiredValidateStatus = (allTags, tag) => inputValidators.isValidTag({ allTags, tag }) ? 'success' : 'error'; const mapTableRowsToTags = rows => rows.map(row => row.tag); @@ -19,7 +19,7 @@ function DefaultTagsSettings({ defaultTags, onChange }) { const fireOnChange = rows => { const tags = mapTableRowsToTags(rows); - onChange(tags, { isValid: tags.every(tag => inputValidators.isValidTag(tags, tag)) }); + onChange(tags, { isValid: tags.every(tag => inputValidators.isValidTag({ tag, tags })) }); }; const handleMoveClick = (index, offset) => { diff --git a/src/utils/input-validators.js b/src/utils/input-validators.js index 20160a75..78ac9983 100644 --- a/src/utils/input-validators.js +++ b/src/utils/input-validators.js @@ -5,7 +5,7 @@ function isValidPassword({ password, minLength = 8 }) { return sanitizedPassword.length >= minLength && minOneLetterAndOneDigitPattern.test(sanitizedPassword); } -function isValidTag(allTags, tag) { +function isValidTag({ tag, allTags = [] }) { const trimmedTag = (tag || '').trim(); if (trimmedTag.length < 3 || trimmedTag.length > 30 || (/\s/).test(trimmedTag)) { diff --git a/src/utils/input-validators.spec.js b/src/utils/input-validators.spec.js index 51a515d8..5af26b82 100644 --- a/src/utils/input-validators.spec.js +++ b/src/utils/input-validators.spec.js @@ -33,7 +33,6 @@ describe.only('input-validators', () => { describe('isValidTag', () => { let result; - const allOtherTags = ['tag1', 'tag2', 'tag3']; const testCases = [ { tag: null, expectedResult: false }, @@ -42,17 +41,17 @@ describe.only('input-validators', () => { { tag: ' tag ', expectedResult: true }, { tag: 't a g', expectedResult: false }, { tag: 't\tag', expectedResult: false }, - { tag: 'tag2', expectedResult: false }, { tag: ' ', expectedResult: false }, + { tag: 'tag2', allTags: ['tag1', 'tag2'], expectedResult: true }, + { tag: 'tag2', allTags: ['tag1', 'tag2', 'tag2'], expectedResult: false }, { tag: 'aPrettyLongTagToConsiderValid?', expectedResult: true }, { tag: 'anEvenLongerTagToConsiderValid?', expectedResult: false } ]; - testCases.forEach(({ tag, expectedResult }) => { + testCases.forEach(({ tag, allTags, expectedResult }) => { describe(`when validating tag='${tag}'`, () => { beforeEach(() => { - const allTags = [...allOtherTags, tag]; - result = sut.isValidTag(allTags, tag); + result = sut.isValidTag({ tag, allTags }); }); it(`should return ${expectedResult}`, () => { expect(result).toBe(expectedResult); From 086e6a8c8b047917fb8159159d49fb93b2fdb591 Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 17:08:27 +0200 Subject: [PATCH 11/12] ELMU #184 Fix faulty validation call --- src/components/settings/default-tags-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/default-tags-settings.js b/src/components/settings/default-tags-settings.js index c5f4d514..98b5249f 100644 --- a/src/components/settings/default-tags-settings.js +++ b/src/components/settings/default-tags-settings.js @@ -19,7 +19,7 @@ function DefaultTagsSettings({ defaultTags, onChange }) { const fireOnChange = rows => { const tags = mapTableRowsToTags(rows); - onChange(tags, { isValid: tags.every(tag => inputValidators.isValidTag({ tag, tags })) }); + onChange(tags, { isValid: tags.every(tag => inputValidators.isValidTag({ tag, allTags: tags })) }); }; const handleMoveClick = (index, offset) => { From 8816a808b1e377dbc8353a66c9605a64455e0bbf Mon Sep 17 00:00:00 2001 From: Laurentiu Date: Fri, 22 Oct 2021 17:18:58 +0200 Subject: [PATCH 12/12] ELMU #184 Fix tags validation --- src/components/document-metadata-editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/document-metadata-editor.js b/src/components/document-metadata-editor.js index f970bd30..5d2e37ba 100644 --- a/src/components/document-metadata-editor.js +++ b/src/components/document-metadata-editor.js @@ -56,11 +56,11 @@ class DocumentMetadataEditor extends React.Component { onChanged({ metadata: { ...documentRevision, slug: event.target.value } }); } - handleTagsChange(selectedValue) { + handleTagsChange(selectedValues) { const { onChanged, documentRevision } = this.props; - const areTagsValid = selectedValue.every(tag => isValidTag({ tag })); + const areTagsValid = selectedValues.length > 0 && selectedValues.every(tag => isValidTag({ tag })); this.setState({ tagsValidationStatus: areTagsValid ? '' : 'error' }); - onChanged({ metadata: { ...documentRevision, tags: selectedValue }, invalidMetadata: !areTagsValid }); + onChanged({ metadata: { ...documentRevision, tags: selectedValues }, invalidMetadata: !areTagsValid }); } render() {