diff --git a/addons/api/addon/services/scope.js b/addons/api/addon/services/scope.js new file mode 100644 index 000000000..e9088e40e --- /dev/null +++ b/addons/api/addon/services/scope.js @@ -0,0 +1,111 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { computed } from '@ember/object'; + +/** + * This module includes two parts: + * + * - `Scope` class: a data structure representing an org/project tuple + * - `ScopeService`: manages the scope and exposes it to the application + */ + +/** + * Represents a _scope_, which is defined as an organization/project tuple. + * This class is intended as a simple data structure and is used by the + * scope service (see below). + * Immutable. + */ +class Scope { + /** + * @type {?object} + * @private + */ + #org = null; + + /** + * @type {?object} + * @private + */ + #project = null; + + /** + * Initializes an instance of `Scope` with optional org and/or project. + * @param {?object} org + * @param {?object} project + */ + constructor(org = null, project = null) { + if (org) this.#org = org; + if (project) this.#project = project; + } + + /** + * Org for this scope. Immutable. + * @type {?object} + */ + get org() { + return this.#org; + } + + /** + * Project for this scope. If org is null, so is project. + * @type {?object} + */ + get project() { + return this.org ? this.#project : null; + } + + /** + * Returns the JSON serialization of this scope. + * @return {?object} + */ + toJSON() { + const { org, project } = this; + return JSON.stringify({ org, project }); + } +} + +export { Scope }; + +/** + * The scope service manages and persists the current scope. 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. + * + * For now, should an invalid scope arise, it is left to the user to refresh, + * since they are likely to encounter 404s. This is an acceptable tradeoff for + * an initial release because invalid scopes are considered uncommon. + */ +export default class ScopeService extends Service { + /** + * The currently selected scope. + * @type {?Scope} + */ + @tracked scope = null; + + /** + * When setting org, `service.org = {...}`, a new scope is instantiated, + * since scopes are immutable. + * @type {?object} + */ + @computed('scope.org') + get org() { + return this.scope ? this.scope.org : null; + } + set org(org) { + this.scope = new Scope(org, this.project); + } + + /** + * When setting project, `service.project = {...}`, a new scope is + * instantiated, since scopes are immutable. + * @type {?object} + */ + @computed('scope.project') + get project() { + return this.scope ? this.scope.project : null; + } + set project(project) { + this.scope = new Scope(this.org, project); + } +} diff --git a/addons/api/app/services/scope.js b/addons/api/app/services/scope.js new file mode 100644 index 000000000..6c4b4d6c8 --- /dev/null +++ b/addons/api/app/services/scope.js @@ -0,0 +1 @@ +export { default, Scope } from 'api/services/scope'; diff --git a/addons/api/tests/dummy/app/routes/application.js b/addons/api/tests/dummy/app/routes/application.js new file mode 100644 index 000000000..62946d66d --- /dev/null +++ b/addons/api/tests/dummy/app/routes/application.js @@ -0,0 +1,10 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class ApplicationRoute extends Route { + @service scope; + + afterModel() { + //console.log(this.scope); + } +} diff --git a/addons/api/tests/dummy/app/templates/application.hbs b/addons/api/tests/dummy/app/templates/application.hbs index 5230580f8..f8bc38e7b 100644 --- a/addons/api/tests/dummy/app/templates/application.hbs +++ b/addons/api/tests/dummy/app/templates/application.hbs @@ -1,3 +1,3 @@

Welcome to Ember

-{{outlet}} \ No newline at end of file +{{outlet}} diff --git a/addons/api/tests/dummy/app/templates/index.hbs b/addons/api/tests/dummy/app/templates/index.hbs new file mode 100644 index 000000000..9015a7a32 --- /dev/null +++ b/addons/api/tests/dummy/app/templates/index.hbs @@ -0,0 +1 @@ +index diff --git a/addons/api/tests/unit/services/scope-test.js b/addons/api/tests/unit/services/scope-test.js new file mode 100644 index 000000000..a1844e3e4 --- /dev/null +++ b/addons/api/tests/unit/services/scope-test.js @@ -0,0 +1,67 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { Scope } from '../../../services/scope'; + +module('Unit | Service | scope class', function (hooks) { + setupTest(hooks); + + test('it provides a scope class', function (assert) { + assert.expect(1); + assert.ok(Scope); + }); + + test('it has initializable org and project members', function (assert) { + assert.expect(2); + const org = { id: 1 }; + const project = { id: 2 }; + const scope = new Scope(org, project); + assert.equal(scope.org.id, 1); + assert.equal(scope.project.id, 2); + }); + + test('it has a null project if org is null, even if project was initialized to non-null', function (assert) { + assert.expect(2); + const org = null; + const project = { id: 2 }; + const scope = new Scope(org, project); + assert.notOk(scope.org); + assert.notOk(scope.project); + }); + + test('it serializes to JSON', function (assert) { + assert.expect(2); + let scope = new Scope({ id: 1 }); + assert.deepEqual(JSON.parse(scope.toJSON()), { + org: { id: 1 }, + project: null, + }); + scope = new Scope({ id: 1 }, { id: 2 }); + assert.deepEqual(JSON.parse(scope.toJSON()), { + org: { id: 1 }, + project: { id: 2 }, + }); + }); +}); + +module('Unit | Service | scope', function (hooks) { + setupTest(hooks); + + test('it returns nulls when scope is not set', function (assert) { + assert.expect(3); + const service = this.owner.lookup('service:scope'); + assert.equal(service.scope, null); + assert.equal(service.org, null); + assert.equal(service.project, null); + }); + + test('it aliases org and project to a scope instance', 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.org, service.scope.org); + assert.equal(service.project, service.scope.project); + }); +}); diff --git a/addons/rose/app/controllers/index.js b/addons/rose/app/controllers/index.js index d311e73a0..3b56b102a 100644 --- a/addons/rose/app/controllers/index.js +++ b/addons/rose/app/controllers/index.js @@ -3,8 +3,8 @@ import { action } from '@ember/object'; export default class IndexController extends Controller { @action - selectChange(value) { + selectChange(/*value*/) { // eslint-disable-next-line no-console - console.log(value); + //console.log(value); } }