diff --git a/CliClient/app/command-dump.js b/CliClient/app/command-dump.js index 087dbd839..7db87bf5c 100644 --- a/CliClient/app/command-dump.js +++ b/CliClient/app/command-dump.js @@ -31,7 +31,7 @@ class Command extends BaseCommand { let tags = await Tag.all(); for (let i = 0; i < tags.length; i++) { - tags[i].notes_ = await Tag.tagNoteIds(tags[i].id); + tags[i].notes_ = await Tag.noteIds(tags[i].id); } items = items.concat(tags); diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index bab33069e..142cb74b4 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -511,24 +511,24 @@ describe('Synchronizer', function() { expect(remoteTag.id).toBe(tag.id); await Tag.addNote(remoteTag.id, n1.id); await Tag.addNote(remoteTag.id, n2.id); - let noteIds = await Tag.tagNoteIds(tag.id); + let noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(2); await synchronizer().start(); await switchClient(1); await synchronizer().start(); - let remoteNoteIds = await Tag.tagNoteIds(tag.id); + let remoteNoteIds = await Tag.noteIds(tag.id); expect(remoteNoteIds.length).toBe(2); await Tag.removeNote(tag.id, n1.id); - remoteNoteIds = await Tag.tagNoteIds(tag.id); + remoteNoteIds = await Tag.noteIds(tag.id); expect(remoteNoteIds.length).toBe(1); await synchronizer().start(); await switchClient(2); await synchronizer().start(); - noteIds = await Tag.tagNoteIds(tag.id); + noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(1); expect(remoteNoteIds[0]).toBe(noteIds[0]); diff --git a/ReactNativeClient/lib/base-model.js b/ReactNativeClient/lib/base-model.js index 24bbebda7..aa23191e5 100644 --- a/ReactNativeClient/lib/base-model.js +++ b/ReactNativeClient/lib/base-model.js @@ -307,6 +307,10 @@ class BaseModel { return this.db_; } + static isReady() { + return !!this.db_; + } + } BaseModel.TYPE_NOTE = 1; diff --git a/ReactNativeClient/lib/components/global-style.js b/ReactNativeClient/lib/components/global-style.js index 0fd4bf861..0c591c9ee 100644 --- a/ReactNativeClient/lib/components/global-style.js +++ b/ReactNativeClient/lib/components/global-style.js @@ -5,7 +5,7 @@ const globalStyle = { colorFaded: "#777777", // For less important text fontSize: 10, dividerColor: "#dddddd", - selectedColor: '#eeeeee', + selectedColor: '#e5e5e5', disabledOpacity: 0.3, // For WebView - must correspond to the properties above diff --git a/ReactNativeClient/lib/components/note-item.js b/ReactNativeClient/lib/components/note-item.js index 5c3eb0669..56dff02ae 100644 --- a/ReactNativeClient/lib/components/note-item.js +++ b/ReactNativeClient/lib/components/note-item.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux' -import { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } from 'react-native'; +import { ListView, Text, TouchableHighlight, View, StyleSheet } from 'react-native'; import { Log } from 'lib/log.js'; import { _ } from 'lib/locale.js'; import { Checkbox } from 'lib/components/checkbox.js'; @@ -39,10 +39,29 @@ class NoteItemComponent extends Component { }); } + async todoCheckbox_change(checked) { + if (!this.props.note) return; + + const newNote = { + id: this.props.note.id, + todo_completed: checked ? time.unixMs() : 0, + } + await Note.save(newNote); + } + + onPress() { + if (!this.props.note) return; + + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'Note', + noteId: this.props.note.id, + }); + } + render() { const note = this.props.note ? this.props.note : {}; const onPress = this.props.onPress; - const onLongPress = this.props.onLongPress; const onCheckboxChange = this.props.onCheckboxChange; const checkboxStyle = !Number(note.is_todo) ? { display: 'none' } : { color: globalStyle.color }; @@ -51,9 +70,14 @@ class NoteItemComponent extends Component { const listItemStyle = !!Number(note.is_todo) && checkboxChecked ? styles.listItemFadded : styles.listItem; return ( - onPress ? onPress(note) : this.noteItem_press(note.id)} onLongPress={() => onLongPress(note)} underlayColor="#0066FF"> + this.onPress()} underlayColor="#0066FF"> - { onCheckboxChange(note, checked) }}/>{note.title} + this.todoCheckbox_change(checked)} + /> + {note.title} ); diff --git a/ReactNativeClient/lib/components/note-list.js b/ReactNativeClient/lib/components/note-list.js index 78919b156..f768c3c28 100644 --- a/ReactNativeClient/lib/components/note-list.js +++ b/ReactNativeClient/lib/components/note-list.js @@ -66,21 +66,6 @@ class NoteListComponent extends Component { }); } - async todoCheckbox_change(itemId, checked) { - let note = await Note.load(itemId); - await Note.save({ id: note.id, todo_completed: checked ? time.unixMs() : 0 }); - } - - listView_itemPress(noteId) { - this.props.dispatch({ - type: 'NAV_GO', - routeName: 'Note', - noteId: noteId, - }); - } - - listView_itemLongPress(itemId) {} - render() { // `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39 @@ -89,11 +74,8 @@ class NoteListComponent extends Component { { - return this.listView_itemPress(note.id) } - onCheckboxChange={(note, checked) => this.todoCheckbox_change(note.id, checked) } - /> }} + return + }} enableEmptySections={true} /> ); diff --git a/ReactNativeClient/lib/components/screens/notes.js b/ReactNativeClient/lib/components/screens/notes.js index 310e62b79..a446a987d 100644 --- a/ReactNativeClient/lib/components/screens/notes.js +++ b/ReactNativeClient/lib/components/screens/notes.js @@ -5,6 +5,7 @@ import { reg } from 'lib/registry.js'; import { Log } from 'lib/log.js' import { NoteList } from 'lib/components/note-list.js' import { Folder } from 'lib/models/folder.js' +import { Tag } from 'lib/models/tag.js' import { Note } from 'lib/models/note.js' import { ScreenHeader } from 'lib/components/screen-header.js'; import { MenuOption, Text } from 'react-native-popup-menu'; @@ -27,7 +28,9 @@ class NotesScreenComponent extends BaseScreenComponent { async componentWillReceiveProps(newProps) { if (newProps.notesOrder.orderBy != this.props.notesOrder.orderBy || newProps.notesOrder.orderByDir != this.props.notesOrder.orderByDir || - newProps.selectedFolderId != this.props.selectedFolderId) { + newProps.selectedFolderId != this.props.selectedFolderId || + newProps.selectedTagId != this.props.selectedTagId || + newProps.notesParentType != this.props.notesParentType) { await this.refreshNotes(newProps); } } @@ -40,16 +43,26 @@ class NotesScreenComponent extends BaseScreenComponent { orderByDir: props.notesOrder.orderByDir, }; + const parent = this.parentItem(props); + const source = JSON.stringify({ options: options, - selectedFolderId: props.selectedFolderId, + parentId: parent.id, }); - let folder = Folder.byId(props.folders, props.selectedFolderId); + if (source == props.notesSource) { + console.info('NO SOURCE CHAGNE'); + console.info(source); + console.info(props.notesSource); + return; + } - if (source == props.notesSource) return; - - const notes = await Note.previews(props.selectedFolderId, options); + let notes = []; + if (props.notesParentType == 'Folder') { + notes = await Note.previews(props.selectedFolderId, options); + } else { + notes = await Tag.notes(props.selectedTagId); // TODO: should also return previews + } this.props.dispatch({ type: 'NOTES_UPDATE_ALL', @@ -82,18 +95,36 @@ class NotesScreenComponent extends BaseScreenComponent { } menuOptions() { - if (this.props.selectedFolderId == Folder.conflictFolderId()) return []; - - return [ - { title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } }, - { title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } }, - ]; + if (this.props.notesParentType == 'Folder') { + if (this.props.selectedFolderId == Folder.conflictFolderId()) return []; + + return [ + { title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } }, + { title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } }, + ]; + } else { + return []; // TODO + } + } + + parentItem(props = null) { + if (!props) props = this.props; + + let output = null; + if (props.notesParentType == 'Folder') { + output = Folder.byId(props.folders, props.selectedFolderId); + } else if (props.notesParentType == 'Tag') { + output = Tag.byId(props.tags, props.selectedTagId); + } else { + throw new Error('Invalid parent type: ' + props.notesParentType); + } + return output; } render() { - let folder = Folder.byId(this.props.folders, this.props.selectedFolderId); + const parent = this.parentItem(); - if (!folder) { + if (!parent) { return ( @@ -101,8 +132,8 @@ class NotesScreenComponent extends BaseScreenComponent { ) } - let title = folder ? folder.title : null; - const addFolderNoteButtons = folder.id != Folder.conflictFolderId(); + let title = parent ? parent.title : null; + const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId(); const { navigate } = this.props.navigation; return ( @@ -120,7 +151,10 @@ const NotesScreen = connect( (state) => { return { folders: state.folders, + tags: state.tags, selectedFolderId: state.selectedFolderId, + selectedTagId: state.selectedTagId, + notesParentType: state.notesParentType, notes: state.notes, notesOrder: state.notesOrder, notesSource: state.notesSource, diff --git a/ReactNativeClient/lib/components/screens/search.js b/ReactNativeClient/lib/components/screens/search.js index 8a4629aea..ddc2dc02c 100644 --- a/ReactNativeClient/lib/components/screens/search.js +++ b/ReactNativeClient/lib/components/screens/search.js @@ -134,9 +134,7 @@ class SearchScreenComponent extends BaseScreenComponent { item.id} - renderItem={(event) => } + renderItem={(event) => } /> diff --git a/ReactNativeClient/lib/components/screens/tag.js b/ReactNativeClient/lib/components/screens/tag.js new file mode 100644 index 000000000..cba3517e3 --- /dev/null +++ b/ReactNativeClient/lib/components/screens/tag.js @@ -0,0 +1,76 @@ +import React, { Component } from 'react'; +import { ListView, StyleSheet, View, TextInput, FlatList, TouchableHighlight } from 'react-native'; +import { connect } from 'react-redux' +import { ScreenHeader } from 'lib/components/screen-header.js'; +import Icon from 'react-native-vector-icons/Ionicons'; +import { _ } from 'lib/locale.js'; +import { Note } from 'lib/models/note.js'; +import { NoteItem } from 'lib/components/note-item.js'; +import { BaseScreenComponent } from 'lib/components/base-screen.js'; +import { globalStyle } from 'lib/components/global-style.js'; + +let styles = { + body: { + flex: 1, + }, +} + +class TagScreenComponent extends BaseScreenComponent { + + static navigationOptions(options) { + return { header: null }; + } + + componentDidMount() { + this.refreshNotes(); + } + + componentWillReceiveProps(newProps) { + if (newProps.selectedTagId !== this.props.selectedTagId) { + this.refreshNotes(newProps); + } + } + + async refreshNotes(props = null) { + if (props === null) props = this.props; + + const source = JSON.stringify({ selectedTagId: props.selectedTagId }); + if (source == props.tagNotesSource) return; + + const notes = await Tag.notes(props.selectedTagId); + + this.props.dispatch({ + type: 'NOTES_UPDATE_ALL', + notes: notes, + notesSource: source, + }); + } + + render() { + let title = tag ? tag.title : ''; + + // + + const { navigate } = this.props.navigation; + return ( + + + + { this.dialogbox = dialogbox }}/> + + ); + } + +} + +const TagScreen = connect( + (state) => { + return { + tag: tag, + notes: state.notes, + notesSource: state.notesSource, + }; + } +)(TagScreenComponent) + +export { TagScreen }; \ No newline at end of file diff --git a/ReactNativeClient/lib/components/side-menu-content.js b/ReactNativeClient/lib/components/side-menu-content.js index 043514936..9832eb5d1 100644 --- a/ReactNativeClient/lib/components/side-menu-content.js +++ b/ReactNativeClient/lib/components/side-menu-content.js @@ -3,6 +3,7 @@ import { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View } import { connect } from 'react-redux' import Icon from 'react-native-vector-icons/Ionicons'; import { Log } from 'lib/log.js'; +import { Tag } from 'lib/models/tag.js'; import { Note } from 'lib/models/note.js'; import { Setting } from 'lib/models/setting.js'; import { FoldersScreenUtils } from 'lib/components/screens/folders-utils.js' @@ -11,7 +12,7 @@ import { reg } from 'lib/registry.js'; import { _ } from 'lib/locale.js'; import { globalStyle } from 'lib/components/global-style.js'; -const styleObject = { +let styles = { menu: { flex: 1, backgroundColor: globalStyle.backgroundColor, @@ -43,24 +44,39 @@ const styleObject = { paddingRight: globalStyle.marginRight, color: globalStyle.colorFaded, }, + tagItemList: { + flex: 1, + flexDirection: 'row', + flexWrap: 'wrap' + }, }; -styleObject.folderButton = Object.assign({}, styleObject.button); -styleObject.folderButtonText = Object.assign({}, styleObject.buttonText); -styleObject.folderIcon = Object.assign({}, globalStyle.icon); -styleObject.folderIcon.color = '#0072d5'; -styleObject.syncButton = Object.assign({}, styleObject.button); -styleObject.syncButtonText = Object.assign({}, styleObject.buttonText); -styleObject.folderButtonSelected = Object.assign({}, styleObject.folderButton); -styleObject.folderButtonSelected.backgroundColor = globalStyle.selectedColor; +styles.folderButton = Object.assign({}, styles.button); +styles.folderButtonText = Object.assign({}, styles.buttonText); +styles.folderButtonSelected = Object.assign({}, styles.folderButton); +styles.folderButtonSelected.backgroundColor = globalStyle.selectedColor; +styles.folderIcon = Object.assign({}, globalStyle.icon); +styles.folderIcon.color = '#0072d5'; -const styles = StyleSheet.create(styleObject); +styles.tagButton = Object.assign({}, styles.button); +styles.tagButtonSelected = Object.assign({}, styles.tagButton); +styles.tagButtonSelected.backgroundColor = globalStyle.selectedColor; +styles.tagButtonSelected.borderRadius = 1000; +styles.tagButtonText = Object.assign({}, styles.buttonText); +styles.tagButtonText.flex = 0; + +styles.syncButton = Object.assign({}, styles.button); +styles.syncButtonText = Object.assign({}, styles.buttonText); + +styles = StyleSheet.create(styles); class SideMenuContentComponent extends Component { constructor() { super(); - this.state = { syncReportText: '' }; + this.state = { syncReportText: '', + //width: 0, + }; } folder_press(folder) { @@ -73,6 +89,16 @@ class SideMenuContentComponent extends Component { }); } + tag_press(tag) { + this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); + + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'Notes', + tagId: tag.id, + }); + } + async synchronize_press() { if (Setting.value('sync.target') == Setting.SYNC_TARGET_ONEDRIVE && !reg.oneDriveApi().auth()) { this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); @@ -107,6 +133,20 @@ class SideMenuContentComponent extends Component { ); } + tagItem(tag, selected) { + const iconComp = + const tagButtonStyle = selected ? styles.tagButtonSelected : styles.tagButton; + + return ( + { this.tag_press(tag) }}> + + { iconComp } + {tag.title} + + + ); + } + synchronizeButton(state) { const title = state == 'sync' ? _('Synchronize') : _('Cancel synchronization'); const iconComp = state == 'sync' ? : ; @@ -121,6 +161,16 @@ class SideMenuContentComponent extends Component { ); } + makeDivider(key) { + return + } + + // onLayout(event) { + // const newWidth = event.nativeEvent.layout.width; + // if (this.state.width == newWidth) return; + // this.setState({ width: newWidth }); + // } + render() { let items = []; @@ -130,10 +180,24 @@ class SideMenuContentComponent extends Component { for (let i = 0; i < this.props.folders.length; i++) { let folder = this.props.folders[i]; - items.push(this.folderItem(folder, this.props.selectedFolderId == folder.id)); + items.push(this.folderItem(folder, this.props.selectedFolderId == folder.id && this.props.notesParentType == 'Folder')); } - if (items.length) items.push(); // DIVIDER + if (items.length) items.push(this.makeDivider('divider_1')); + + let tagItems = []; + for (let i = 0; i < this.props.tags.length; i++) { + const tag = this.props.tags[i]; + tagItems.push(this.tagItem(tag, this.props.selectedTagId == tag.id && this.props.notesParentType == 'Tag')); + } + + items.push( + + {tagItems} + + ); + + if (items.length) items.push(this.makeDivider('divider_2')); let lines = Synchronizer.reportToLines(this.props.syncReport); const syncReportText = lines.join("\n"); @@ -144,6 +208,8 @@ class SideMenuContentComponent extends Component { items.push(); + // onLayout={(event) => this.onLayout(event)} + return ( @@ -161,9 +227,12 @@ const SideMenuContent = connect( (state) => { return { folders: state.folders, + tags: state.tags, syncStarted: state.syncStarted, syncReport: state.syncReport, selectedFolderId: state.selectedFolderId, + selectedTagId: state.selectedTagId, + notesParentType: state.notesParentType, }; } )(SideMenuContentComponent) diff --git a/ReactNativeClient/lib/models/tag.js b/ReactNativeClient/lib/models/tag.js index 81c10ad56..eda684367 100644 --- a/ReactNativeClient/lib/models/tag.js +++ b/ReactNativeClient/lib/models/tag.js @@ -21,7 +21,7 @@ class Tag extends BaseItem { return super.serialize(item, 'tag', fieldNames); } - static async tagNoteIds(tagId) { + static async noteIds(tagId) { let rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]); let output = []; for (let i = 0; i < rows.length; i++) { @@ -31,7 +31,7 @@ class Tag extends BaseItem { } static async notes(tagId) { - let noteIds = await this.tagNoteIds(tagId); + let noteIds = await this.noteIds(tagId); if (!noteIds.length) return []; return Note.search({ diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 1e7812042..604d05110 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -38,10 +38,13 @@ import { PoorManIntervals } from 'lib/poor-man-intervals.js'; let defaultState = { notes: [], notesSource: '', + notesParentType: null, folders: [], + tags: [], selectedNoteId: null, - selectedItemType: 'note', selectedFolderId: null, + selectedTagId: null, + selectedItemType: 'note', showSideMenu: false, screens: {}, loading: true, @@ -148,6 +151,12 @@ const reducer = (state = defaultState, action) => { if ('folderId' in action) { newState.selectedFolderId = action.folderId; + newState.notesParentType = 'Folder'; + } + + if ('tagId' in action) { + newState.selectedTagId = action.tagId; + newState.notesParentType = 'Tag'; } if ('itemType' in action) { @@ -229,6 +238,12 @@ const reducer = (state = defaultState, action) => { newState.folders = action.folders; break; + case 'TAGS_UPDATE_ALL': + + newState = Object.assign({}, state); + newState.tags = action.tags; + break; + case 'FOLDERS_UPDATE_ONE': var newFolders = state.folders.splice(0); @@ -414,6 +429,13 @@ async function initialize(dispatch, backButtonHandler) { await FoldersScreenUtils.refreshFolders(); + const tags = await Tag.all(); + + dispatch({ + type: 'TAGS_UPDATE_ALL', + tags: tags, + }); + dispatch({ type: 'APPLICATION_LOADING_DONE', });