diff --git a/addons/api/addon/services/scope.js b/addons/api/addon/services/scope.js index e9088e40e..44a34e874 100644 --- a/addons/api/addon/services/scope.js +++ b/addons/api/addon/services/scope.js @@ -1,6 +1,7 @@ import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; /** * This module includes two parts: @@ -55,19 +56,25 @@ class Scope { } /** - * Returns the JSON serialization of this scope. + * Returns the JSON serialization of this scope. This is necessary because + * scopes may be persisted in storage with their JSON representation. * @return {?object} */ toJSON() { const { org, project } = this; - return JSON.stringify({ org, project }); + return { org, project }; } } export { Scope }; /** - * The scope service manages and persists the current scope. Validating against + * The scope service manages and persists the current scope. To interact with + * this service, get and set `service.org` or `service.project` directly, rather + * than using the tracked `scope` member, which is internal. + * + * A scope is valid only if the user has access to the organization and project + * specified in the scope, and they exist. However, validating against * available organizations and projects is outside the scope (tee hee) of this * service because it is used by the adapter mixin to generate API paths, which * could lead to problems were this service to attempt to fetch data. @@ -78,14 +85,20 @@ export { Scope }; */ export default class ScopeService extends Service { /** - * The currently selected scope. + * The storage service is used to persist the current scope. + * @type {StorageService} + */ + @service storage; + + /** + * The currently selected scope. This is an internal member only. * @type {?Scope} */ - @tracked scope = null; + @tracked scope = new Scope(); /** * When setting org, `service.org = {...}`, a new scope is instantiated, - * since scopes are immutable. + * since scopes are immutable. Also persists scope on set. * @type {?object} */ @computed('scope.org') @@ -94,11 +107,12 @@ export default class ScopeService extends Service { } set org(org) { this.scope = new Scope(org, this.project); + this.saveScope(this.scope); } /** * When setting project, `service.project = {...}`, a new scope is - * instantiated, since scopes are immutable. + * instantiated, since scopes are immutable. Also persists scope on set. * @type {?object} */ @computed('scope.project') @@ -107,5 +121,31 @@ export default class ScopeService extends Service { } set project(project) { this.scope = new Scope(this.org, project); + this.saveScope(this.scope); + } + + /** + * Saves the current scope into storage. + */ + saveScope(scope) { + this.storage.setItem('scope', scope); + } + + /** + * Retrieves a saved scope from storage, if any. + * @return {?Scope} + */ + fetchScope() { + const json = this.storage.getItem('scope'); + if (json) return new Scope(json.org, json.project); + } + + /** + * Initializes the service's current scope from storage, if any. + */ + init() { + const savedScope = this.fetchScope(); + if (savedScope) this.scope = savedScope; + super.init(...arguments); } } diff --git a/addons/api/addon/services/storage.js b/addons/api/addon/services/storage.js index 2be7034d2..18928bc19 100644 --- a/addons/api/addon/services/storage.js +++ b/addons/api/addon/services/storage.js @@ -108,7 +108,8 @@ export default class StorageService extends Service { * @return {any} */ getItem(key) { - return this.storage.getItem(this.qualifiedKey(key)); + const value = this.storage.getItem(this.qualifiedKey(key)); + if (value) return JSON.parse(value); } /** @@ -116,7 +117,7 @@ export default class StorageService extends Service { * @param {string} key */ setItem(key, value) { - this.storage.setItem(this.qualifiedKey(key), value); + this.storage.setItem(this.qualifiedKey(key), JSON.stringify(value)); } /** diff --git a/addons/api/tests/dummy/app/routes/application.js b/addons/api/tests/dummy/app/routes/application.js index a9e919f63..62946d66d 100644 --- a/addons/api/tests/dummy/app/routes/application.js +++ b/addons/api/tests/dummy/app/routes/application.js @@ -3,9 +3,8 @@ import { inject as service } from '@ember/service'; export default class ApplicationRoute extends Route { @service scope; - @service storage; afterModel() { - //console.log(this.storage); + //console.log(this.scope); } } diff --git a/addons/api/tests/unit/services/scope-test.js b/addons/api/tests/unit/services/scope-test.js index a1844e3e4..c97a59f4a 100644 --- a/addons/api/tests/unit/services/scope-test.js +++ b/addons/api/tests/unit/services/scope-test.js @@ -31,12 +31,12 @@ module('Unit | Service | scope class', function (hooks) { test('it serializes to JSON', function (assert) { assert.expect(2); let scope = new Scope({ id: 1 }); - assert.deepEqual(JSON.parse(scope.toJSON()), { + assert.deepEqual(scope.toJSON(), { org: { id: 1 }, project: null, }); scope = new Scope({ id: 1 }, { id: 2 }); - assert.deepEqual(JSON.parse(scope.toJSON()), { + assert.deepEqual(scope.toJSON(), { org: { id: 1 }, project: { id: 2 }, }); @@ -47,9 +47,8 @@ module('Unit | Service | scope', function (hooks) { setupTest(hooks); test('it returns nulls when scope is not set', function (assert) { - assert.expect(3); + assert.expect(2); const service = this.owner.lookup('service:scope'); - assert.equal(service.scope, null); assert.equal(service.org, null); assert.equal(service.project, null); }); @@ -64,4 +63,34 @@ module('Unit | Service | scope', function (hooks) { assert.equal(service.org, service.scope.org); assert.equal(service.project, service.scope.project); }); + + test('it persists changes to scope', function (assert) { + assert.expect(2); + const service = this.owner.lookup('service:scope'); + const org = { id: 1 }; + const project = { id: 2 }; + service.org = org; + service.project = project; + assert.equal(service.fetchScope().org.id, 1); + assert.equal(service.fetchScope().project.id, 2); + }); + + test('it initializes to the persisted scope, if any', function (assert) { + assert.expect(4); + const service = this.owner.lookup('service:scope'); + const org = { id: 1 }; + const project = { id: 2 }; + service.org = org; + service.project = project; + // first, persist a scope directly, without setting it on the service + const sneakyScope = new Scope({ id: 3 }, { id: 4 }); + service.saveScope(sneakyScope); + // establish that the service scope wasn't changed... + assert.equal(service.scope.org.id, 1); + assert.equal(service.scope.project.id, 2); + // finally, call init and check that the scope changed + service.init(); + assert.equal(service.scope.org.id, 3); + assert.equal(service.scope.project.id, 4); + }); });