Side menu and sync fixes

pull/41/head
Laurent Cozic 2017-05-24 19:09:46 +00:00
parent c01d850b2d
commit 3078797134
10 changed files with 169 additions and 20 deletions

View File

@ -12,6 +12,7 @@
"react-native-action-button": "^2.6.9", "react-native-action-button": "^2.6.9",
"react-native-checkbox": "^1.1.0", "react-native-checkbox": "^1.1.0",
"react-native-popup-menu": "^0.7.4", "react-native-popup-menu": "^0.7.4",
"react-native-side-menu": "^0.20.1",
"react-native-vector-icons": "^2.0.3", "react-native-vector-icons": "^2.0.3",
"react-navigation": "^1.0.0-beta.9", "react-navigation": "^1.0.0-beta.9",
"uuid": "^3.0.1" "uuid": "^3.0.1"

View File

@ -64,7 +64,6 @@ const FolderScreen = connect(
(state) => { (state) => {
return { return {
folderId: state.selectedFolderId, folderId: state.selectedFolderId,
//folder: state.selectedFolderId ? Folder.byId(state.folders, state.selectedFolderId) : Folder.newFolder(),
}; };
} }
)(FolderScreenComponent) )(FolderScreenComponent)

View File

@ -0,0 +1,101 @@
const React = require('react');
const {
Dimensions,
StyleSheet,
ScrollView,
View,
Image,
Text,
} = require('react-native');
const { Component } = React;
const window = Dimensions.get('window');
const uri = 'https://pickaface.net/gallery/avatar/Opi51c74d0125fd4.png';
const styles = StyleSheet.create({
menu: {
flex: 1,
width: window.width,
height: window.height,
backgroundColor: 'gray',
padding: 20,
},
avatarContainer: {
marginBottom: 20,
marginTop: 20,
},
avatar: {
width: 48,
height: 48,
borderRadius: 24,
flex: 1,
},
name: {
position: 'absolute',
left: 70,
top: 20,
},
item: {
fontSize: 14,
fontWeight: '300',
paddingTop: 5,
},
});
module.exports = class Menu extends Component {
static propTypes = {
onItemSelected: React.PropTypes.func.isRequired,
};
render() {
return (
<ScrollView scrollsToTop={false} style={styles.menu}>
<View style={styles.avatarContainer}>
<Image
style={styles.avatar}
source={{ uri, }}/>
<Text style={styles.name}>Your name</Text>
</View>
<Text
onPress={() => this.props.onItemSelected('About')}
style={styles.item}>
About
</Text>
<Text
onPress={() => this.props.onItemSelected('Contacts')}
style={styles.item}>
Contacts
</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>Contacts</Text>
<Text style={styles.item}>ContactsLL</Text>
</ScrollView>
);
}
};

View File

@ -159,6 +159,11 @@ const AppNavigator = StackNavigator({
Login: {screen: LoginScreen}, Login: {screen: LoginScreen},
}); });
const SideMenu = require('react-native-side-menu');
import Menu from 'src/menu.js';
class AppComponent extends React.Component { class AppComponent extends React.Component {
componentDidMount() { componentDidMount() {
@ -207,13 +212,17 @@ class AppComponent extends React.Component {
} }
render() { render() {
const menu = <Menu/>;
return ( return (
<MenuContext style={{ flex: 1 }}> <SideMenu menu={menu}>
<AppNavigator navigation={addNavigationHelpers({ <MenuContext style={{ flex: 1 }}>
dispatch: this.props.dispatch, <AppNavigator navigation={addNavigationHelpers({
state: this.props.nav, dispatch: this.props.dispatch,
})} /> state: this.props.nav,
</MenuContext> })} />
</MenuContext>
</SideMenu>
); );
} }
} }

View File

@ -131,12 +131,20 @@ class Synchronizer {
return p.then(() => { return p.then(() => {
processedChangeIds = processedChangeIds.concat(c.ids); processedChangeIds = processedChangeIds.concat(c.ids);
}).catch((error) => { }).catch((error) => {
Log.warn('Failed applying changes', c.ids); Log.warn('Failed applying changes', c.ids, error.message, error.type);
// This is fine - trying to apply changes to an object that has been deleted
if (error.type == 'NotFoundException') {
processedChangeIds = processedChangeIds.concat(c.ids);
} else {
throw error;
}
}); });
}); });
} }
return promiseChain(chain).then(() => { return promiseChain(chain).catch((error) => {
Log.warn('Synchronization was interrupted due to an error:', error);
}).then(() => {
Log.info('IDs to delete: ', processedChangeIds); Log.info('IDs to delete: ', processedChangeIds);
Change.deleteMultiple(processedChangeIds); Change.deleteMultiple(processedChangeIds);
}); });

View File

@ -1,6 +1,21 @@
import { Log } from 'src/log.js'; import { Log } from 'src/log.js';
import { stringify } from 'query-string'; import { stringify } from 'query-string';
class WebApiError extends Error {
constructor(msg) {
let type = 'WebApiError';
// Create a regular JS Error object from a web api error response { error: "something", type: "NotFoundException" }
if (typeof msg === 'object' && msg !== null) {
if (msg.type) type = msg.type;
msg = msg.error ? msg.error : 'error';
}
super(msg);
this.type = type;
}
}
class WebApi { class WebApi {
constructor(baseUrl) { constructor(baseUrl) {
@ -73,7 +88,7 @@ class WebApi {
let responseClone = response.clone(); let responseClone = response.clone();
return response.json().then(function(data) { return response.json().then(function(data) {
if (data && data.error) { if (data && data.error) {
reject(new Error(data.error)); reject(new WebApiError(data));
} else { } else {
resolve(data); resolve(data);
} }

View File

@ -7,10 +7,8 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController; use AppBundle\Controller\ApiController;
use AppBundle\Model\Folder; use AppBundle\Model\Folder;
use AppBundle\Exception\NotFoundException;
use AppBundle\Exception\MethodNotAllowedException;
use AppBundle\Model\BaseItem;
class FoldersController extends ApiController { class FoldersController extends ApiController {
@ -30,7 +28,7 @@ class FoldersController extends ApiController {
return static::successResponse($folder); return static::successResponse($folder);
} }
return static::errorResponse('Invalid method'); throw new MethodNotAllowedException();
} }
/** /**
@ -38,7 +36,7 @@ class FoldersController extends ApiController {
*/ */
public function oneAction($id, Request $request) { public function oneAction($id, Request $request) {
$folder = Folder::byId(Folder::unhex($id)); $folder = Folder::byId(Folder::unhex($id));
if (!$folder && !$request->isMethod('PUT')) return static::errorResponse('Not found', 0, 404); if (!$folder && !$request->isMethod('PUT')) throw new NotFoundException();
if ($request->isMethod('GET')) { if ($request->isMethod('GET')) {
return static::successResponse($folder); return static::successResponse($folder);
@ -68,7 +66,7 @@ class FoldersController extends ApiController {
return static::successResponse(array('id' => $id)); return static::successResponse(array('id' => $id));
} }
return static::errorResponse('Invalid method'); throw new MethodNotAllowedException();
} }
/** /**
@ -76,7 +74,7 @@ class FoldersController extends ApiController {
*/ */
public function linkAction($id, Request $request) { public function linkAction($id, Request $request) {
$folder = Folder::byId(Folder::unhex($id)); $folder = Folder::byId(Folder::unhex($id));
if (!$folder) return static::errorResponse('Not found', 0, 404); if (!$folder) throw new NotFoundException();
if ($request->isMethod('GET')) { if ($request->isMethod('GET')) {
return static::successResponse($folder->notes()); return static::successResponse($folder->notes());
@ -91,7 +89,7 @@ class FoldersController extends ApiController {
return static::successResponse(); return static::successResponse();
} }
return static::errorResponse('Invalid method'); throw new MethodNotAllowedException();
} }
} }

View File

@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController; use AppBundle\Controller\ApiController;
use AppBundle\Model\Note; use AppBundle\Model\Note;
use AppBundle\Exception\NotFoundException;
class NotesController extends ApiController { class NotesController extends ApiController {
@ -30,7 +31,7 @@ class NotesController extends ApiController {
*/ */
public function oneAction($id, Request $request) { public function oneAction($id, Request $request) {
$note = Note::find(Note::unhex($id)); $note = Note::find(Note::unhex($id));
if (!$note && !$request->isMethod('PUT')) return static::errorResponse('Not found', 0, 404); if (!$note && !$request->isMethod('PUT')) throw new NotFoundException();
if ($request->isMethod('GET')) { if ($request->isMethod('GET')) {
return static::successResponse($note); return static::successResponse($note);

View File

@ -30,4 +30,18 @@ class Folder extends BaseItem {
return Note::where('parent_id', '=', $this->id)->get(); return Note::where('parent_id', '=', $this->id)->get();
} }
static public function countByOwnerId($ownerId) {
return Folder::where('owner_id', '=', $ownerId)->count();
}
public function delete() {
if (self::countByOwnerId($this->owner_id) <= 1) throw new \Exception('Cannot delete the last folder');
$notes = $this->notes();
foreach ($notes as $note) {
$note->delete();
}
return parent::delete();
}
} }

3
start_emulator.bat Normal file
View File

@ -0,0 +1,3 @@
c:
C:\Users\Laurent\AppData\Local\Android\sdk\tools
emulator.exe -avd Nexus_5X_API_23_Google_API_