diff --git a/addons/core/package.json b/addons/core/package.json index 92ef345ee..ca419d343 100644 --- a/addons/core/package.json +++ b/addons/core/package.json @@ -30,14 +30,15 @@ "dependencies": { "api": "*", "auth": "*", + "ember-browser-services": "^1.1.6", "ember-cli-babel": "^7.23.0", "ember-cli-htmlbars": "^5.3.1", + "ember-feature-flags": "^6.0.0", "ember-get-config": "^0.3.0", "ember-intl": "^5.5.0", "ember-loading": "^0.4.0", "ember-page-title": "^6.0.3", "ember-route-action-helper": "^2.0.8", - "ember-feature-flags": "^6.0.0", "rose": "*" }, "devDependencies": { diff --git a/ui/admin/app/routes/authentication-error.js b/ui/admin/app/routes/authentication-error.js index 382741c74..3f9147cc6 100644 --- a/ui/admin/app/routes/authentication-error.js +++ b/ui/admin/app/routes/authentication-error.js @@ -1,12 +1,17 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class AuthenticationErrorRoute extends Route { + // =services + + @service('browser/window') window; + // =methods /** * @return {string} */ model() { - return window.location.toString(); + return this.window.location.toString(); } } diff --git a/ui/admin/app/routes/scopes/scope/authenticate/method.js b/ui/admin/app/routes/scopes/scope/authenticate/method.js index 2261c90f2..a7f6be207 100644 --- a/ui/admin/app/routes/scopes/scope/authenticate/method.js +++ b/ui/admin/app/routes/scopes/scope/authenticate/method.js @@ -11,6 +11,7 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { @service session; @service notify; @service intl; + @service('browser/window') window; // =methods @@ -36,13 +37,11 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { } /** - * Opens the specified URL in a new tab or window. By default this uses - * `window.open`, but may be overriden. + * Opens the specified URL in a new tab or window. * @param {string} url */ - async openExternalOIDCFlow(url) { - // TODO don't use window directly - await window.open(url); + openExternalOIDCFlow(url) { + this.window.open(url); } // =actions diff --git a/ui/admin/app/services/window-manager.js b/ui/admin/app/services/window-manager.js new file mode 100644 index 000000000..8c54be361 --- /dev/null +++ b/ui/admin/app/services/window-manager.js @@ -0,0 +1,58 @@ +import Service from '@ember/service'; +import { inject as service } from '@ember/service'; + +/** + * The window manager service enables browser windows to open and close + * without the need to retain window references at the callsite. + */ +export default class WindowManagerService extends Service { + // =services + + @service('browser/window') window; + + // =attributes + + /** + * An array of windows under management. + * @private + * @type {[Window]} + */ + #managedWindows = []; + + // =methods + + /** + * Opens a new window of the specified URL via browser services. + * @param {string} url + * @see `window.open` + */ + open(url) { + const newWindow = this.window.open(url); + this.add(newWindow); + } + + /** + * Closes all windows under management and purges their references. + * @see `window.close` + */ + closeAll() { + this.#managedWindows.forEach((window) => window.close()); + this.purgeAll(); + } + + /** + * Adds a window to management. + * @param {Window} window + */ + add(window) { + this.#managedWindows.push(window); + } + + /** + * Resets the managed windows array. As long as no array references are + * retained, this should result in the original array being GC'd. + */ + purgeAll() { + this.#managedWindows = []; + } +} diff --git a/ui/admin/tests/unit/services/window-manager-test.js b/ui/admin/tests/unit/services/window-manager-test.js new file mode 100644 index 000000000..f0489bdc9 --- /dev/null +++ b/ui/admin/tests/unit/services/window-manager-test.js @@ -0,0 +1,54 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { setupBrowserFakes } from 'ember-browser-services/test-support'; +import Service from '@ember/service'; + +module('Unit | Service | window-manager', function (hooks) { + setupTest(hooks); + setupBrowserFakes(hooks, { window: true }); + + test('it opens window instances', function (assert) { + assert.expect(1); + const fakeURL = 'bloop://not-a-real-url'; + // Register mock window service + this.owner.register( + 'service:browser/window', + class extends Service { + open(url) { + assert.equal(url, fakeURL); + } + } + ); + const service = this.owner.lookup('service:window-manager'); + service.open(fakeURL); + }); + + test('it closes all open window instances and _does not_ close already-closed instances', function (assert) { + const instanceCount = 3; + const assertionsPerInstance = 1; + assert.expect(instanceCount * assertionsPerInstance); + // Register mock window service + this.owner.register( + 'service:browser/window', + class extends Service { + open() { + return { + close() { + assert.ok(true, 'Window closed.'); + }, + }; + } + } + ); + const service = this.owner.lookup('service:window-manager'); + for (let i = 0; i < instanceCount; i++) { + service.open(); + } + // First, close all open instances. + service.closeAll(); + // Try closeAll again, which should result in no additional assertions. + // See assert.expect above, which accounts for a single close call + // per instance. + service.closeAll(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8653d0e50..7e936fe49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6324,9 +6324,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001208: - version "1.0.30001208" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" - integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== + version "1.0.30001219" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz#5bfa5d0519f41f993618bd318f606a4c4c16156b" + integrity sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ== capture-exit@^2.0.0: version "2.0.0" @@ -8244,6 +8244,15 @@ ember-breadcrumbs@^0.2.3: dependencies: ember-cli-babel "^7.10.0" +ember-browser-services@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/ember-browser-services/-/ember-browser-services-1.1.6.tgz#ea4a9cec2af1210bcc65d57cdf635d51781cf803" + integrity sha512-a5Hyo3JCnZRwN8utEoQA5fHIXQobxvwVXlJXgFSQJbm7FaogqBpQ+GNvS1ECl2KG8m/jTKJoLv2rhZxgoKSrvA== + dependencies: + ember-cli-babel "^7.24.0" + ember-cli-htmlbars "^5.7.1" + ember-cli-typescript "^4.1.0" + ember-cli-addon-docs-yuidoc@^0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/ember-cli-addon-docs-yuidoc/-/ember-cli-addon-docs-yuidoc-0.2.4.tgz#dd8f5cf4982e8492b255fb677e9d771925e27dce" @@ -8353,6 +8362,39 @@ ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.16.0, ember-cli-babel@^6.3.0, ember-cli-version-checker "^2.1.2" semver "^5.5.0" +ember-cli-babel@^7.24.0: + version "7.26.4" + resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.4.tgz#b73d53592c05479814ca1770b3b849d4e01a87f0" + integrity sha512-GibwLrBUVj8DxNMnbE5PObEIeznX6ohOdYHoSMmTkCBuXa/I9amRGIjBhNlDbAJj+exVKKZoEGAdrV13gnyftQ== + dependencies: + "@babel/core" "^7.12.0" + "@babel/helper-compilation-targets" "^7.12.0" + "@babel/plugin-proposal-class-properties" "^7.13.0" + "@babel/plugin-proposal-decorators" "^7.13.5" + "@babel/plugin-transform-modules-amd" "^7.13.0" + "@babel/plugin-transform-runtime" "^7.13.9" + "@babel/plugin-transform-typescript" "^7.13.0" + "@babel/polyfill" "^7.11.5" + "@babel/preset-env" "^7.12.0" + "@babel/runtime" "7.12.18" + amd-name-resolver "^1.3.1" + babel-plugin-debug-macros "^0.3.4" + babel-plugin-ember-data-packages-polyfill "^0.1.2" + babel-plugin-ember-modules-api-polyfill "^3.5.0" + babel-plugin-module-resolver "^3.2.0" + broccoli-babel-transpiler "^7.8.0" + broccoli-debug "^0.6.4" + broccoli-funnel "^2.0.2" + broccoli-source "^2.1.2" + clone "^2.1.2" + ember-cli-babel-plugin-helpers "^1.1.1" + ember-cli-version-checker "^4.1.0" + ensure-posix-path "^1.0.2" + fixturify-project "^1.10.0" + resolve-package-path "^3.1.0" + rimraf "^3.0.1" + semver "^5.5.0" + ember-cli-code-coverage@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/ember-cli-code-coverage/-/ember-cli-code-coverage-1.0.2.tgz#615fc7af8bc7d9388a28371c2825c224a936d73f" @@ -19191,9 +19233,9 @@ socks-proxy-agent@^5.0.0: socks "^2.3.3" socks@^2.3.3: - version "2.6.0" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2" - integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw== + version "2.6.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== dependencies: ip "^1.1.5" smart-buffer "^4.1.0"