diff --git a/package.json b/package.json index 5e1de06..51ca0d1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "@angular/platform-browser-dynamic": "2.2.1", "@angular/platform-server": "2.2.1", "@ionic/storage": "1.1.8", + "apollo-angular": "^0.11.0", + "apollo-client": "^0.8.7", + "apollo-client-rxjs": "^0.5.1", + "graphql-tag": "^1.2.4", "ionic-angular": "2.0.1", "ionic-native": "2.5.1", "ionicons": "3.0.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5d514e4..a7af541 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,10 +1,24 @@ +import { AppConfig } from './app.config'; +import { ApolloClient, createNetworkInterface } from 'apollo-client'; +import { ApolloModule } from 'apollo-angular'; import { BookingPage } from '../pages/booking/booking'; -import { BookingService } from '../services/booking.service'; import { NgModule, ErrorHandler } from '@angular/core'; import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; import { MyApp } from './app.component'; import { HomePage } from '../pages/home/home'; + +// by default, this client will send queries to `/graphql` (relative to the URL of your app) +const client = new ApolloClient({ + networkInterface: createNetworkInterface({ + uri: AppConfig.apiEndpoint + }), +}); + +export function provideClient(): ApolloClient { + return client; +} + @NgModule({ declarations: [ MyApp, @@ -12,7 +26,8 @@ import { HomePage } from '../pages/home/home'; BookingPage ], imports: [ - IonicModule.forRoot(MyApp) + IonicModule.forRoot(MyApp), + ApolloModule.forRoot(provideClient) ], bootstrap: [IonicApp], entryComponents: [ @@ -23,9 +38,8 @@ import { HomePage } from '../pages/home/home'; providers: [ { provide: ErrorHandler, - useClass: IonicErrorHandler + useClass: IonicErrorHandler, }, - BookingService ] }) export class AppModule { } diff --git a/src/pages/home/home.html b/src/pages/home/home.html index 2b0c768..6697fd3 100644 --- a/src/pages/home/home.html +++ b/src/pages/home/home.html @@ -25,11 +25,11 @@ - + refreshingText="Update in progress"> diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts index 904f285..983abb4 100644 --- a/src/pages/home/home.ts +++ b/src/pages/home/home.ts @@ -1,10 +1,32 @@ +import { Subject, Subscription } from 'rxjs/Rx'; +import { Apollo, ApolloQueryObservable } from 'apollo-angular'; import { Room } from '../../models/room'; -import { BookingService } from '../../services/booking.service'; import { Component, ViewChild } from '@angular/core'; import { Content, NavController, AlertController } from 'ionic-angular'; import { BookingPage } from '../booking/booking'; import { Vibration, BarcodeScanner } from 'ionic-native'; +import gql from 'graphql-tag'; + +const floorMasterRoomNumberQuery = gql` + query AvailableRoomsQuery($roomNumber: Int!) + { roomsOnFloor: rooms(floorMasterRoomNumber: $roomNumber) { + name + number + capacity + availability { + busy + availableFor + availableFrom + } + } + }` + ; + +interface QueryResponse { + roomsOnFloor: Room[]; +} + @Component({ selector: 'page-home', templateUrl: 'home.html' @@ -13,43 +35,57 @@ import { Vibration, BarcodeScanner } from 'ionic-native'; export class HomePage { @ViewChild(Content) content: Content; + roomsObs: ApolloQueryObservable; + rooms: Room[] = []; + filteredRooms: Room[] = []; + availabilityFilter: string; + floorMasterRoomNumber: Subject = new Subject(); + updateInProgress: boolean; + roomSelected: number; + roomsSubscription: Subscription; - private rooms: Room[]; - public filteredRooms: Room[]; - private availabilityFilter: string; - private roomSelected: number; - private updateInProgress: boolean; + constructor( + public navCtrl: NavController, + private apollo: Apollo, + private alertCtrl: AlertController + ){} - constructor(public navCtrl: NavController, private bookingService: BookingService, private alertCtrl: AlertController) { - this.roomSelected = 412; // "magic" room number - this.updateRooms(); - } - - updateRooms() { + //Ionic's alternative to ngOnInit() + ionViewWillEnter() { + this.roomSelected = 412; //Magic room number - to be substituted with config this.updateInProgress = true; - this.filteredRooms = null; //toggles "loading" spinner on UI - this.bookingService.getMeetingRooms(this.roomSelected).subscribe( - result => this.processData(result), - err => { - this.showError(err); - } + + this.roomsObs = this.apollo.watchQuery({ + query: floorMasterRoomNumberQuery, + variables: {roomNumber: this.roomSelected} + }); + + this.roomsSubscription = this.roomsObs.subscribe( + res => { + this.processData(res) + this.updateInProgress = false; + }, + err => this.rooms = [] ); } + + //Ionic's alternative to ngOnInit() + ionViewDidLeave() { + this.roomsSubscription.unsubscribe(); + } - processData(result: Room[]) { - this.rooms = result; + //Update rooms list, apply default filter, drop "sync in progress" + processData(res, refresher?) { + this.rooms = res.data.roomsOnFloor || []; this.availabilityFilter = 'available'; + this.updateInProgress = false; this.showAvailableRooms(); - //Vibration added to check integration with native functionality - try { - Vibration.vibrate(1000) + if (refresher !== undefined) { + refresher.complete(); } - catch(e) { - console.log('Vibration failed: '+ (e).message); - }; - this.updateInProgress = false; } - + + //Alert modal window showError(err) { let alert = this.alertCtrl.create({ title: 'Error', @@ -70,13 +106,23 @@ export class HomePage { this.content.scrollToTop(); } + + //Refresh spinner doRefresh(refresher) { - this.updateRooms(); - refresher.complete(); + this.rooms = []; + this.filteredRooms = []; + this.roomsObs.refetch() + .then(res => { + this.processData(res, refresher); + }) + .catch(err => { + this.showError(err); + refresher.complete() + }); } - + openBookingPage(room: Room) { - this.navCtrl.push(BookingPage, {room}); + this.navCtrl.push(BookingPage, { room }); } // Barcode scanning using native cordova plugin @@ -88,12 +134,26 @@ export class HomePage { let roomNumber = Number(url.substr(28, 3)); if (roomNumber > 0) { this.roomSelected = roomNumber; - this.updateRooms(); + this.roomsObs.refetch({roomNumber: roomNumber}); } else { this.showError('Could not identify room number'); } } - }, - err => this.showError('Unknown error') - )} + }, + err => { + this.showError('Unknown error'); + } + ) + } + + //Vibration added to check integration with native functionality + vibrate() { + try { + Vibration.vibrate(1000) + } + catch (e) { + console.log('Vibration failed: ' + (e).message); + }; + } + } \ No newline at end of file diff --git a/src/services/booking.service.ts b/src/services/booking.service.ts deleted file mode 100644 index 665c34a..0000000 --- a/src/services/booking.service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { Http, URLSearchParams, Response } from '@angular/http'; -import { AppConfig } from '../app/app.config'; -import { Room } from '../models/room'; - -import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/observable/throw'; - -//Implemented for POC purpose. Selected room will not be displayed in results. To be changed to something reasonable. -const floorMasterRoomNumberQuery = 'query AvailableRoomsQuery($roomNumber: Int!) { roomsOnFloor: rooms(floorMasterRoomNumber: $roomNumber) { name number capacity availability { busy availableFor availableFrom __typename } __typename }}'; - -@Injectable() -export class BookingService { - - constructor(private http: Http) { - } - - getMeetingRooms(roomSelected: number): Observable { - let params: URLSearchParams = new URLSearchParams(); - let variables = {"roomNumber": roomSelected}; - params.set('query', floorMasterRoomNumberQuery); - params.set('variables', JSON.stringify(variables)); - return this.http.get(AppConfig.apiEndpoint, { search: params }) - .map(this.extractData) - .catch(this.handleError);; - } - - private extractData(res: Response) { - let body = res.json(); - return body.data.roomsOnFloor || {}; - } - - private handleError(error: Response | any) { - // In a real world app, we might use a remote logging infrastructure - let errMsg: string; - if (error instanceof Response) { - const body = error.json() || ''; - const err = body.error || JSON.stringify(body); - errMsg = `${error.status} - ${error.statusText || ''} ${err}`; - } else { - errMsg = error.message ? error.message : error.toString(); - } - console.error(errMsg); - return Observable.throw('connection error - '+ errMsg); - } - -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8d7f092..c4d583f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,6 +97,18 @@ localforage "~1.4.2" localforage-cordovasqlitedriver "~1.5.0" +"@types/async@^2.0.31": + version "2.0.38" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.38.tgz#5c369dcb14788da0621daafa8594a053b0edcb21" + +"@types/graphql@^0.8.0": + version "0.8.6" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68" + +"@types/isomorphic-fetch@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.30.tgz#a21717624cde9a48c2db53a4e500fc5c32a99bbc" + "@types/localforage@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/localforage/-/localforage-0.0.30.tgz#3d60a6bf6dda38e3f8a469611598379f1f649509" @@ -164,6 +176,30 @@ anymatch@^1.3.0: arrify "^1.0.0" micromatch "^2.1.5" +apollo-angular@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/apollo-angular/-/apollo-angular-0.11.0.tgz#57da1f5bc3472c18d8c061ba544c2d1e0ac0f405" + dependencies: + apollo-client-rxjs "~0.5.0" + +apollo-client-rxjs@^0.5.1, apollo-client-rxjs@~0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/apollo-client-rxjs/-/apollo-client-rxjs-0.5.1.tgz#52cd0e836f28178d5f2cbdee9917b3395afd063f" + +apollo-client@^0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-0.8.7.tgz#73f52d1dd6dfe0f07e8bfe05a8e47c4bcd3bf7a3" + dependencies: + graphql-anywhere "^2.1.0" + graphql-tag "^1.1.1" + redux "^3.4.0" + symbol-observable "^1.0.2" + whatwg-fetch "^2.0.0" + optionalDependencies: + "@types/async" "^2.0.31" + "@types/graphql" "^0.8.0" + "@types/isomorphic-fetch" "0.0.30" + aproba@^1.0.3: version "1.1.1" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" @@ -1456,6 +1492,14 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +graphql-anywhere@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-2.2.0.tgz#652c3fa23a4a6cfeb98817512fb48100b97f3d5c" + +graphql-tag@^1.1.1, graphql-tag@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.2.4.tgz#90c59bea41378513fd7213dc92537fcd20e4570f" + har-validator@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" @@ -1861,6 +1905,10 @@ localforage@>=1.4.0, localforage@~1.4.2: dependencies: lie "3.0.2" +lodash-es@^4.2.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" @@ -1881,7 +1929,7 @@ lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" -lodash@^4.0.0, lodash@^4.14.0, lodash@^4.2.0: +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -1893,7 +1941,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -2536,6 +2584,15 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redux@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.2" + reflect-metadata@^0.1.2: version "0.1.9" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.9.tgz#987238dc87a516895fe457f130435ffbd763a4d4" @@ -2945,7 +3002,7 @@ sw-toolbox@3.4.0: path-to-regexp "^1.0.1" serviceworker-cache-polyfill "^4.0.0" -symbol-observable@^1.0.1: +symbol-observable@^1.0.1, symbol-observable@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -3236,6 +3293,10 @@ websocket-extensions@>=0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" +whatwg-fetch@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"