1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-04-26 12:02:59 +02:00

Electron app: handle tags

This commit is contained in:
Laurent Cozic 2017-11-11 23:13:14 +00:00
parent e649670bfe
commit eda3be066d
15 changed files with 151 additions and 60 deletions

@ -28,7 +28,7 @@ class Command extends BaseCommand {
if (command == 'add') { if (command == 'add') {
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note)); if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
if (!tag) tag = await Tag.save({ title: args.tag }); if (!tag) tag = await Tag.save({ title: args.tag }, { userSideValidation: true });
for (let i = 0; i < notes.length; i++) { for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id); await Tag.addNote(tag.id, notes[i].id);
} }

@ -86,7 +86,9 @@ class Application extends BaseApplication {
case 'WINDOW_COMMAND': case 'WINDOW_COMMAND':
newState = Object.assign({}, state); newState = Object.assign({}, state);
newState.windowCommand = { name: action.name }; let command = Object.assign({}, action);
delete command.type;
newState.windowCommand = command;
break; break;
} }

@ -84,9 +84,9 @@ class ImportScreenComponent extends React.Component {
}, },
} }
// const folder = await Folder.save({ title: folderTitle }); const folder = await Folder.save({ title: folderTitle });
// await importEnex(folder.id, filePath, options); await importEnex(folder.id, filePath, options);
this.addMessage('done', _('The notes have been imported: %s', lastProgress)); this.addMessage('done', _('The notes have been imported: %s', lastProgress));
this.setState({ doImport: false }); this.setState({ doImport: false });

@ -38,6 +38,10 @@ class ItemList extends React.Component {
render() { render() {
const items = this.props.items; const items = this.props.items;
const style = Object.assign({}, this.props.style, {
overflowX: 'hidden',
overflowY: 'auto',
});
if (!this.props.itemHeight) throw new Error('itemHeight is required'); if (!this.props.itemHeight) throw new Error('itemHeight is required');
@ -60,7 +64,7 @@ class ItemList extends React.Component {
const that = this; const that = this;
return ( return (
<div className={classes.join(' ')} style={this.props.style} onScroll={ (event) => { this.onScroll(event.target.scrollTop) }}> <div className={classes.join(' ')} style={style} onScroll={ (event) => { this.onScroll(event.target.scrollTop) }}>
{ itemComps } { itemComps }
</div> </div>
); );

@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js'); const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js'); const { PromptDialog } = require('./PromptDialog.min.js');
const { Setting } = require('lib/models/setting.js'); const { Setting } = require('lib/models/setting.js');
const { Tag } = require('lib/models/tag.js');
const { Note } = require('lib/models/note.js'); const { Note } = require('lib/models/note.js');
const { Folder } = require('lib/models/folder.js'); const { Folder } = require('lib/models/folder.js');
const { themeStyle } = require('../theme.js'); const { themeStyle } = require('../theme.js');
@ -17,8 +18,6 @@ class MainScreenComponent extends React.Component {
componentWillMount() { componentWillMount() {
this.setState({ this.setState({
newNotePromptVisible: false,
newFolderPromptVisible: false,
promptOptions: null, promptOptions: null,
noteVisiblePanes: ['editor', 'viewer'], noteVisiblePanes: ['editor', 'viewer'],
}); });
@ -43,7 +42,7 @@ class MainScreenComponent extends React.Component {
this.setState({ noteVisiblePanes: panes }); this.setState({ noteVisiblePanes: panes });
} }
doCommand(command) { async doCommand(command) {
if (!command) return; if (!command) return;
const createNewNote = async (title, isTodo) => { const createNewNote = async (title, isTodo) => {
@ -68,7 +67,7 @@ class MainScreenComponent extends React.Component {
if (command.name === 'newNote') { if (command.name === 'newNote') {
this.setState({ this.setState({
promptOptions: { promptOptions: {
message: _('Note title:'), label: _('Note title:'),
onClose: async (answer) => { onClose: async (answer) => {
if (answer) await createNewNote(answer, false); if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null }); this.setState({ promptOptions: null });
@ -78,7 +77,7 @@ class MainScreenComponent extends React.Component {
} else if (command.name === 'newTodo') { } else if (command.name === 'newTodo') {
this.setState({ this.setState({
promptOptions: { promptOptions: {
message: _('To-do title:'), label: _('To-do title:'),
onClose: async (answer) => { onClose: async (answer) => {
if (answer) await createNewNote(answer, true); if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null }); this.setState({ promptOptions: null });
@ -88,7 +87,7 @@ class MainScreenComponent extends React.Component {
} else if (command.name === 'newNotebook') { } else if (command.name === 'newNotebook') {
this.setState({ this.setState({
promptOptions: { promptOptions: {
message: _('Notebook title:'), label: _('Notebook title:'),
onClose: async (answer) => { onClose: async (answer) => {
if (answer) { if (answer) {
let folder = null; let folder = null;
@ -105,6 +104,24 @@ class MainScreenComponent extends React.Component {
}); });
} }
this.setState({ promptOptions: null });
}
},
});
} else if (command.name === 'setTags') {
const tags = await Tag.tagsByNoteId(command.noteId);
const tagTitles = tags.map((a) => { return a.title });
this.setState({
promptOptions: {
label: _('Add or remove tags:'),
description: _('Separate each tag by a comma.'),
value: tagTitles.join(', '),
onClose: async (answer) => {
if (answer !== null) {
const tagTitles = answer.split(',').map((a) => { return a.trim() });
await Tag.setNoteTagsByTitles(command.noteId, tagTitles);
}
this.setState({ promptOptions: null }); this.setState({ promptOptions: null });
} }
}, },
@ -133,14 +150,14 @@ class MainScreenComponent extends React.Component {
const rowHeight = style.height - theme.headerHeight; const rowHeight = style.height - theme.headerHeight;
const sideBarStyle = { const sideBarStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)), width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight, height: rowHeight,
display: 'inline-block', display: 'inline-block',
verticalAlign: 'top', verticalAlign: 'top',
}; };
const noteListStyle = { const noteListStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)), width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight, height: rowHeight,
display: 'inline-block', display: 'inline-block',
verticalAlign: 'top', verticalAlign: 'top',
@ -188,7 +205,14 @@ class MainScreenComponent extends React.Component {
return ( return (
<div style={style}> <div style={style}>
<PromptDialog theme={this.props.theme} style={promptStyle} onClose={(answer) => promptOptions.onClose(answer)} message={promptOptions ? promptOptions.message : ''} visible={!!this.state.promptOptions}/> <PromptDialog
value={promptOptions && promptOptions.value ? promptOptions.value : ''}
theme={this.props.theme}
style={promptStyle}
onClose={(answer) => promptOptions.onClose(answer)}
label={promptOptions ? promptOptions.label : ''}
description={promptOptions ? promptOptions.description : null}
visible={!!this.state.promptOptions} />
<Header style={headerStyle} showBackButton={false} buttons={headerButtons} /> <Header style={headerStyle} showBackButton={false} buttons={headerButtons} />
<SideBar style={sideBarStyle} /> <SideBar style={sideBarStyle} />
<NoteList itemHeight={40} style={noteListStyle} /> <NoteList itemHeight={40} style={noteListStyle} />

@ -57,16 +57,24 @@ class NoteListComponent extends React.Component {
const menu = new Menu() const menu = new Menu()
menu.append(new MenuItem({label: _('Delete'), click: async () => { menu.append(new MenuItem({label: _('Add or remove tags'), click: async () => {
const ok = bridge().showConfirmMessageBox(_('Delete note?')); this.props.dispatch({
if (!ok) return; type: 'WINDOW_COMMAND',
await Note.delete(noteId); name: 'setTags',
noteId: noteId,
});
}})); }}));
menu.append(new MenuItem({label: _('Switch between note and to-do'), click: async () => { menu.append(new MenuItem({label: _('Switch between note and to-do'), click: async () => {
const note = await Note.load(noteId); const note = await Note.load(noteId);
await Note.save(Note.toggleIsTodo(note)); await Note.save(Note.toggleIsTodo(note));
}})) }}));
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(_('Delete note?'));
if (!ok) return;
await Note.delete(noteId);
}}));
menu.popup(bridge().window()); menu.popup(bridge().window());
} }

@ -8,7 +8,7 @@ class PromptDialog extends React.Component {
componentWillMount() { componentWillMount() {
this.setState({ this.setState({
visible: false, visible: false,
answer: '', answer: this.props.value ? this.props.value : '',
}); });
this.focusInput_ = true; this.focusInput_ = true;
} }
@ -18,6 +18,10 @@ class PromptDialog extends React.Component {
this.setState({ visible: newProps.visible }); this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true; if (newProps.visible) this.focusInput_ = true;
} }
if ('value' in newProps) {
this.setState({ answer: newProps.value });
}
} }
componentDidUpdate() { componentDidUpdate() {
@ -60,6 +64,7 @@ class PromptDialog extends React.Component {
fontSize: theme.fontSize, fontSize: theme.fontSize,
color: theme.color, color: theme.color,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
verticalAlign: 'top',
}; };
const inputStyle = { const inputStyle = {
@ -67,6 +72,10 @@ class PromptDialog extends React.Component {
maxWidth: 400, maxWidth: 400,
}; };
const descStyle = Object.assign({}, theme.textStyle, {
marginTop: 10,
});
const onClose = (accept) => { const onClose = (accept) => {
if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null); if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null);
this.setState({ visible: false, answer: '' }); this.setState({ visible: false, answer: '' });
@ -84,17 +93,22 @@ class PromptDialog extends React.Component {
} }
} }
const descComp = this.props.description ? <div style={descStyle}>{this.props.description}</div> : null;
return ( return (
<div style={modalLayerStyle}> <div style={modalLayerStyle}>
<div style={promptDialogStyle}> <div style={promptDialogStyle}>
<label style={labelStyle}>{this.props.message ? this.props.message : ''}</label> <label style={labelStyle}>{this.props.label ? this.props.label : ''}</label>
<input <div style={{display: 'inline-block'}}>
style={inputStyle} <input
ref={input => this.answerInput_ = input} style={inputStyle}
value={this.state.answer} ref={input => this.answerInput_ = input}
type="text" value={this.state.answer}
onChange={(event) => onChange(event)} type="text"
onKeyDown={(event) => onKeyDown(event)} /> onChange={(event) => onChange(event)}
onKeyDown={(event) => onKeyDown(event)} />
{descComp}
</div>
<div style={{ textAlign: 'right', marginTop: 10 }}> <div style={{ textAlign: 'right', marginTop: 10 }}>
<button style={buttonStyle} onClick={() => onClose(true)}>OK</button> <button style={buttonStyle} onClick={() => onClose(true)}>OK</button>
<button style={buttonStyle} onClick={() => onClose(false)}>Cancel</button> <button style={buttonStyle} onClick={() => onClose(false)}>Cancel</button>

@ -13,11 +13,18 @@ const { app } = require('../app');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
async function initialize(dispatch) { async function initialize(dispatch) {
this.wcsTimeoutId_ = null;
bridge().window().on('resize', function() { bridge().window().on('resize', function() {
store.dispatch({ if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_);
type: 'WINDOW_CONTENT_SIZE_SET',
size: bridge().windowContentSize(), this.wcsTimeoutId_ = setTimeout(() => {
}); store.dispatch({
type: 'WINDOW_CONTENT_SIZE_SET',
size: bridge().windowContentSize(),
});
this.wcsTimeoutId_ = null;
}, 10);
}); });
store.dispatch({ store.dispatch({

@ -75,6 +75,7 @@ class SideBarComponent extends React.Component {
marginTop: 10, marginTop: 10,
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
minHeight: 70,
}, },
}; };
@ -157,7 +158,10 @@ class SideBarComponent extends React.Component {
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
const style = Object.assign({}, this.style().root, this.props.style); const style = Object.assign({}, this.style().root, this.props.style, {
overflowX: 'hidden',
overflowY: 'auto',
});
let items = []; let items = [];

@ -4,8 +4,10 @@ body {
} }
#content { #content {
overflow-y: scroll; overflow-y: auto;
height: 100%; height: 100%;
padding-left: 10px;
padding-right: 10px;
} }
</style> </style>

@ -1,6 +1,6 @@
{ {
"name": "joplin-desktop", "name": "joplin-desktop",
"version": "0.0.1", "version": "0.10.0",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

@ -6,37 +6,13 @@ body, textarea {
#react-root { #react-root {
height: 100%; height: 100%;
overflow: hidden;
} }
/*.item-list {
overflow-x: hidden;
overflow-y: scroll;
}
.note-list .item {
height: 40px; This must match NoteList.itemHeight
vertical-align: middle;
cursor: pointer;
}
.note-list .item.odd {
background-color: lightgray;
}
.note-list .selected {
font-weight: bold;
}
*/
.note-list .list-item:hover { .note-list .list-item:hover {
background-color: rgba(0,160,255,0.1) !important; background-color: rgba(0,160,255,0.1) !important;
} }
.side-bar .selected {
font-weight: bold;
}
.side-bar .list-item:hover, .side-bar .list-item:hover,
.side-bar .synchronize-button:hover { .side-bar .synchronize-button:hover {
background-color: #533d7d; background-color: #533d7d;

@ -56,6 +56,16 @@ globalStyle.lineInput = {
backgroundColor: globalStyle.backgroundColor, backgroundColor: globalStyle.backgroundColor,
}; };
globalStyle.textStyle = {
color: globalStyle.color,
fontFamily: globalStyle.fontFamily,
fontSize: globalStyle.fontSize,
};
globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
color: globalStyle.color2,
});
let themeCache_ = {}; let themeCache_ = {};
function themeStyle(theme) { function themeStyle(theme) {

@ -23,6 +23,15 @@ class NoteTag extends BaseItem {
return this.modelSelectAll('SELECT * FROM note_tags WHERE note_id IN ("' + noteIds.join('","') + '")'); return this.modelSelectAll('SELECT * FROM note_tags WHERE note_id IN ("' + noteIds.join('","') + '")');
} }
static async tagIdsByNoteId(noteId) {
let rows = await this.db().selectAll('SELECT tag_id FROM note_tags WHERE note_id = ?', [noteId]);
let output = [];
for (let i = 0; i < rows.length; i++) {
output.push(rows[i].tag_id);
}
return output;
}
} }
module.exports = { NoteTag }; module.exports = { NoteTag };

@ -98,7 +98,38 @@ class Tag extends BaseItem {
return await Tag.modelSelectAll('SELECT * FROM tags WHERE id IN (SELECT DISTINCT tag_id FROM note_tags)'); return await Tag.modelSelectAll('SELECT * FROM tags WHERE id IN (SELECT DISTINCT tag_id FROM note_tags)');
} }
static async tagsByNoteId(noteId) {
const tagIds = await NoteTag.tagIdsByNoteId(noteId);
return this.modelSelectAll('SELECT * FROM tags WHERE id IN ("' + tagIds.join('","') + '")');
}
static async setNoteTagsByTitles(noteId, tagTitles) {
const previousTags = await this.tagsByNoteId(noteId);
const addedTitles = [];
for (let i = 0; i < tagTitles.length; i++) {
const title = tagTitles[i].trim().toLowerCase();
if (!title) continue;
let tag = await this.loadByField('title', title);
if (!tag) tag = await Tag.save({ title: title }, { userSideValidation: true });
await this.addNote(tag.id, noteId);
addedTitles.push(title);
}
for (let i = 0; i < previousTags.length; i++) {
if (addedTitles.indexOf(previousTags[i].title) < 0) {
await this.removeNote(previousTags[i].id, noteId);
}
}
}
static async save(o, options = null) { static async save(o, options = null) {
if (options.userSideValidation) {
if ('title' in o) {
o.title = o.title.trim().toLowerCase();
}
}
return super.save(o, options).then((tag) => { return super.save(o, options).then((tag) => {
this.dispatch({ this.dispatch({
type: 'TAG_UPDATE_ONE', type: 'TAG_UPDATE_ONE',