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

Delete folders and tags

This commit is contained in:
Laurent Cozic 2017-11-08 21:22:24 +00:00
parent 7d12da27ad
commit e3554c7aec
20 changed files with 176 additions and 92 deletions

View File

@ -112,8 +112,8 @@ class AppGui {
if (!nextItem) return; // Normally not possible
let actionType = 'FOLDERS_SELECT';
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAGS_SELECT';
let actionType = 'FOLDER_SELECT';
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAG_SELECT';
if (nextItem.type_ === BaseModel.TYPE_SEARCH) actionType = 'SEARCH_SELECT';
this.store_.dispatch({
@ -122,12 +122,12 @@ class AppGui {
});
} else if (item.type_ === Folder.modelType()) {
this.store_.dispatch({
type: 'FOLDERS_SELECT',
type: 'FOLDER_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === Tag.modelType()) {
this.store_.dispatch({
type: 'TAGS_SELECT',
type: 'TAG_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
@ -160,8 +160,8 @@ class AppGui {
noteList.on('currentItemChange', async () => {
let note = noteList.currentItem;
this.store_.dispatch({
type: 'NOTES_SELECT',
noteId: note ? note.id : null,
type: 'NOTE_SELECT',
id: note ? note.id : null,
});
});
this.rootWidget_.connect(noteList, (state) => {

View File

@ -324,7 +324,7 @@ class Application extends BaseApplication {
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTINGS_UPDATE_ALL even, which mean state.settings will not be
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
@ -333,12 +333,12 @@ class Application extends BaseApplication {
const tags = await Tag.allWithNotes();
this.dispatch({
type: 'TAGS_UPDATE_ALL',
type: 'TAG_UPDATE_ALL',
tags: tags,
});
this.store().dispatch({
type: 'FOLDERS_SELECT',
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
}

View File

@ -98,8 +98,8 @@ class Command extends BaseCommand {
}
this.dispatch({
type: 'NOTES_SELECT',
noteId: note.id,
type: 'NOTE_SELECT',
id: note.id,
});
await onFinishedEditing();

View File

@ -87,7 +87,7 @@ class Application extends BaseApplication {
this.initRedux();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTINGS_UPDATE_ALL even, which mean state.settings will not be
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
@ -96,12 +96,12 @@ class Application extends BaseApplication {
const tags = await Tag.allWithNotes();
this.dispatch({
type: 'TAGS_UPDATE_ALL',
type: 'TAG_UPDATE_ALL',
tags: tags,
});
this.store().dispatch({
type: 'FOLDERS_SELECT',
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
}

View File

@ -7,6 +7,7 @@ const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const { Folder } = require('lib/models/folder.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const layoutUtils = require('lib/layout-utils.js');
@ -16,6 +17,7 @@ class MainScreenComponent extends React.Component {
componentWillMount() {
this.setState({ newNotePromptVisible: false });
this.setState({ newFolderPromptVisible: false });
}
render() {
@ -60,28 +62,57 @@ class MainScreenComponent extends React.Component {
onClick: () => {
this.setState({ newNotePromptVisible: true });
},
}, {
title: _('New notebook'),
onClick: () => {
this.setState({ newFolderPromptVisible: true });
},
},
];
const newNotePromptOnAccept = async (answer) => {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const newNotePromptOnClose = async (answer) => {
if (answer) {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: answer,
parent_id: folderId,
});
Note.updateGeolocation(note.id);
const note = await Note.save({
title: answer,
parent_id: folderId,
});
Note.updateGeolocation(note.id);
this.props.dispatch({
type: 'NOTES_SELECT',
noteId: note.id,
});
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
}
this.setState({ newNotePromptVisible: false });
}
const newFolderPromptOnClose = async (answer) => {
if (answer) {
let folder = null;
try {
folder = await Folder.save({ title: answer }, { userSideValidation: true });
} catch (error) {
bridge().showErrorMessageBox(error.message);
return;
}
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder.id,
});
}
this.setState({ newFolderPromptVisible: false });
}
return (
<div style={style}>
<PromptDialog style={promptStyle} onAccept={(answer) => newNotePromptOnAccept(answer)} message={_('Note title:')} visible={this.state.newNotePromptVisible}/>
<PromptDialog style={promptStyle} onClose={(answer) => newNotePromptOnClose(answer)} message={_('Note title:')} visible={this.state.newNotePromptVisible}/>
<PromptDialog style={promptStyle} onClose={(answer) => newFolderPromptOnClose(answer)} message={_('Notebook title:')} visible={this.state.newFolderPromptVisible}/>
<Header style={headerStyle} showBackButton={false} buttons={headerButtons} />
<SideBar style={sideBarStyle} />
<NoteList itemHeight={40} style={noteListStyle} />

View File

@ -14,7 +14,7 @@ class NoteListComponent extends React.Component {
if (!noteId) throw new Error('No data-id on element');
const menu = new Menu()
menu.append(new MenuItem({label: _('Delete'), async click() {
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(_('Delete note?'));
if (!ok) return;
await Note.delete(noteId);
@ -25,8 +25,8 @@ class NoteListComponent extends React.Component {
itemRenderer(index, item, theme) {
const onClick = (item) => {
this.props.dispatch({
type: 'NOTES_SELECT',
noteId: item.id,
type: 'NOTE_SELECT',
id: item.id,
});
}

View File

@ -60,13 +60,8 @@ class PromptDialog extends React.Component {
maxWidth: 400,
};
const onAccept = () => {
if (this.props.onAccept) this.props.onAccept(this.state.answer);
this.setState({ visible: false, answer: '' });
}
const onReject = () => {
if (this.props.onReject) this.props.onReject();
const onClose = (accept) => {
if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null);
this.setState({ visible: false, answer: '' });
}
@ -76,9 +71,9 @@ class PromptDialog extends React.Component {
const onKeyDown = (event) => {
if (event.key === 'Enter') {
onAccept();
onClose(true);
} else if (event.key === 'Escape') {
onReject();
onClose(false);
}
}
@ -94,8 +89,8 @@ class PromptDialog extends React.Component {
onChange={(event) => onChange(event)}
onKeyDown={(event) => onKeyDown(event)} />
<div style={{ textAlign: 'right', marginTop: 10 }}>
<button style={buttonStyle} onClick={() => onAccept()}>OK</button>
<button style={buttonStyle} onClick={() => onReject()}>Cancel</button>
<button style={buttonStyle} onClick={() => onClose(true)}>OK</button>
<button style={buttonStyle} onClick={() => onClose(false)}>Cancel</button>
</div>
</div>
</div>

View File

@ -30,14 +30,14 @@ class RootComponent extends React.Component {
async componentDidMount() {
if (this.props.appState == 'starting') {
this.props.dispatch({
type: 'SET_APP_STATE',
type: 'APP_STATE_SET',
state: 'initializing',
});
await initialize(this.props.dispatch);
this.props.dispatch({
type: 'SET_APP_STATE',
type: 'APP_STATE_SET',
state: 'ready',
});
}

View File

@ -2,7 +2,14 @@ const React = require('react');
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Tag } = require('lib/models/tag.js');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
class SideBarComponent extends React.Component {
@ -23,16 +30,45 @@ class SideBarComponent extends React.Component {
return style;
}
itemContextMenu(event) {
const itemId = event.target.getAttribute('data-id');
const itemType = Number(event.target.getAttribute('data-type'));
if (!itemId || !itemType) throw new Error('No data on element');
let deleteMessage = '';
if (itemType === BaseModel.TYPE_FOLDER) {
deleteMessage = _('Delete notebook?');
} else if (itemType === BaseModel.TYPE_TAG) {
deleteMessage = _('Remove this tag from all the notes?');
}
const menu = new Menu();
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage);
if (!ok) return;
if (itemType === BaseModel.TYPE_FOLDER) {
await Folder.delete(itemId);
} else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId);
}
}}))
menu.popup(bridge().window());
}
folderItem_click(folder) {
this.props.dispatch({
type: 'FOLDERS_SELECT',
type: 'FOLDER_SELECT',
id: folder ? folder.id : null,
});
}
tagItem_click(tag) {
this.props.dispatch({
type: 'TAGS_SELECT',
type: 'TAG_SELECT',
id: tag ? tag.id : null,
});
}
@ -45,14 +81,14 @@ class SideBarComponent extends React.Component {
const style = Object.assign({}, this.style().listItem, {
fontWeight: selected ? 'bold' : 'normal',
});
return <a href="#" key={folder.id} style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}</a>
return <a href="#" data-id={folder.id} data-type={BaseModel.TYPE_FOLDER} onContextMenu={(event) => this.itemContextMenu(event)} key={folder.id} style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}</a>
}
tagItem(tag, selected) {
const style = Object.assign({}, this.style().listItem, {
fontWeight: selected ? 'bold' : 'normal',
});
return <a href="#" key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>Tag: {tag.title}</a>
return <a href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>Tag: {tag.title}</a>
}
makeDivider(key) {

View File

@ -56,7 +56,7 @@ class BaseApplication {
switchCurrentFolder(folder) {
this.dispatch({
type: 'FOLDERS_SELECT',
type: 'FOLDER_SELECT',
id: folder ? folder.id : '',
});
}
@ -169,14 +169,14 @@ class BaseApplication {
}
this.store().dispatch({
type: 'NOTES_UPDATE_ALL',
type: 'NOTE_UPDATE_ALL',
notes: notes,
notesSource: source,
});
this.store().dispatch({
type: 'NOTES_SELECT',
noteId: notes.length ? notes[0].id : null,
type: 'NOTE_SELECT',
id: notes.length ? notes[0].id : null,
});
}
@ -203,13 +203,13 @@ class BaseApplication {
const result = next(action);
const newState = store.getState();
if (action.type == 'FOLDERS_SELECT' || action.type === 'FOLDER_DELETE') {
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE') {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
await this.refreshNotes(Folder.modelType(), newState.selectedFolderId);
}
if (action.type == 'TAGS_SELECT') {
if (action.type == 'TAG_SELECT') {
await this.refreshNotes(Tag.modelType(), action.id);
}
@ -217,7 +217,7 @@ class BaseApplication {
await this.refreshNotes(BaseModel.TYPE_SEARCH, action.id);
}
if (this.hasGui() && action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') {
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}

View File

@ -203,8 +203,6 @@ class MdToHtml {
if (!options) options = {};
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
console.info(style);
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;

View File

@ -62,7 +62,7 @@ class NotesScreenComponent extends BaseScreenComponent {
}
this.props.dispatch({
type: 'NOTES_UPDATE_ALL',
type: 'NOTE_UPDATE_ALL',
notes: notes,
notesSource: source,
});

View File

@ -40,7 +40,7 @@ class TagScreenComponent extends BaseScreenComponent {
const notes = await Tag.notes(props.selectedTagId);
this.props.dispatch({
type: 'NOTES_UPDATE_ALL',
type: 'NOTE_UPDATE_ALL',
notes: notes,
notesSource: source,
});

View File

@ -6,7 +6,7 @@ class FoldersScreenUtils {
let initialFolders = await Folder.all({ includeConflictFolder: true });
this.dispatch({
type: 'FOLDERS_UPDATE_ALL',
type: 'FOLDER_UPDATE_ALL',
folders: initialFolders,
});
}

View File

@ -143,7 +143,7 @@ class Folder extends BaseItem {
return super.save(o, options).then((folder) => {
this.dispatch({
type: 'FOLDERS_UPDATE_ONE',
type: 'FOLDER_UPDATE_ONE',
folder: folder,
});
return folder;

View File

@ -386,7 +386,7 @@ class Note extends BaseItem {
return super.save(o, options).then((note) => {
this.dispatch({
type: 'NOTES_UPDATE_ONE',
type: 'NOTE_UPDATE_ONE',
note: note,
});
@ -398,7 +398,7 @@ class Note extends BaseItem {
let r = await super.delete(id, options);
this.dispatch({
type: 'NOTES_DELETE',
type: 'NOTE_DELETE',
noteId: id,
});
}
@ -407,7 +407,7 @@ class Note extends BaseItem {
const result = super.batchDelete(ids, options);
for (let i = 0; i < ids.length; i++) {
this.dispatch({
type: 'NOTES_DELETE',
type: 'NOTE_DELETE',
noteId: ids[i],
});
}

View File

@ -80,7 +80,7 @@ class Setting extends BaseModel {
}
this.dispatch({
type: 'SETTINGS_UPDATE_ALL',
type: 'SETTING_UPDATE_ALL',
settings: keyToValues,
});
}
@ -113,7 +113,7 @@ class Setting extends BaseModel {
c.value = this.formatValue(key, value);
this.dispatch({
type: 'SETTINGS_UPDATE_ONE',
type: 'SETTING_UPDATE_ONE',
key: key,
value: c.value,
});
@ -129,7 +129,7 @@ class Setting extends BaseModel {
});
this.dispatch({
type: 'SETTINGS_UPDATE_ONE',
type: 'SETTING_UPDATE_ONE',
key: key,
value: this.formatValue(key, value),
});

View File

@ -39,6 +39,30 @@ class Tag extends BaseItem {
});
}
// Untag all the notes and delete tag
static async untagAll(tagId) {
const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ?', [tagId]);
for (let i = 0; i < noteTags.length; i++) {
await NoteTag.delete(noteTags[i].id);
}
const tags = await Tag.allWithNotes();
this.dispatch({
type: 'TAG_UPDATE_ALL',
tags: tags,
});
this.dispatch({
type: 'TAG_SELECT',
id: null,
});
// Currently not actually deleting it as the reducer would need to be updated. The
// tag will stay in the db but just won't show up in the UI.
//await Tag.delete(tagId);
}
static async addNote(tagId, noteId) {
let hasIt = await this.hasNote(tagId, noteId);
if (hasIt) return;
@ -49,7 +73,7 @@ class Tag extends BaseItem {
});
this.dispatch({
type: 'TAGS_UPDATE_ONE',
type: 'TAG_UPDATE_ONE',
tag: await Tag.load(tagId),
});
@ -63,7 +87,7 @@ class Tag extends BaseItem {
}
this.dispatch({
type: 'TAGS_UPDATE_ONE',
type: 'TAG_UPDATE_ONE',
tag: await Tag.load(tagId),
});
}
@ -80,7 +104,7 @@ class Tag extends BaseItem {
static async save(o, options = null) {
return super.save(o, options).then((tag) => {
this.dispatch({
type: 'TAGS_UPDATE_ONE',
type: 'TAG_UPDATE_ONE',
tag: tag,
});
return tag;

View File

@ -59,8 +59,8 @@ function folderOrNoteDelete(state, action) {
}
function updateOneTagOrFolder(state, action) {
let newItems = action.type === 'TAGS_UPDATE_ONE' ? state.tags.splice(0) : state.folders.splice(0);
let item = action.type === 'TAGS_UPDATE_ONE' ? action.tag : action.folder;
let newItems = action.type === 'TAG_UPDATE_ONE' ? state.tags.splice(0) : state.folders.splice(0);
let item = action.type === 'TAG_UPDATE_ONE' ? action.tag : action.folder;
var found = false;
for (let i = 0; i < newItems.length; i++) {
@ -76,7 +76,7 @@ function updateOneTagOrFolder(state, action) {
let newState = Object.assign({}, state);
if (action.type === 'TAGS_UPDATE_ONE') {
if (action.type === 'TAG_UPDATE_ONE') {
newState.tags = newItems;
} else {
newState.folders = newItems;
@ -105,13 +105,13 @@ const reducer = (state = defaultState, action) => {
try {
switch (action.type) {
case 'NOTES_SELECT':
case 'NOTE_SELECT':
newState = Object.assign({}, state);
newState.selectedNoteId = action.noteId;
newState.selectedNoteId = action.id;
break;
case 'FOLDERS_SELECT':
case 'FOLDER_SELECT':
newState = Object.assign({}, state);
newState.selectedFolderId = action.id;
@ -122,13 +122,13 @@ const reducer = (state = defaultState, action) => {
}
break;
case 'SETTINGS_UPDATE_ALL':
case 'SETTING_UPDATE_ALL':
newState = Object.assign({}, state);
newState.settings = action.settings;
break;
case 'SETTINGS_UPDATE_ONE':
case 'SETTING_UPDATE_ONE':
newState = Object.assign({}, state);
let newSettings = Object.assign({}, state.settings);
@ -137,7 +137,7 @@ const reducer = (state = defaultState, action) => {
break;
// Replace all the notes with the provided array
case 'NOTES_UPDATE_ALL':
case 'NOTE_UPDATE_ALL':
newState = Object.assign({}, state);
newState.notes = action.notes;
@ -146,7 +146,7 @@ const reducer = (state = defaultState, action) => {
// Insert the note into the note list if it's new, or
// update it within the note array if it already exists.
case 'NOTES_UPDATE_ONE':
case 'NOTE_UPDATE_ONE':
const modNote = action.note;
@ -188,24 +188,24 @@ const reducer = (state = defaultState, action) => {
}
break;
case 'NOTES_DELETE':
case 'NOTE_DELETE':
newState = folderOrNoteDelete(state, action);
break;
case 'FOLDERS_UPDATE_ALL':
case 'FOLDER_UPDATE_ALL':
newState = Object.assign({}, state);
newState.folders = action.folders;
break;
case 'TAGS_UPDATE_ALL':
case 'TAG_UPDATE_ALL':
newState = Object.assign({}, state);
newState.tags = action.tags;
break;
case 'TAGS_SELECT':
case 'TAG_SELECT':
newState = Object.assign({}, state);
newState.selectedTagId = action.id;
@ -216,12 +216,12 @@ const reducer = (state = defaultState, action) => {
}
break;
case 'TAGS_UPDATE_ONE':
case 'TAG_UPDATE_ONE':
newState = updateOneTagOrFolder(state, action);
break;
case 'FOLDERS_UPDATE_ONE':
case 'FOLDER_UPDATE_ONE':
newState = updateOneTagOrFolder(state, action);
break;
@ -292,7 +292,7 @@ const reducer = (state = defaultState, action) => {
}
break;
case 'SET_APP_STATE':
case 'APP_STATE_SET':
newState = Object.assign({}, state);
newState.appState = action.state;

View File

@ -46,15 +46,15 @@ const generalMiddleware = store => next => async (action) => {
if (action.type == 'NAV_GO') Keyboard.dismiss();
if (['NOTES_UPDATE_ONE', 'NOTES_DELETE', 'FOLDERS_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncStarted()) reg.scheduleSync();
}
if (action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}
if (action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTINGS_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
}
@ -281,7 +281,7 @@ async function initialize(dispatch, backButtonHandler) {
const tags = await Tag.all();
dispatch({
type: 'TAGS_UPDATE_ALL',
type: 'TAG_UPDATE_ALL',
tags: tags,
});
@ -329,14 +329,14 @@ class AppComponent extends React.Component {
async componentDidMount() {
if (this.props.appState == 'starting') {
this.props.dispatch({
type: 'SET_APP_STATE',
type: 'APP_STATE_SET',
state: 'initializing',
});
await initialize(this.props.dispatch, this.backButtonHandler.bind(this));
this.props.dispatch({
type: 'SET_APP_STATE',
type: 'APP_STATE_SET',
state: 'ready',
});
}