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:
parent
28fa83c406
commit
18717bac79
@ -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();
|
||||
|
@ -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,
|
||||
|
21
ElectronClient/app/gui/TagItem.jsx
Normal file
21
ElectronClient/app/gui/TagItem.jsx
Normal 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;
|
50
ElectronClient/app/gui/TagList.jsx
Normal file
50
ElectronClient/app/gui/TagList.jsx
Normal 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;
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user