diff --git a/.vscode/launch.json b/.vscode/launch.json index d16cd3eb..1d247bfe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,18 +3,18 @@ // Ref: https://github.com/Microsoft/vscode-chrome-debug "version": "0.2.0", "configurations": [ - { - "name": "Launch Chrome", - "type": "chrome", - "request": "launch", - "url": "http://localhost:4200/#", - "webRoot": "${workspaceRoot}" - }, + // { + // "name": "Launch Chrome", + // "type": "chrome", + // "request": "launch", + // "url": "http://localhost:4200/admin/#", + // "webRoot": "${workspaceRoot}" + // }, { "name": "Attach Chrome", "type": "chrome", "request": "attach", - "url": "http://localhost:4200", + "url": "http://localhost:4200/admin", "webRoot": "${workspaceRoot}", "port": 9222 // }, diff --git a/src/app/applications/application-add-edit/application-add-edit.component.html b/src/app/applications/application-add-edit/application-add-edit.component.html index abc06c53..d1858f7b 100644 --- a/src/app/applications/application-add-edit/application-add-edit.component.html +++ b/src/app/applications/application-add-edit/application-add-edit.component.html @@ -58,7 +58,11 @@

Disposition

- + @@ -350,7 +354,7 @@

Interest ID: {{shape.properties.INTRID_SID}}

diff --git a/src/app/applications/application-add-edit/application-add-edit.component.ts b/src/app/applications/application-add-edit/application-add-edit.component.ts index 5cb9f755..931fb840 100644 --- a/src/app/applications/application-add-edit/application-add-edit.component.ts +++ b/src/app/applications/application-add-edit/application-add-edit.component.ts @@ -60,6 +60,7 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { // check for unsaved changes before closing (or refreshing) current tab/window @HostListener('window:beforeunload', ['$event']) handleBeforeUnload(event) { + // display browser alert if needed if (!this.allowDeactivate && this.applicationForm.dirty) { event.returnValue = true; } @@ -79,8 +80,8 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { message: 'Click OK to discard your changes or Cancel to return to the application.' }, { backdropColor: 'rgba(0, 0, 0, 0.5)' - } - ); + }) + .takeUntil(this.ngUnsubscribe); } ngOnInit() { @@ -95,7 +96,9 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { .subscribe( (data: { application: Application }) => { if (data.application) { - this.application = data.application; + // make a (deep) copy of the in-memory application so we don't change it + // this allows us to abort editing + this.application = _.cloneDeep(data.application); if (!this.application.publishDate) { this.application.publishDate = new Date(); @@ -225,7 +228,50 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { ); } - saveApplication() { + // create new application + public createApplication() { + if (this.applicationForm.invalid) { + this.dialogService.addDialog(ConfirmComponent, + { + title: 'Cannot Create Application', + message: 'Please check for required fields or errors.', + okOnly: true + }, { + backdropColor: 'rgba(0, 0, 0, 0.5)' + }) + .takeUntil(this.ngUnsubscribe); + } else if (!this.isDispositionValid()) { + this.dialogService.addDialog(ConfirmComponent, + { + title: 'Cannot Create Application', + message: 'Please check that disposition data (basic information) has been successfully loaded.', + okOnly: true + }, { + backdropColor: 'rgba(0, 0, 0, 0.5)' + }) + .takeUntil(this.ngUnsubscribe); + } else { + // adjust for current tz + this.application.publishDate = moment(this.application.publishDate).format(); + + this.applicationService.add(this.application) + .takeUntil(this.ngUnsubscribe) + .subscribe( + application => { + this.showMessage(false, 'Application created!'); + // reload cached data + this.reloadData(application._id); + }, + error => { + console.log('error =', error); + this.showMessage(true, 'Error creating application'); + } + ); + } + } + + // save current application + public saveApplication() { if (this.applicationForm.invalid) { this.dialogService.addDialog(ConfirmComponent, { @@ -247,11 +293,22 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { }) .takeUntil(this.ngUnsubscribe); } else { - if (this.application._id === '0') { - this.internalAddApplication(); - } else { - this.internalSaveApplication(); - } + // adjust for current tz + this.application.publishDate = moment(this.application.publishDate).format(); + + this.applicationService.save(this.application) + .takeUntil(this.ngUnsubscribe) + .subscribe( + application => { + this.showMessage(false, 'Application saved!'); + // reload cached data + this.reloadData(application._id); + }, + error => { + console.log('error =', error); + this.showMessage(true, 'Error saving application'); + } + ); } } @@ -260,54 +317,34 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { && this.application.features[0].properties.DISPOSITION_TRANSACTION_SID === this.application.tantalisID); } - private internalAddApplication() { - // adjust for current tz - this.application.publishDate = moment(this.application.publishDate).format(); - - // create new application - // then reload the page - this.applicationService.addApplication(this.application) - .takeUntil(this.ngUnsubscribe) - .subscribe( - application => { - this.allowDeactivate = true; - this.router.navigate(['/a', application._id, 'edit']); - }, - error => { - console.log('error =', error); - this.showMessage(true, 'Error adding application'); - } - ); - } - - private internalSaveApplication() { - // adjust for current tz - this.application.publishDate = moment(this.application.publishDate).format(); - - // save current application - this.applicationService.save(this.application) - .takeUntil(this.ngUnsubscribe) - .subscribe( - () => { - this.showMessage(false, 'Application saved!'); - // reload cached app data - this.applicationService.getById(this.application._id, true) - .takeUntil(this.ngUnsubscribe) - .subscribe(() => this.applicationForm.form.markAsPristine()); - }, - error => { - console.log('error =', error); - this.showMessage(true, 'Error saving application'); - } - ); + public resetApplication() { + if (this.applicationForm.pristine) { + this.reloadData(this.application._id); + } else { + this.dialogService.addDialog(ConfirmComponent, + { + title: 'Confirm Reset', + message: 'Click OK to discard your changes or Cancel to return to the application.' + }, { + backdropColor: 'rgba(0, 0, 0, 0.5)' + }) + .takeUntil(this.ngUnsubscribe) + .subscribe(isConfirmed => { + if (isConfirmed) { + this.reloadData(this.application._id); + } + }); + } } - resetApplication() { - // reload cached app data - this.applicationService.getById(this.application._id, true) + private reloadData(id: string) { + // force-reload app data + this.applicationService.getById(id, true) .takeUntil(this.ngUnsubscribe) .subscribe(application => { - this.application = application; + // make a (deep) copy of the in-memory application so we don't change it + // this allows us to abort editing + this.application = _.cloneDeep(application); this.applicationForm.form.markAsPristine(); }); } @@ -539,6 +576,6 @@ export class ApplicationAddEditComponent implements OnInit, OnDestroy { this.error = isError; this.showMsg = true; this.status = msg; - setTimeout(() => this.showMsg = false, 3000); + setTimeout(() => this.showMsg = false, 2000); } } diff --git a/src/app/applications/application-aside/application-aside.component.ts b/src/app/applications/application-aside/application-aside.component.ts index 412a6baf..120c5090 100644 --- a/src/app/applications/application-aside/application-aside.component.ts +++ b/src/app/applications/application-aside/application-aside.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnChanges, OnDestroy, Input, SimpleChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs/Subject'; import 'rxjs/add/operator/takeUntil'; @@ -19,10 +19,10 @@ import { SearchService } from '../../services/search.service'; styleUrls: ['./application-aside.component.scss'] }) -export class ApplicationAsideComponent implements OnInit, OnDestroy { - public application: Application = null; - public daysRemaining = '?'; - public numComments = '?'; +export class ApplicationAsideComponent implements OnChanges, OnDestroy { + @Input() application: Application = null; + public daysRemaining = '-'; + public numComments = '-'; private ngUnsubscribe: Subject = new Subject(); public fg: L.FeatureGroup; public map: L.Map; @@ -40,149 +40,141 @@ export class ApplicationAsideComponent implements OnInit, OnDestroy { private commentService: CommentService ) { } - ngOnInit() { - // if we're not logged in, redirect - if (!this.api.ensureLoggedIn()) { - return false; - } - - // get data from route resolver - this.route.data - .takeUntil(this.ngUnsubscribe) - .subscribe( - (data: { application: Application }) => { - if (data.application) { - this.application = data.application; - - if (this.application._id !== '0') { - // get comment period days remaining - if (this.application.currentPeriod) { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - // use moment to handle Daylight Saving Time changes - const days = moment(this.application.currentPeriod.endDate).diff(moment(today), 'days') + 1; - this.daysRemaining = days + (days === 1 ? ' Day ' : ' Days ') + 'Remaining'; - } - - // get number of pending comments - this.commentService.getAllByApplicationId(this.application._id) - .takeUntil(this.ngUnsubscribe) - .subscribe( - (comments: Comment[]) => { - const pending = comments.filter(comment => this.commentService.isPending(comment)); - const count = pending.length; - this.numComments = count.toString(); - }, - error => console.log('couldn\'t get pending comments, error =', error) - ); - - if (this.application.tantalisID) { - const self = this; - this.searchService.getByDTID(this.application.tantalisID).subscribe( - features => { - const World_Topo_Map = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' - }); - const World_Imagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' - }); - const OpenMapSurfer_Roads = L.tileLayer('https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}', { - maxZoom: 20, - attribution: 'Imagery from GIScience Research Group @ University of Heidelberg — Map data © OpenStreetMap' - }); - const Esri_OceanBasemap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri', - maxZoom: 13 - }); - const Esri_NatGeoWorldMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC', - maxZoom: 16 - }); - self.map = L.map('map', { - layers: [World_Imagery] - }); - - // Setup the controls - self.baseMaps = { - 'Ocean Base': Esri_OceanBasemap, - 'Nat Geo World Map': Esri_NatGeoWorldMap, - 'Open Surfer Roads': OpenMapSurfer_Roads, - 'World Topographic': World_Topo_Map, - 'World Imagery': World_Imagery - }; - self.control = L.control.layers(self.baseMaps, null, { collapsed: true }).addTo(self.map); - - if (self.fg) { - _.each(self.layers, function (layer) { - self.map.removeLayer(layer); - }); - self.fg.clearLayers(); - } else { - self.fg = L.featureGroup(); - } - - _.each(features, function (feature) { - const f = JSON.parse(JSON.stringify(feature)); - // Needed to be valid GeoJSON - delete f.geometry_name; - const featureObj: GeoJSON.Feature = f; - const layer = L.geoJSON(featureObj); - const options = { maxWidth: 400 }; - self.fg.addLayer(layer); - layer.addTo(self.map); - }); - - const bounds = self.fg.getBounds(); - if (!_.isEmpty(bounds)) { - self.map.fitBounds(bounds, self.maxZoom); - } - }, - error => { - console.log('error =', error); - } - ); - } + ngOnChanges(changes: SimpleChanges) { + // console.log('changes.application =', changes.application); + + // guard against null application + if (changes.application.currentValue) { + // get comment period days remaining + if (this.application._id !== '0' && this.application.currentPeriod) { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + // use moment to handle Daylight Saving Time changes + const days = moment(this.application.currentPeriod.endDate).diff(moment(today), 'days') + 1; + this.daysRemaining = days + (days === 1 ? ' Day ' : ' Days ') + 'Remaining'; + } + + // get number of pending comments + if (this.application._id !== '0') { + this.commentService.getAllByApplicationId(this.application._id) + .takeUntil(this.ngUnsubscribe) + .subscribe( + (comments: Comment[]) => { + const pending = comments.filter(comment => this.commentService.isPending(comment)); + this.numComments = pending.length.toString(); + }, + error => console.log('couldn\'t get pending comments, error =', error) + ); + } + + if (this.application._id !== '0' && this.application.tantalisID) { + const self = this; + this.searchService.getByDTID(this.application.tantalisID) + .takeUntil(this.ngUnsubscribe) + .subscribe( + features => { + const World_Topo_Map = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' + }); + const World_Imagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' + }); + const OpenMapSurfer_Roads = L.tileLayer('https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}', { + maxZoom: 20, + attribution: 'Imagery from GIScience Research Group @ University of Heidelberg — Map data © OpenStreetMap' + }); + const Esri_OceanBasemap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri', + maxZoom: 13 + }); + const Esri_NatGeoWorldMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC', + maxZoom: 16 + }); + + try { + self.map = L.map('map', { + layers: [World_Imagery] + }); + + // set up the controls + self.baseMaps = { + 'Ocean Base': Esri_OceanBasemap, + 'Nat Geo World Map': Esri_NatGeoWorldMap, + 'Open Surfer Roads': OpenMapSurfer_Roads, + 'World Topographic': World_Topo_Map, + 'World Imagery': World_Imagery + }; + self.control = L.control.layers(self.baseMaps, null, { collapsed: true }).addTo(self.map); + + if (self.fg) { + _.each(self.layers, function (layer) { + self.map.removeLayer(layer); + }); + self.fg.clearLayers(); + } else { + self.fg = L.featureGroup(); + } + + _.each(features, function (feature) { + const f = JSON.parse(JSON.stringify(feature)); + // Needed to be valid GeoJSON + delete f.geometry_name; + const featureObj: GeoJSON.Feature = f; + const layer = L.geoJSON(featureObj); + const options = { maxWidth: 400 }; + self.fg.addLayer(layer); + layer.addTo(self.map); + }); + + const bounds = self.fg.getBounds(); + if (!_.isEmpty(bounds)) { + self.map.fitBounds(bounds, self.maxZoom); + } + } catch (e) { } + }, + error => { + console.log('error =', error); } - } else { - // application not found --> navigate back to application list - alert('Uh-oh, couldn\'t load application'); - this.router.navigate(['/applications']); - } - } - ); + ); + } + } } - drawMap(app: Application) { + // called from parent component + public drawMap(app: Application) { if (app._id !== '0' && app.tantalisID) { const self = this; - this.searchService.getByDTID(app.tantalisID).subscribe( - features => { - if (self.fg) { - _.each(self.layers, function (layer) { - self.map.removeLayer(layer); + this.searchService.getByDTID(app.tantalisID) + .takeUntil(this.ngUnsubscribe) + .subscribe( + features => { + if (self.fg) { + _.each(self.layers, function (layer) { + self.map.removeLayer(layer); + }); + self.fg.clearLayers(); + } + _.each(features, function (feature) { + const f = JSON.parse(JSON.stringify(feature)); + // Needed to be valid GeoJSON + delete f.geometry_name; + const featureObj: GeoJSON.Feature = f; + const layer = L.geoJSON(featureObj); + const options = { maxWidth: 400 }; + self.fg.addLayer(layer); + layer.addTo(self.map); }); - self.fg.clearLayers(); - } - _.each(features, function (feature) { - const f = JSON.parse(JSON.stringify(feature)); - // Needed to be valid GeoJSON - delete f.geometry_name; - const featureObj: GeoJSON.Feature = f; - const layer = L.geoJSON(featureObj); - const options = { maxWidth: 400 }; - self.fg.addLayer(layer); - layer.addTo(self.map); - }); - - const bounds = self.fg.getBounds(); - if (!_.isEmpty(bounds)) { - self.map.fitBounds(bounds, self.maxZoom); + + const bounds = self.fg.getBounds(); + if (!_.isEmpty(bounds)) { + self.map.fitBounds(bounds, self.maxZoom); + } + }, + error => { + console.log('error =', error); } - }, - error => { - console.log('error =', error); - } - ); + ); } } diff --git a/src/app/applications/application-detail/application-detail.component.html b/src/app/applications/application-detail/application-detail.component.html index 9278a41d..0085ba3d 100644 --- a/src/app/applications/application-detail/application-detail.component.html +++ b/src/app/applications/application-detail/application-detail.component.html @@ -187,7 +187,7 @@

Interest ID: {{shape.properties.INTRID_SID}}

diff --git a/src/app/applications/application-list/application-list.component.html b/src/app/applications/application-list/application-list.component.html index aeae62e2..2a551cb6 100644 --- a/src/app/applications/application-list/application-list.component.html +++ b/src/app/applications/application-list/application-list.component.html @@ -24,18 +24,19 @@

Crown Lands Applications in British Columbia

CL File - + Purpose / Subpurpose - + Region - Status - + Status + + + Cmnt'ing + @@ -43,7 +44,7 @@

Crown Lands Applications in British Columbia

- +