1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +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

View File

@ -28,7 +28,7 @@ class Command extends BaseCommand {
if (command == 'add') {
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++) {
await Tag.addNote(tag.id, notes[i].id);
}

View File

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

View File

@ -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.setState({ doImport: false });

View File

@ -38,6 +38,10 @@ class ItemList extends React.Component {
render() {
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');
@ -60,7 +64,7 @@ class ItemList extends React.Component {
const that = this;
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 }
</div>
);

View File

@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const { Setting } = require('lib/models/setting.js');
const { Tag } = require('lib/models/tag.js');
const { Note } = require('lib/models/note.js');
const { Folder } = require('lib/models/folder.js');
const { themeStyle } = require('../theme.js');
@ -17,8 +18,6 @@ class MainScreenComponent extends React.Component {
componentWillMount() {
this.setState({
newNotePromptVisible: false,
newFolderPromptVisible: false,
promptOptions: null,
noteVisiblePanes: ['editor', 'viewer'],
});
@ -43,7 +42,7 @@ class MainScreenComponent extends React.Component {
this.setState({ noteVisiblePanes: panes });
}
doCommand(command) {
async doCommand(command) {
if (!command) return;
const createNewNote = async (title, isTodo) => {
@ -68,7 +67,7 @@ class MainScreenComponent extends React.Component {
if (command.name === 'newNote') {
this.setState({
promptOptions: {
message: _('Note title:'),
label: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
@ -78,7 +77,7 @@ class MainScreenComponent extends React.Component {
} else if (command.name === 'newTodo') {
this.setState({
promptOptions: {
message: _('To-do title:'),
label: _('To-do title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
@ -88,7 +87,7 @@ class MainScreenComponent extends React.Component {
} else if (command.name === 'newNotebook') {
this.setState({
promptOptions: {
message: _('Notebook title:'),
label: _('Notebook title:'),
onClose: async (answer) => {
if (answer) {
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 });
}
},
@ -133,14 +150,14 @@ class MainScreenComponent extends React.Component {
const rowHeight = style.height - theme.headerHeight;
const sideBarStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)),
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteListStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)),
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
@ -188,7 +205,14 @@ class MainScreenComponent extends React.Component {
return (
<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} />
<SideBar style={sideBarStyle} />
<NoteList itemHeight={40} style={noteListStyle} />

View File

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

View File

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

View File

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

View File

@ -75,6 +75,7 @@ class SideBarComponent extends React.Component {
marginTop: 10,
marginLeft: 5,
marginRight: 5,
minHeight: 70,
},
};
@ -157,7 +158,10 @@ class SideBarComponent extends React.Component {
render() {
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 = [];

View File

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

View File

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

View File

@ -6,37 +6,13 @@ body, textarea {
#react-root {
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 {
background-color: rgba(0,160,255,0.1) !important;
}
.side-bar .selected {
font-weight: bold;
}
.side-bar .list-item:hover,
.side-bar .synchronize-button:hover {
background-color: #533d7d;

View File

@ -56,6 +56,16 @@ globalStyle.lineInput = {
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_ = {};
function themeStyle(theme) {

View File

@ -23,6 +23,15 @@ class NoteTag extends BaseItem {
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 };

View File

@ -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)');
}
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) {
if (options.userSideValidation) {
if ('title' in o) {
o.title = o.title.trim().toLowerCase();
}
}
return super.save(o, options).then((tag) => {
this.dispatch({
type: 'TAG_UPDATE_ONE',