1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Adds functionality to display tags under the open note. (#893)

* Adds functionality to display tags under the open note.

Towards #469

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Ensured tags in the dialog box and under the note appear in the same order.

Few formatting tweaks.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Fixes issues raised during code review.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Refactored code to always display tags in ascending order.

This changes the order of the tags in the dialog box and below the tag title.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
This commit is contained in:
Abijeet Patro 2018-11-08 03:46:05 +05:30 committed by Laurent Cozic
parent 28fa83c406
commit 18717bac79
7 changed files with 181 additions and 15 deletions

View File

@ -120,7 +120,7 @@ class MainScreenComponent extends React.Component {
});
} else if (command.name === 'setTags') {
const tags = await Tag.tagsByNoteId(command.noteId);
const tagTitles = tags.map((a) => { return a.title });
const tagTitles = tags.map((a) => { return a.title }).sort();
this.setState({
promptOptions: {
@ -158,7 +158,7 @@ class MainScreenComponent extends React.Component {
},
});
} else if (command.name === 'renameTag') {
const tag = await Tag.load(command.id);
const tag = await Tag.load(command.id);
if(!tag) return;
this.setState({
@ -173,12 +173,12 @@ class MainScreenComponent extends React.Component {
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
}
}
this.setState({promptOptions: null });
}
}
})
} else if (command.name === 'search') {
if (!this.searchId_) this.searchId_ = uuid.create();

View File

@ -8,6 +8,7 @@ const Setting = require('lib/models/Setting.js');
const { IconButton } = require('./IconButton.min.js');
const { urlDecode } = require('lib/string-utils');
const Toolbar = require('./Toolbar.min.js');
const TagList = require('./TagList.min.js');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
@ -53,6 +54,7 @@ class NoteTextComponent extends React.Component {
scrollHeight: null,
editorScrollTop: 0,
newNote: null,
noteTags: [],
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
@ -213,7 +215,7 @@ class NoteTextComponent extends React.Component {
// Note:
// - What's called "cursor position" is expressed as { row: x, column: y } and is how Ace Editor get/set the cursor position
// - A "range" defines a selection with a start and end cusor position, expressed as { start: <CursorPos>, end: <CursorPos> }
// - A "range" defines a selection with a start and end cusor position, expressed as { start: <CursorPos>, end: <CursorPos> }
// - A "text offset" below is the absolute position of the cursor in the string, as would be used in the indexOf() function.
// The functions below are used to convert between the different types.
rangeToTextOffsets(range, body) {
@ -261,7 +263,7 @@ class NoteTextComponent extends React.Component {
}
row++;
currentOffset += line.length + 1;
currentOffset += line.length + 1;
}
}
@ -275,11 +277,12 @@ class NoteTextComponent extends React.Component {
async componentWillMount() {
let note = null;
let noteTags = [];
if (this.props.newNote) {
note = Object.assign({}, this.props.newNote);
} else if (this.props.noteId) {
note = await Note.load(this.props.noteId);
noteTags = this.props.noteTags || [];
}
const folder = note ? Folder.byId(this.props.folders, note.parent_id) : null;
@ -289,6 +292,7 @@ class NoteTextComponent extends React.Component {
note: note,
folder: folder,
isLoading: false,
noteTags: noteTags
});
this.lastLoadedNoteId_ = note ? note.id : null;
@ -361,6 +365,7 @@ class NoteTextComponent extends React.Component {
let note = null;
let loadingNewNote = true;
let parentFolder = null;
let noteTags = [];
if (props.newNote) {
note = Object.assign({}, props.newNote);
@ -369,6 +374,7 @@ class NoteTextComponent extends React.Component {
} else {
noteId = props.noteId;
loadingNewNote = stateNoteId !== noteId;
noteTags = await Tag.tagsByNoteId(noteId);
this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
@ -446,6 +452,7 @@ class NoteTextComponent extends React.Component {
webviewReady: webviewReady,
folder: parentFolder,
lastKeys: [],
noteTags: noteTags
};
if (!note) {
@ -459,6 +466,13 @@ class NoteTextComponent extends React.Component {
this.setState(newState);
if (!this.props.newNote) {
this.props.dispatch({
type: "SET_NOTE_TAGS",
items: noteTags,
});
}
this.updateHtml(newState.note ? newState.note.body : '');
}
@ -467,6 +481,10 @@ class NoteTextComponent extends React.Component {
await this.reloadNote(nextProps);
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
await this.reloadNote(nextProps);
} else if ('noteTags' in nextProps && this.areNoteTagsModified(nextProps.noteTags, this.state.noteTags)) {
this.setState({
noteTags: nextProps.noteTags
});
}
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
@ -482,6 +500,24 @@ class NoteTextComponent extends React.Component {
return shared.isModified(this);
}
areNoteTagsModified(newTags, oldTags) {
if (!oldTags) return true;
if (newTags.length !== oldTags.length) return true;
for (let i = 0; i < newTags.length; ++i) {
let currNewTag = newTags[i];
for (let j = 0; j < oldTags.length; ++j) {
let currOldTag = oldTags[j];
if (currOldTag.id === currNewTag.id && currOldTag.updated_time !== currNewTag.updated_time) {
return true;
}
}
}
return false;
}
refreshNoteMetadata(force = null) {
return shared.refreshNoteMetadata(this, force);
}
@ -1361,10 +1397,13 @@ class NoteTextComponent extends React.Component {
};
const toolbarStyle = {
};
const tagStyle = {
marginBottom: 10,
};
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight;
const viewerStyle = {
width: Math.floor(innerWidth / 2),
@ -1445,6 +1484,11 @@ class NoteTextComponent extends React.Component {
placeholder={ this.props.newNote ? _('Creating new %s...', isTodo ? _('to-do') : _('note')) : '' }
/>
const tagList = <TagList
style={tagStyle}
items={this.state.noteTags}
/>;
const titleBarMenuButton = <IconButton style={{
display: 'flex',
}} iconName="fa-caret-down" theme={this.props.theme} onClick={() => { this.itemContextMenu() }} />
@ -1516,6 +1560,7 @@ class NoteTextComponent extends React.Component {
{ false ? titleBarMenuButton : null }
</div>
{ toolbar }
{ tagList }
{ editor }
{ viewer }
</div>
@ -1527,6 +1572,7 @@ class NoteTextComponent extends React.Component {
const mapStateToProps = (state) => {
return {
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
noteTags: state.selectedNoteTags,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,

View File

@ -0,0 +1,21 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
class TagItemComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = Object.assign({}, theme.tagStyle);
const title = this.props.title;
return <span style={style}>{title}</span>;
}
}
const mapStateToProps = (state) => {
return { theme: state.settings.theme };
};
const TagItem = connect(mapStateToProps)(TagItemComponent);
module.exports = TagItem;

View File

@ -0,0 +1,50 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
const TagItem = require('./TagItem.min.js');
class TagListComponent extends React.Component {
render() {
const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.theme);
const tags = this.props.items;
style.display = 'flex';
style.flexDirection = 'row';
style.borderBottom = '1px solid ' + theme.dividerColor;
style.boxSizing = 'border-box';
style.fontSize = theme.fontSize;
const tagItems = [];
if (tags || tags.length > 0) {
// Sort by id for now, but probably needs to be changed in the future.
tags.sort((a, b) => { return a.title < b.title ? -1 : +1; });
for (let i = 0; i < tags.length; i++) {
const props = {
title: tags[i].title,
key: tags[i].id
};
tagItems.push(<TagItem {...props} />);
}
}
if (tagItems.length === 0) {
style.visibility = 'hidden';
}
return (
<div className="tag-list" style={style}>
{ tagItems }
</div>
)
}
}
const mapStateToProps = (state) => {
return { theme: state.settings.theme };
};
const TagList = connect(mapStateToProps)(TagListComponent);
module.exports = TagList;

View File

@ -40,6 +40,9 @@ const globalStyle = {
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",
tagItemPadding: 3,
tagBackgroundColor: '#e5e5e5'
};
// For WebView - must correspond to the properties above
@ -104,6 +107,19 @@ globalStyle.toolbarStyle = {
justifyContent: 'center',
};
globalStyle.tagStyle = {
fontSize: globalStyle.fontSize,
fontFamily: globalStyle.fontFamily,
marginTop: globalStyle.itemMarginTop * 0.4,
marginBottom: globalStyle.itemMarginBottom * 0.4,
marginRight: globalStyle.margin * 0.3,
paddingTop: globalStyle.tagItemPadding,
paddingBottom: globalStyle.tagItemPadding,
paddingRight: globalStyle.tagItemPadding * 2,
paddingLeft: globalStyle.tagItemPadding * 2,
backgroundColor: globalStyle.tagBackgroundColor
};
let themeCache_ = {};
function themeStyle(theme) {

View File

@ -80,7 +80,7 @@ class Tag extends BaseItem {
}
this.dispatch({
type: 'TAG_UPDATE_ONE',
type: 'NOTE_TAG_REMOVE',
item: await Tag.load(tagId),
});
}
@ -167,4 +167,4 @@ class Tag extends BaseItem {
}
module.exports = Tag;
module.exports = Tag;

View File

@ -37,6 +37,7 @@ const defaultState = {
itemIndex: 0,
itemCount: 0,
},
selectedNoteTags: []
};
const stateUtils = {};
@ -122,11 +123,14 @@ function handleItemDelete(state, action) {
return newState;
}
function updateOneItem(state, action) {
function updateOneItem(state, action, keyName = '') {
let itemsKey = null;
if (action.type === 'TAG_UPDATE_ONE') itemsKey = 'tags';
if (action.type === 'FOLDER_UPDATE_ONE') itemsKey = 'folders';
if (action.type === 'MASTERKEY_UPDATE_ONE') itemsKey = 'masterKeys';
if (keyName) itemsKey = keyName;
else {
if (action.type === 'TAG_UPDATE_ONE') itemsKey = 'tags';
if (action.type === 'FOLDER_UPDATE_ONE') itemsKey = 'folders';
if (action.type === 'MASTERKEY_UPDATE_ONE') itemsKey = 'masterKeys';
}
let newItems = state[itemsKey].splice(0);
let item = action.item;
@ -214,6 +218,17 @@ function changeSelectedNotes(state, action) {
throw new Error('Unreachable');
}
function removeItemFromArray(array, property, value) {
for (let i = 0; i !== array.length; ++i) {
let currentItem = array[i];
if (currentItem[property] === value) {
array.splice(i, 1);
break;
}
}
return array;
}
const reducer = (state = defaultState, action) => {
let newState = state;
@ -359,6 +374,7 @@ const reducer = (state = defaultState, action) => {
case 'TAG_DELETE':
newState = handleItemDelete(state, action);
newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', action.id);
break;
case 'FOLDER_UPDATE_ALL':
@ -405,6 +421,18 @@ const reducer = (state = defaultState, action) => {
break;
case 'TAG_UPDATE_ONE':
newState = updateOneItem(state, action);
newState = updateOneItem(newState, action, 'selectedNoteTags');
break;
case 'NOTE_TAG_REMOVE':
newState = updateOneItem(state, action, 'tags');
let tagRemoved = action.item;
newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', tagRemoved.id);;
break;
case 'FOLDER_UPDATE_ONE':
case 'MASTERKEY_UPDATE_ONE':
@ -506,7 +534,7 @@ const reducer = (state = defaultState, action) => {
case 'SEARCH_DELETE':
newState = handleItemDelete(state, action);
break;
break;
case 'SEARCH_SELECT':
@ -557,6 +585,11 @@ const reducer = (state = defaultState, action) => {
newState.decryptionWorker = decryptionWorker;
break;
case 'SET_NOTE_TAGS':
newState = Object.assign({}, state);
newState.selectedNoteTags = action.items;
break;
}
} catch (error) {
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);