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

Commit

Permalink
Merge pull request #211 from elmu/feature/184-add-tag-field-to-document
Browse files Browse the repository at this point in the history
Feature/184 add tag field to document
  • Loading branch information
laurentiu-ilici authored Oct 22, 2021
2 parents 698cc99 + 8816a80 commit 2cd1088
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 22 deletions.
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

0 comments on commit 2cd1088

Please sign in to comment.