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:
parent
e649670bfe
commit
eda3be066d
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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 });
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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} />
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -4,8 +4,10 @@ body {
|
||||
}
|
||||
|
||||
#content {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin-desktop",
|
||||
"version": "0.0.1",
|
||||
"version": "0.10.0",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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 };
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user