diff --git a/migrations/2021-10-22-02-create-tags-index.js b/migrations/2021-10-22-02-create-tags-index.js
new file mode 100644
index 00000000..5935e72f
--- /dev/null
+++ b/migrations/2021-10-22-02-create-tags-index.js
@@ -0,0 +1,15 @@
+class Migration2021102202 {
+ constructor(db) {
+ this.db = db;
+ }
+
+ async up() {
+ await this.db.collection('documents').createIndex({ tags: 1 }, { unique: false, name: 'tagsIndex' });
+ }
+
+ async down() {
+ await this.db.collection('documents').dropIndex('tagsIndex');
+ }
+}
+
+export default Migration2021102202;
diff --git a/src/components/document-metadata-editor.js b/src/components/document-metadata-editor.js
index 9641ae30..5d2e37ba 100644
--- a/src/components/document-metadata-editor.js
+++ b/src/components/document-metadata-editor.js
@@ -2,15 +2,17 @@ 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, Form } 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 validators from '../utils/input-validators';
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;
@@ -18,11 +20,13 @@ const RadioGroup = Radio.Group;
const MODE_EDIT = 'edit';
const MODE_PREVIEW = 'preview';
+const { isValidTag } = validators;
+
class DocumentMetadataEditor extends React.Component {
constructor(props) {
super(props);
autoBind(this);
- this.state = { mode: MODE_PREVIEW };
+ this.state = { mode: MODE_PREVIEW, tagsValidationStatus: '' };
}
handleEditClick() {
@@ -39,22 +43,31 @@ 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(selectedValues) {
+ const { onChanged, documentRevision } = this.props;
+ const areTagsValid = selectedValues.length > 0 && selectedValues.every(tag => isValidTag({ tag }));
+ this.setState({ tagsValidationStatus: areTagsValid ? '' : 'error' });
+ onChanged({ metadata: { ...documentRevision, tags: selectedValues }, invalidMetadata: !areTagsValid });
}
render() {
- const { mode } = this.state;
- const { documentRevision, languageNameProvider, language, t } = this.props;
+ const { mode, tagsValidationStatus } = this.state;
+ const { documentRevision, languageNameProvider, language, t, settings } = this.props;
+
+ const mergedTags = new Set([...settings.defaultTags, ...documentRevision.tags]);
let docLanguage;
let componentToShow;
@@ -68,6 +81,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 +95,17 @@ class DocumentMetadataEditor extends React.Component {
{t('language')}:
{t('slug')}:
+ {t('tags')}:
+
+
);
break;
@@ -113,11 +139,12 @@ class DocumentMetadataEditor extends React.Component {
DocumentMetadataEditor.propTypes = {
...translationProps,
...languageProps,
+ ...settingsProps,
documentRevision: documentRevisionShape.isRequired,
languageNameProvider: PropTypes.instanceOf(LanguageNameProvider).isRequired,
onChanged: PropTypes.func.isRequired
};
-export default withTranslation('documentMetadataEditor')(withLanguage(inject({
+export default withTranslation('documentMetadataEditor')(withSettings(withLanguage(inject({
languageNameProvider: LanguageNameProvider
-}, DocumentMetadataEditor)));
+}, DocumentMetadataEditor))));
diff --git a/src/components/document-metadata-editor.yml b/src/components/document-metadata-editor.yml
index d922a28f..885d1a72 100644
--- a/src/components/document-metadata-editor.yml
+++ b/src/components/document-metadata-editor.yml
@@ -13,3 +13,9 @@ slug:
unassigned:
en: unassigned
de: nicht zugewiesen
+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 = (