Skip to content
This repository has been archived by the owner on Oct 20, 2022. It is now read-only.

Feature/184 add tag field to document #211

Merged
merged 13 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions migrations/2021-10-22-02-create-tags-index.js
Original file line number Diff line number Diff line change
@@ -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;
47 changes: 37 additions & 10 deletions src/components/document-metadata-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@ 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;

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() {
Expand All @@ -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;
Expand All @@ -68,6 +81,8 @@ class DocumentMetadataEditor extends React.Component {
<span>{t('language')}:</span> <span><CountryFlagAndName code={docLanguage.flag} name={docLanguage.name} /></span>
<br />
<span>{t('slug')}:</span> {documentRevision.slug ? <span>{urls.getArticleUrl(documentRevision.slug)}</span> : <i>({t('unassigned')})</i>}
<br />
<span>{t('tags')}</span>: {documentRevision.tags.map(item => (<Space key={item}><Tag key={item}>{item}</Tag></Space>))}
</div>
);
break;
Expand All @@ -80,6 +95,17 @@ class DocumentMetadataEditor extends React.Component {
<span>{t('language')}:</span> <LanguageSelect value={documentRevision.language} onChange={this.handleLanguageChange} />
<br />
<span>{t('slug')}:</span> <Input addonBefore={urls.articlesPrefix} value={documentRevision.slug || ''} onChange={this.handleSlugChange} />
<span>{t('tags')}</span>:
<Form.Item validateStatus={tagsValidationStatus} help={tagsValidationStatus && t('invalidTags')}>
<Select
mode="tags"
tokenSeparators={[' ', '\t']}
value={documentRevision.tags}
style={{ width: '100%' }}
onChange={selectedValue => this.handleTagsChange(selectedValue)}
options={Array.from(mergedTags).map(tag => ({ value: tag, key: tag }))}
/>
</Form.Item>
</div>
);
break;
Expand Down Expand Up @@ -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))));
6 changes: 6 additions & 0 deletions src/components/document-metadata-editor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions src/components/pages/edit-doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ class EditDoc extends React.Component {
editedDocumentRevision: clonedRevision,
isDirty: false,
proposedSectionKeys,
invalidSectionKeys: []
invalidSectionKeys: [],
invalidMetadata: false
};
}

Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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 = (
<Menu>
Expand All @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions src/components/settings/default-tags-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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, allTags: tags })) });
};

const handleMoveClick = (index, offset) => {
Expand Down
4 changes: 4 additions & 0 deletions src/stores/collection-specs/documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default {
{
name: '_idx_namespace_slug_',
key: { namespace: 1, slug: 1 }
},
{
name: 'tagsIndex',
key: { tags: 1 }
}
]
};
2 changes: 1 addition & 1 deletion src/utils/input-validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
9 changes: 4 additions & 5 deletions src/utils/input-validators.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ describe.only('input-validators', () => {

describe('isValidTag', () => {
let result;
const allOtherTags = ['tag1', 'tag2', 'tag3'];

const testCases = [
{ tag: null, expectedResult: false },
Expand All @@ -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);
Expand Down