diff --git a/src/about/index.js b/src/about/index.js index b816a80e..6d3f7f7e 100644 --- a/src/about/index.js +++ b/src/about/index.js @@ -18,29 +18,30 @@ limitations under the License. import $ from 'jquery'; import ServiceWorkerBridge from '../service-worker-bridge'; -const tabActivator = new ServiceWorkerBridge().serverLocatoor; - -$(() => { - const date = new Date().getFullYear(); - $('#copyright').append(`${date} Nuxeo`); - $('#apache').click(() => { - tabActivator.loadNewExtensionTab( - 'http://www.apache.org/licenses/LICENSE-2.0' - ); - }); - $('#feedback').click(() => { - tabActivator.loadNewExtensionTab( - 'https://portal.prodpad.com/40c295d6-739d-11e7-9e52-06df22ffaf6f' - ); - }); - $('#apache').click(() => { - tabActivator.loadNewExtensionTab( - 'http://www.apache.org/licenses/LICENSE-2.0' - ); - }); - $('#feedback').click(() => { - tabActivator.loadNewExtensionTab( - 'https://portal.prodpad.com/40c295d6-739d-11e7-9e52-06df22ffaf6f' - ); - }); -}); +new ServiceWorkerBridge() + .bootstrap() + .then((worker) => worker.tabActivator) + .then((tabActivator) => $(() => { + const date = new Date().getFullYear(); + $('#copyright').append(`${date} Nuxeo`); + $('#apache').click(() => { + tabActivator.loadNewExtensionTab( + 'http://www.apache.org/licenses/LICENSE-2.0' + ); + }); + $('#feedback').click(() => { + tabActivator.loadNewExtensionTab( + 'https://portal.prodpad.com/40c295d6-739d-11e7-9e52-06df22ffaf6f' + ); + }); + $('#apache').click(() => { + tabActivator.loadNewExtensionTab( + 'http://www.apache.org/licenses/LICENSE-2.0' + ); + }); + $('#feedback').click(() => { + tabActivator.loadNewExtensionTab( + 'https://portal.prodpad.com/40c295d6-739d-11e7-9e52-06df22ffaf6f' + ); + }); + })); diff --git a/src/content/main.js b/src/content/main.js index d151ea18..31674093 100644 --- a/src/content/main.js +++ b/src/content/main.js @@ -19,11 +19,9 @@ limitations under the License. import ServiceWorkerBridge from '../service-worker-bridge'; -const serviceWorkerBridge = new ServiceWorkerBridge(); - class Content { - constructor() { - this.browserStore = serviceWorkerBridge.browserStore; + constructor(store) { + this.browserStore = store; // fetch back the server url from the browser store this.browserStore.get(['server']).then((data) => { @@ -60,7 +58,10 @@ class ContentMessageHandler { } } -const content = new Content(); -const handler = new ContentMessageHandler(content); +new ServiceWorkerBridge().bootstrap().then((worker) => { + const content = new Content(worker.browserStore); + const handler = new ContentMessageHandler(content); -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => handler.handle(request, sender, sendResponse)); + chrome.runtime.onMessage + .addListener((request, sender, sendResponse) => handler.handle(request, sender, sendResponse)); +}); diff --git a/src/json/index.js b/src/json/index.js index e0d93088..da42a6e3 100644 --- a/src/json/index.js +++ b/src/json/index.js @@ -13,21 +13,24 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import $ from 'jquery'; +import jQuery from 'jquery'; import 'bootstrap'; import hljs from 'highlight.js'; import '../../public/styles/mono-blue.css'; import ServiceWorkerBridge from '../service-worker-bridge'; -$(() => { - const jsonHighlighter = new ServiceWorkerBridge().jsonHighlighter; - jsonHighlighter.input().then((input) => { - document.getElementById('json-string').textContent = input; - try { - hljs.highlightAll(); - } catch (e) { - console.log('Sorry! JSON highlighting only available in Chrome.'); - } - }); -}); +new ServiceWorkerBridge() + .bootstrap() + .then((worker) => jQuery(() => { + worker.jsonHighlighter + .input() + .then((input) => { + document.getElementById('json-string').textContent = input; + try { + hljs.highlightAll(); + } catch (e) { + console.log('Sorry! JSON highlighting only available in Chrome.'); + } + }); + })); diff --git a/src/main/browser-store.js b/src/main/browser-store.js index 42d8c7d2..8c00f537 100644 --- a/src/main/browser-store.js +++ b/src/main/browser-store.js @@ -1,8 +1,20 @@ -class BrowserStore { +import ServiceWorkerComponent from './service-worker-component'; + +class BrowserStore extends ServiceWorkerComponent { constructor(worker, namespace = 'nuxeo-browser-extension.') { - this.worker = worker; + super(worker); + + // Define propertie this.namespace = namespace; + // Bind methods + Object.getOwnPropertyNames(Object.getPrototypeOf(this)) + .filter((prop) => typeof this[prop] === 'function' && prop !== 'constructor') + .forEach((method) => { + this[method] = this[method].bind(this); + }); + + // Define functors this.keysOf = (data) => { if (Array.isArray(data)) { return data; @@ -31,13 +43,6 @@ class BrowserStore { // call the function to get the default value return data[key](); }; - - // Bind methods - Object.getOwnPropertyNames(Object.getPrototypeOf(this)) - .filter((prop) => typeof this[prop] === 'function' && prop !== 'constructor') - .forEach((method) => { - this[method] = this[method].bind(this); - }); } get(input) { diff --git a/src/main/connect-locator.js b/src/main/connect-locator.js index eaa05470..ab81f038 100644 --- a/src/main/connect-locator.js +++ b/src/main/connect-locator.js @@ -1,21 +1,24 @@ import CryptoJS from 'crypto-js'; +import ServiceWorkerComponent from './service-worker-component'; -class ConnectLocator { +class ConnectLocator extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; - // Declare private methods - this.credentialsKeyOf = (location) => { - const hash = CryptoJS - .SHA512(location.toString()) - .toString(); - return `connect-locator.${hash}`; - }; + super(worker); + // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) .filter((prop) => typeof this[prop] === 'function' && prop !== 'constructor') .forEach((method) => { this[method] = this[method].bind(this); }); + + // Declare functors + this.credentialsKeyOf = (location) => { + const hash = CryptoJS + .SHA512(location.toString()) + .toString(); + return `connect-locator.${hash}`; + }; } withUrl(input) { diff --git a/src/main/declarative-net-engine.js b/src/main/declarative-net-engine.js index d859797f..3b8b472e 100644 --- a/src/main/declarative-net-engine.js +++ b/src/main/declarative-net-engine.js @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ +import ServiceWorkerComponent from './service-worker-component'; class BaseRule { constructor() { @@ -146,9 +147,11 @@ class RedirectRule extends BaseRule { } } -class DeclarativeNetEngine { +class DeclarativeNetEngine extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); + + // Define properties this.rules = new Map(); this.rulesToAdd = []; this.rulesToRemove = []; @@ -160,7 +163,7 @@ class DeclarativeNetEngine { this[method] = this[method].bind(this); }); - // Private functions + // Define functors this.requestOf = (rulesToAdd, rulesToRemove) => ({ addRules: rulesToAdd.map((rule) => rule.toJson()), removeRuleIds: rulesToRemove.map((rule) => rule.hashCode()), diff --git a/src/main/designer-live-preview.js b/src/main/designer-live-preview.js index 912365ae..6ddb27b6 100644 --- a/src/main/designer-live-preview.js +++ b/src/main/designer-live-preview.js @@ -18,16 +18,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { resolve } from 'nuxeo/lib/deps/promise'; import DeclarativeNetComponents from './declarative-net-engine'; +import ServiceWorkerComponent from './service-worker-component'; const BasicAuthenticationHeaderRule = DeclarativeNetComponents.BasicAuthenticationHeaderRule; const RedirectRule = DeclarativeNetComponents.RedirectRule; -class DesignerLivePreview { +class DesignerLivePreview extends ServiceWorkerComponent { // eslint-disable-next-line no-unused-vars constructor(worker) { - this.worker = worker; + super(worker); // Set defaukt properties for the class this.undoByProjectNames = new Map(); @@ -124,12 +124,12 @@ class DesignerLivePreview { modifyUrlForUIPath(url) { const fragments = url.pathname.split('/'); if (fragments[2] !== 'ui') { - return resolve(url); + return Promise.resolve(url); } fragments.splice(3, 0, ''); // Insert an empty string after 'ui' const newUrl = new URL(url); newUrl.pathname = fragments.join('/'); - return resolve(newUrl); + return Promise.resolve(newUrl); } toggle(projectName) { diff --git a/src/main/desktop-notifier.js b/src/main/desktop-notifier.js index 4685b196..b1e49d45 100644 --- a/src/main/desktop-notifier.js +++ b/src/main/desktop-notifier.js @@ -15,11 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +import ServiceWorkerComponent from './service-worker-component'; + const namespacedIdOf = (id) => `nuxeo-web-extension-${id}`; -class DesktopNotifier { +class DesktopNotifier extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) diff --git a/src/main/document-browser.js b/src/main/document-browser.js index 5114d7f3..3215ca16 100644 --- a/src/main/document-browser.js +++ b/src/main/document-browser.js @@ -1,14 +1,15 @@ /* eslint-disable comma-dangle */ import DOMPurify from 'dompurify'; +import ServiceWorkerComponent from './service-worker-component'; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const pathPattern = /^\//; const selectFromPattern = /SELECT .* FROM /i; const webuiPattern = /nuxeo\/ui\/#!\//; -class DocumentBrowser { +class DocumentBrowser extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) @@ -18,51 +19,67 @@ class DocumentBrowser { }); } - listenToChromeEvents() { - const cleanupFunctions = []; + // eslint-disable-next-line no-unused-vars + activate(self) { + return Promise + .resolve([]) + .then((undoStack) => { + chrome.omnibox.onInputEntered.addListener(this.openDocument); + undoStack.push(() => chrome.omnibox.onInputEntered.removeListener(this.openDocument)); - chrome.omnibox.onInputEntered.addListener(this.openDocument); - cleanupFunctions.push(() => chrome.omnibox.onInputEntered.removeListener(this.openDocument)); + chrome.omnibox.onInputChanged.addListener(this.suggestDocument); + undoStack.push(() => chrome.omnibox.onInputChanged.removeListener(this.suggestDocument)); - chrome.omnibox.onInputChanged.addListener(this.suggestDocument); - cleanupFunctions.push(() => chrome.omnibox.onInputChanged.removeListener(this.suggestDocument)); - - return () => { - while (cleanupFunctions.length > 0) { - const cleanupFunction = cleanupFunctions.pop(); - cleanupFunction(); - } - }; + return undoStack; + }) + .then((undoStack) => () => undoStack.forEach((undo) => undo())); } onWebUI() { - return webuiPattern.exec(this.worker.serverConnector._nuxeo._baseURL); + return Promise.resolve( + webuiPattern.exec(this.worker.serverConnector._nuxeo._baseURL) + ); } openDocument(input) { - if (uuidPattern.test(input)) { - this.openDocFromId(input); - } else if (pathPattern.test(input)) { - this.openDocFromPath(input); - } else { - console.error(`Invalid input ${input}`); - } + return Promise.resolve(input) + .then((text) => { + if (!uuidPattern.test(input)) { + return { text, promise: null }; + } + return { text, promise: this.openDoc(text) }; + }) + .then(({ text, promise }) => { + if (promise !== null) { + return { text, promise }; + } + if (!pathPattern.test(input)) { + return { text, promise: null }; + } + return { text, promise: this.openDoc(text) }; + }) + .then(({ text, promise }) => { + if (promise == null) { + return Promise.reject(new Error(`Invalid input ${text}`)); + } + return promise; + }); } openDocFromId(id) { - this.worker.serverConnector.withNuxeo() + return this.worker.serverConnector.withNuxeo() .then((nuxeo) => nuxeo.request(`/id/${id}`)) .then((request) => this.doOpenDoc(request)); } openDocFromPath(path) { - this.worker.serverConnector.withNuxeo() + return this.worker.serverConnector.withNuxeo() .then((nuxeo) => nuxeo.request(`/path/${path}`)) .then((request) => this.doOpenDoc(request)); } doOpenDoc(request) { - request + return request .schemas('*') .enrichers({ document: ['acls', 'permissions'] }) .get() @@ -73,60 +90,73 @@ class DocumentBrowser { } return `nxdoc/default/${uid}/view_documents`; } - this.browserNavigator.loadNewExtensionTab(pathOf(doc.uid), true); + this.tabNavigationHandler.loadNewExtensionTab(pathOf(doc.uid), true); }) .catch((error) => { console.log(error); }); } - suggestDocument(text, suggest) { - if (uuidPattern.test(text)) { - this.openDocFromId(text); - return; - } - if (pathPattern.test(text)) { - this.openDocFromPath(text); - return; - } - const jsonQueryOf = (query) => { - if (selectFromPattern.test(query)) { - return text; - } - return `SELECT * FROM Document WHERE ecm:fulltext = "${text}"`; - }; - const query = jsonQueryOf(text).replace(/'/g, '"'); - const suggestions = []; - this.nuxeo - .repository() - .schemas(['dublincore', 'common', 'uid']) - .query({ - query, - sortBy: 'dc:modified', + suggestDocument(input, suggest) { + return Promise.resolve({ text: input, pronise: null }) + .then(({ text, promise }) => { + if (promise || !uuidPattern.test(text)) { + return { text, promise }; + } + // document selection using UUID + return { text, promise: this.openDocFromId(text) }; }) - .then((res) => { - if (res.entries.length > 0) { - res.entries.forEach((doc) => { - const sanitizedDoc = DOMPurify.sanitize( - `${doc.title} ${doc.path}`, - { ALLOWED_TAGS: ['match', 'dim'] } - ); - suggestions.push({ - content: doc.uid, - description: sanitizedDoc, - }); - }); + .then(({ text, promise }) => { + if (promise || !pathPattern.test(text)) { + return { text, promise }; } - if (res.entries.length > 5) { - const sanitizedDoc = DOMPurify.sanitize( - 'Want more results? Try the fulltext searchbox from the Nuxeo Dev Tools extension window.', - { ALLOWED_TAGS: ['match', 'dim'] } - ); - chrome.omnibox.setDefaultSuggestion({ - description: sanitizedDoc, - }); + // document selection using path + return { text, promise: this.openDocFromPath(text) }; + }) + .then(({ text, promise }) => { + if (promise) { + return promise; } - suggest(suggestions); + // document selection using NXQL + const jsonQueryOf = (query) => { + if (selectFromPattern.test(query)) { + return text; + } + return `SELECT * FROM Document WHERE ecm:fulltext = "${text}"`; + }; + const query = jsonQueryOf(text).replace(/'/g, '"'); + const suggestions = []; + return this.nuxeo + .repository() + .schemas(['dublincore', 'common', 'uid']) + .query({ + query, + sortBy: 'dc:modified', + }) + .then((res) => { + if (res.entries.length > 0) { + res.entries.forEach((doc) => { + const sanitizedDoc = DOMPurify.sanitize( + `${doc.title} ${doc.path}`, + { ALLOWED_TAGS: ['match', 'dim'] } + ); + suggestions.push({ + content: doc.uid, + description: sanitizedDoc, + }); + }); + } + if (res.entries.length > 5) { + const sanitizedDoc = DOMPurify.sanitize( + 'Want more results? Try the fulltext searchbox from the Nuxeo Dev Tools extension window.', + { ALLOWED_TAGS: ['match', 'dim'] } + ); + chrome.omnibox.setDefaultSuggestion({ + description: sanitizedDoc, + }); + } + suggest(suggestions); + }); }); } diff --git a/src/main/json-highlighter.js b/src/main/json-highlighter.js index 30d1bf63..322b2b99 100644 --- a/src/main/json-highlighter.js +++ b/src/main/json-highlighter.js @@ -1,6 +1,10 @@ -class JsonHighlighter { +import ServiceWorkerComponent from './service-worker-component'; + +class JsonHighlighter extends ServiceWorkerComponent { // eslint-disable-next-line no-unused-vars constructor(worker) { + super(worker); + this._input = ''; // Bind methods diff --git a/src/main/main-firefox.js b/src/main/main-firefox.js index db683652..8ed508e0 100644 --- a/src/main/main-firefox.js +++ b/src/main/main-firefox.js @@ -28,4 +28,4 @@ console.log(`Service Worker: ${buildTime} - ${buildVersion} - ${browserVendor} - const serviceWorker = new ServiceWorker(developmentMode, buildTime, buildVersion, browserVendor); // eslint-disable-next-line no-unused-vars -const undoListeningHandler = serviceWorker.listenToChromeEvents(); +const undoListeningHandler = serviceWorker.activate(); diff --git a/src/main/manifest-chrome.json b/src/main/manifest-chrome.json index ca2984d9..9fadf04d 100644 --- a/src/main/manifest-chrome.json +++ b/src/main/manifest-chrome.json @@ -13,7 +13,7 @@ }, "permissions": [ "storage", - "cookies", + "contextMenus", "tabs", "activeTab", "notifications", diff --git a/src/main/repository-indexer.js b/src/main/repository-indexer.js index b0fdc1b3..0577930f 100644 --- a/src/main/repository-indexer.js +++ b/src/main/repository-indexer.js @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -class RepositoryIndexer { +import ServiceWorkerComponent from './service-worker-component'; + +class RepositoryIndexer extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); + this.waiting = undefined; // Bind methods diff --git a/src/main/runtime-build-info.js b/src/main/runtime-build-info.js index 6c5c86a0..b777b3c2 100644 --- a/src/main/runtime-build-info.js +++ b/src/main/runtime-build-info.js @@ -1,17 +1,21 @@ /* eslint-disable max-classes-per-file */ +import ServiceWorkerComponent from './service-worker-component'; + +class RuntimeBuildInfo extends ServiceWorkerComponent { + constructor(worker, buildTime, buildVersion, browserVendor) { + super(worker); -class RuntimeBuildInfo { - constructor(buildTime, buildVersion, browserVendor) { this._developer = 'NOS Team '; this._browserVendor = browserVendor; this._buildTime = buildTime; this._buildVersion = buildVersion; - // binds methods to this - this.developer = this.developer.bind(this); - this.browserVendor = this.browserVendor.bind(this); - this.buildTime = this.buildTime.bind(this); - this.buildVersion = this.buildVersion.bind(this); + // Bind methods + Object.getOwnPropertyNames(Object.getPrototypeOf(this)) + .filter((prop) => typeof this[prop] === 'function' && prop !== 'constructor') + .forEach((method) => { + this[method] = this[method].bind(this); + }); } developer() { @@ -31,8 +35,9 @@ class RuntimeBuildInfo { } } -class DevelopmentMode { - constructor(isEnabled) { +class DevelopmentMode extends ServiceWorkerComponent { + constructor(worker, isEnabled) { + super(worker); this._isEnabled = isEnabled; // bind this methods diff --git a/src/main/server-connector.js b/src/main/server-connector.js index 4dd40ece..ab0c3fb0 100644 --- a/src/main/server-connector.js +++ b/src/main/server-connector.js @@ -1,12 +1,16 @@ /* eslint-disable comma-dangle */ /* eslint-disable max-classes-per-file */ import Nuxeo from 'nuxeo'; +import ServiceWorkerComponent from './service-worker-component'; -class ServerConnector { +class ServerConnector extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; - this.rootUrl = undefined; + super(worker); + + // Define properties + this.disconnect = () => {}; this.nuxeo = undefined; + this.rootUrl = undefined; // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) @@ -14,66 +18,56 @@ class ServerConnector { .forEach((method) => { this[method] = this[method].bind(this); }); - - // listeners - this.onInputChanged = null; } onNewServer(rootUrl) { - return new Promise((resolve, reject) => { - try { - if (rootUrl) { - if (this.isConnected()) { - this.disconnect(); - } - this.connect(rootUrl, resolve, reject); - } else { - this.disconnect(); - resolve(); - } - } catch (error) { - reject(error); - } - }); + if (!rootUrl) { + return this.disconnect(); + } + return this.isConnected() + .then((connected) => { + if (connected) return this.disconnect(); + return true; + }) + .then(() => this.connect(rootUrl)); } - connect(rootUrl, resolve, reject) { + connect(rootUrl) { this.rootUrl = rootUrl; this.nuxeo = new Nuxeo({ baseURL: this.rootUrl }); - this.nuxeo.login() + return this.nuxeo + .login() .then(() => { chrome.omnibox.onInputChanged.addListener(this.onInputChanged = this.suggestDocument); }) - .then(() => resolve()) - .catch((error) => { - if (error.response) { - this.handleErrors(error, this.defaultServerError); - return null; + .then(() => () => { + this.disconnect = undefined; + this.rootUrl = undefined; + this.nuxeo = undefined; + chrome.omnibox.onInputChanged.removeListener(this.suggestDocument); + }) + .then((disconnect) => { + this.disconnect = disconnect.bind(this); + return this.disconnect; + }) + .catch((cause) => { + if (cause.response) { + this.handleErrors(cause, this.defaultServerError); + return () => {}; } - return reject(error); + throw cause; }); } - disconnect() { - chrome.omnibox.onInputChanged.removeListener(this.onInputChanged); - - this.nuxeo = null; - this.rootUrl = null; - this.onInputChanged = null; - } - isConnected() { - return this.rootUrl != null; + return Promise.resolve(this.rootUrl != null); } runtimeInfo() { - return { rootUrl: this.rootUrl, nuxeo: this.nuxeo }; - // return this.worker.browserStore - // .get('serverInfo') - // // eslint-disable-next-line arrow-body-style - // .then((store) => { - // return store.serverInfo; - // }); + return Promise.resolve({ + rootUrl: this.rootUrl, + nuxeo: this.nuxeo + }); } withNuxeo() { @@ -193,6 +187,18 @@ class ServerConnector { }); } + listStudioProjects() { + return this.withNuxeo().then((nuxeo) => nuxeo + .operation('Studio.ListProjects') + .execute() + .catch((cause) => { + if (!cause.response) { + throw cause; + } + return this.handleErrors(cause, this.defaultServerError); + })); + } + restart() { const notifyRestart = (context) => new Promise((resolve) => { this.worker.desktopNotifier.notify('reload', { @@ -201,7 +207,7 @@ class ServerConnector { iconUrl: '../images/nuxeo-128.png', requireInteraction: false, }) - .then(() => this.worker.browserNavigator.reloadServerTab(context, 10000)) + .then(() => this.worker.tabNavigationHandler.reloadServerTab(context, 10000)) .then(() => this.worker.desktopNotifier.cancel('reload')) .then(() => resolve()); }); @@ -219,7 +225,7 @@ class ServerConnector { const rootUrl = this.rootUrl; const restartUrl = `${this.rootUrl}/site/connectClient/uninstall/restart`; - return this.worker.browserNavigator.disableTabExtension() + return this.worker.tabNavigationHandler.disableTabExtension() .then((tabInfo) => this .withNuxeo() .then((nuxeo) => nuxeo diff --git a/src/main/service-worker-component.js b/src/main/service-worker-component.js new file mode 100644 index 00000000..d95fbab3 --- /dev/null +++ b/src/main/service-worker-component.js @@ -0,0 +1,42 @@ +class ServiceWorkerComponent { + constructor(worker = this) { + this.worker = worker; + } + + // eslint-disable-next-line no-unused-vars + walkComponents(object = this, path = '', recursive = false, action = (componentInput, pathInput) => {}) { + Object.getOwnPropertyNames(object) + .filter((prop) => prop !== 'worker' && object[prop] instanceof ServiceWorkerComponent) + .forEach((prop) => { + const component = object[prop]; + const newPath = path ? `${path}.${prop}` : prop; + action(component, newPath); + if (recursive) { + this.walkComponents(component, newPath, true, action); + } + }); + } + + componentsOf(object = this, recursive = false) { + const components = []; + this.walkComponents(object, '', recursive, (component) => { + components.push(component); + }); + return components; + } + + componentNamesOf(object = this, recursive = false) { + const componentNames = []; + this.walkComponents(object, '', recursive, (component, path) => { + componentNames.push(path); + }); + return componentNames; + } + + // eslint-disable-next-line class-methods-use-this + toJSON() { + return {}; // avoid circular dependencies on components + } +} + +export default ServiceWorkerComponent; diff --git a/src/main/service-worker.js b/src/main/service-worker.js index c3657b22..0b173149 100644 --- a/src/main/service-worker.js +++ b/src/main/service-worker.js @@ -1,6 +1,6 @@ /* eslint-disable max-classes-per-file */ /* eslint-disable comma-dangle */ -import BrowserNavigator from './browser-navigator'; +import TabNavigationHandler from './tab-navigation-handler'; import BrowserStore from './browser-store'; import ConnectLocator from './connect-locator'; import DeclararactiveNetCompoments from './declarative-net-engine'; @@ -11,13 +11,14 @@ import JSONHighlighter from './json-highlighter'; import RepositoryIndexer from './repository-indexer'; import RuntimeBuildComponent from './runtime-build-info'; import ServerConnector from './server-connector'; +import ServiceWorkerComponent from './service-worker-component'; import StudioHotReloader from './studio-hot-reloader'; const DeclarativeNetEngine = DeclararactiveNetCompoments.DeclarativeNetEngine; -class ServiceWorkerMessageHandler { +class ServiceWorkerMessageHandler extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) @@ -45,11 +46,11 @@ class ServiceWorkerMessageHandler { function getNestedProperty(obj, path) { return path.split('.').reduce((prev, curr) => (prev ? prev[curr] : null), obj); } - const service = getNestedProperty(worker, request.service); - if (!service) { - return Promise.reject(new Error(`Invalid service ${JSON.stringify(request)}`)); + const component = getNestedProperty(worker, request.component); + if (!component) { + return Promise.reject(new Error(`Invalid component ${JSON.stringify(request)}`)); } - if (typeof service[request.action] !== 'function') { + if (typeof component[request.action] !== 'function') { return Promise.reject(new Error(`Invalid action ${JSON.stringify(request)}`)); } this.worker.developmentMode @@ -57,7 +58,7 @@ class ServiceWorkerMessageHandler { .then((console) => console .log(`ServiceWorkerMessageHandler.handle(${JSON.stringify(request)}) called`)); return Promise - .resolve(service[request.action](...request.params)) + .resolve(component[request.action](...request.params)) .then((response) => { this.worker.developmentMode .asConsole() @@ -85,25 +86,34 @@ class ServiceWorkerMessageHandler { } } -class ServiceWorker { +class ServiceWorkerComponentInventory extends ServiceWorkerComponent { + list(recursive = false) { + const componentNames = this.componentNamesOf(this.worker, recursive); + return Promise.resolve(componentNames); + } +} + +class ServiceWorker extends ServiceWorkerComponent { constructor(developmentMode, buildTime, buildVersion, browserVendor) { + super(); // sub-componments takes reference to the worker // in order to invoke other services. The order is important as // services may be invoked while constructing. this.buildInfo = new RuntimeBuildComponent - .RuntimeBuildInfo(buildTime, buildVersion, browserVendor); - this.browserNavigator = new BrowserNavigator(this); + .RuntimeBuildInfo(this, buildTime, buildVersion, browserVendor); this.browserStore = new BrowserStore(this); + this.componentInventory = new ServiceWorkerComponentInventory(this); this.connectLocator = new ConnectLocator(this); this.declarativeNetEngine = new DeclarativeNetEngine(this); this.designerLivePreview = new DesignerLivePreview(this); this.desktopNotifier = new DesktopNotifier(this); - this.developmentMode = new RuntimeBuildComponent.DevelopmentMode(developmentMode); + this.developmentMode = new RuntimeBuildComponent.DevelopmentMode(this, developmentMode); this.documentBrowser = new DocumentBrowser(this); this.jsonHighlighter = new JSONHighlighter(this); this.repositoryIndexer = new RepositoryIndexer(this); this.serverConnector = new ServerConnector(this); this.studioHotReloader = new StudioHotReloader(this); + this.tabNavigationHandler = new TabNavigationHandler(this); // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) @@ -121,8 +131,8 @@ class ServiceWorker { return this.asPromise() .then((worker) => { // Initialize the cleanup stack - const cleanupFunctions = []; - if (typeof cleanupFunctions.push !== 'function') { + const deactivateStack = []; + if (typeof deactivateStack.push !== 'function') { throw new Error('cleanupFunctions must have a push method'); } @@ -134,21 +144,42 @@ class ServiceWorker { // install the service worker message handler const messageHandle = new ServiceWorkerMessageHandler(worker).handle; chrome.runtime.onMessage.addListener(messageHandle); - cleanupFunctions.push(() => chrome.runtime.onMessage.removeListener(messageHandle)); - - cleanupFunctions.push(worker.browserNavigator.listenToChromeEvents()); - cleanupFunctions.push(worker.documentBrowser.listenToChromeEvents()); - cleanupFunctions.push(() => worker.designerLivePreview.disable()); + deactivateStack.push(() => chrome.runtime.onMessage.removeListener(messageHandle)); + + // activate all sub-components + this.componentsOf(worker) + .map((component) => { + if (typeof component.activate !== 'function') return () => {}; + return component + .activate(self) + .then((cleanup) => worker.developmentMode.asConsole() + .then((console) => console + .log(`ServiceWorkerComponent.activate(${component.constructor.name}) called`)) + .then(() => cleanup)); + }) + .filter((cleanup) => typeof cleanup === 'function') + .forEach((cleanup) => { + deactivateStack.push(cleanup); + }); // can be used in development mode from the console for now - worker.deactivate = () => { - while (cleanupFunctions.length > 0) { - const cleanupFunction = cleanupFunctions.pop(); - cleanupFunction(); + self.nuxeoWebExtension = worker; + + return { + worker, + undo: () => { + self['nuxeo-web-extension'] = undefined; + while (deactivateStack.length > 0) { + const deactivate = deactivateStack.pop(); + deactivate(self); + } } }; - return worker; - }); + }) + // eslint-disable-next-line no-return-assign + .then(({ worker, undo }) => ( + worker.deactivate = undo.bind(worker) + )); } } diff --git a/src/main/studio-hot-reloader.js b/src/main/studio-hot-reloader.js index 707a5775..4a970588 100644 --- a/src/main/studio-hot-reloader.js +++ b/src/main/studio-hot-reloader.js @@ -16,6 +16,7 @@ limitations under the License. */ import NuxeoServerVersion from 'nuxeo/lib/server-version'; +import ServiceWorkerComponent from './service-worker-component'; const checkDependencies = `import groovy.json.JsonOutput; import org.nuxeo.connect.packages.PackageManager; @@ -39,9 +40,9 @@ def dependencies = snapshotPkg == null ? null : snapshotPkg.getDependencies(); println JsonOutput.toJson([studio: pkgName, nx: nxInstance, studioDistrib: targetPlatform, match: match, deps: dependencies]);`; -class StudioHotReloader { +class StudioHotReloader extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); // Bind methods Object.getOwnPropertyNames(Object.getPrototypeOf(this)) @@ -184,7 +185,7 @@ class StudioHotReloader { message: 'A Hot Reload has successfully been completed.', iconUrl: '../images/nuxeo-128.png', }); - this.worker.browserNavigator.reloadServerTab(); + return this.worker.tabNavigationHandler.reloadServerTab(); }) .catch((er) => { this.handleErrors(er, this.worker.serverConnector.defasultServerError); diff --git a/src/main/browser-navigator.js b/src/main/tab-navigation-handler.js similarity index 53% rename from src/main/browser-navigator.js rename to src/main/tab-navigation-handler.js index edb6cc42..3d4e55d1 100644 --- a/src/main/browser-navigator.js +++ b/src/main/tab-navigation-handler.js @@ -1,7 +1,11 @@ /* eslint-disable comma-dangle */ -class Navigator { +import ServiceWorkerComponent from './service-worker-component'; + +class TabNavigationHandler extends ServiceWorkerComponent { constructor(worker) { - this.worker = worker; + super(worker); + + // Define properties this.tabInfo = null; // Bind methods @@ -40,88 +44,98 @@ class Navigator { return Promise.resolve(input) .then((tabInfo) => { this.tabInfo = tabInfo; - chrome.action.enable(tabInfo.id); return tabInfo; }) - .then((tabInfo) => { - this.worker.developmentMode.asConsole() - .then((console) => console.log(`Enabled TabExtension for ${JSON.stringify(tabInfo)}`)); - return tabInfo; - }); + .then((tabInfo) => Promise.resolve(tabInfo.id) + .then((tabId) => chrome.action + .enable(tabId) + .then(() => chrome.action + .setBadgeText({ tabId, text: 'C' })) + .then(() => chrome.action + .setBadgeBackgroundColor({ tabId, color: '#4688F1' })) + .then(() => tabInfo))) + .then((tabInfo) => this.worker.developmentMode + .asConsole() + .then((console) => console.log(`Enabled TabExtension for ${JSON.stringify(tabInfo)}`)) + .then(() => tabInfo)); } disableTabExtension() { if (!this.tabInfo) Promise.reject(new Error('No tab info found')); return Promise.resolve(this.tabInfo) .then((tabInfo) => { - chrome.action.disable(this.tabInfo.id); this.tabInfo = null; return tabInfo; }) - .then((tabInfo) => { - this.worker.developmentMode.asConsole() - .then((console) => console.log(`Disabled TabExtension for ${JSON.stringify(tabInfo)}`)); - return tabInfo; - }); + .then((tabInfo) => Promise.resolve(tabInfo.id) + .then((tabId) => chrome.action + .disable(tabId) + .then(() => chrome.action + .setBadgeText({ tabId, text: 'D' })) + .then(() => chrome.action + .setBadgeBackgroundColor({ tabId, color: '#FF0000' }))) + .then(() => tabInfo)) + .then((tabInfo) => this.worker.developmentMode + .asConsole() + .then((console) => console.log(`Disabled TabExtension for ${JSON.stringify(tabInfo)}`)) + .then(() => tabInfo)); } - listenToChromeEvents() { - const cleanupFunctions = []; - + // eslint-disable-next-line no-unused-vars + activate(self) { // disable extension by default chrome.action.disable(); - // onInstalled event - const onInstalledHandle = () => chrome.action.disable(); - chrome.runtime.onInstalled.addListener(onInstalledHandle); - cleanupFunctions.push(() => chrome.runtime.onInstalled.removeListener(onInstalledHandle)); - - // tab activation handle - const tabActivatedHandle = (activeInfo) => chrome - .tabs.get(activeInfo.tabId, (tabInfo) => this - .enableExtensionIfNuxeoServerTab(tabInfo)); - chrome.tabs.onActivated.addListener(tabActivatedHandle); - cleanupFunctions.push(() => chrome.tabs.onActivated.removeListener(tabActivatedHandle)); - - // tab update handle - const tabUpdatedHandle = (tabId, changeInfo, tab) => { - if (changeInfo.status !== 'complete') return; - - this.enableExtensionIfNuxeoServerTab(tab); - }; - chrome.tabs.onUpdated.addListener(tabUpdatedHandle); - cleanupFunctions.push(() => chrome.tabs.onUpdated.removeListener(tabUpdatedHandle)); - - // windows focus handle - const windowFocusHandle = (windowId) => { - if (windowId === chrome.windows.WINDOW_ID_NONE) return; - chrome.tabs.query({ active: true, windowId }, (tabs) => { - if (!tabs || tabs.length === 0) return; - - this.enableExtensionIfNuxeoServerTab(tabs[0]); - }); - }; - chrome.windows.onFocusChanged.addListener(windowFocusHandle); - cleanupFunctions.push(() => chrome.windows.onFocusChanged.removeListener(windowFocusHandle)); + return Promise.resolve([]) + .then((undoStack) => { + // tab activation handle + const tabActivatedHandle = (activeInfo) => chrome + .tabs.get(activeInfo.tabId, (tabInfo) => this + .enableExtensionIfNuxeoServerTab(tabInfo)); + chrome.tabs.onActivated.addListener(tabActivatedHandle); + undoStack.push(() => chrome.tabs.onActivated.removeListener(tabActivatedHandle)); + return undoStack; + }) + .then((undoStack) => { + // tab update handle + const tabUpdatedHandle = (tabId, changeInfo, tab) => { + if (changeInfo.status !== 'complete') return; - // tab removed handle - // eslint-disable-next-line no-unused-vars - const tabRemovedHandle = (tabId, removeInfo) => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (!tabs || tabs.length === 0) return; + this.enableExtensionIfNuxeoServerTab(tab); + }; + chrome.tabs.onUpdated.addListener(tabUpdatedHandle); + undoStack.push(() => chrome.tabs.onUpdated.removeListener(tabUpdatedHandle)); + return undoStack; + }) + .then((undoStack) => { + // windows focus handle + const windowFocusHandle = (windowId) => { + if (windowId === chrome.windows.WINDOW_ID_NONE) return; + chrome.tabs.query({ active: true, windowId }, (tabs) => { + if (!tabs || tabs.length === 0) return; - this.enableExtensionIfNuxeoServerTab(tabs[0]); - }); - }; - chrome.tabs.onRemoved.addListener(tabRemovedHandle); - cleanupFunctions.push(() => chrome.tabs.onRemoved.removeListener(tabRemovedHandle)); + this.enableExtensionIfNuxeoServerTab(tabs[0]); + }); + }; + chrome.windows.onFocusChanged.addListener(windowFocusHandle); + undoStack.push(() => chrome.windows.onFocusChanged.removeListener(windowFocusHandle)); + return undoStack; + }) + .then((undoStack) => { + // tab removed handle + // eslint-disable-next-line no-unused-vars + const tabRemovedHandle = (tabId, removeInfo) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (!tabs || tabs.length === 0) return; - return () => { - while (cleanupFunctions.length > 0) { - const cleanupFunction = cleanupFunctions.pop(); - cleanupFunction(); - } - }; + this.enableExtensionIfNuxeoServerTab(tabs[0]); + }); + }; + chrome.tabs.onRemoved.addListener(tabRemovedHandle); + undoStack.push(() => chrome.tabs.onRemoved.removeListener(tabRemovedHandle)); + return undoStack; + }) + .then((undoStack) => () => undoStack.forEach((cleanup) => cleanup())); } enableExtensionIfNuxeoServerTab(info) { @@ -130,7 +144,6 @@ class Navigator { if (rootUrl) return rootUrl; return chrome.action .disable(info.tabId) - .then(() => chrome.action.isEnabled(info.id)) .then(() => undefined); }) .then((rootUrl) => { @@ -150,10 +163,10 @@ class Navigator { .then((rootUrl) => chrome.action.isEnabled(info.id) .then((isEnabled) => this.worker.developmentMode.asConsole() .then((console) => console - .log(`Handled activation of ${JSON.stringify(info)} <- rootUrl=${rootUrl}, extension=${isEnabled ? 'enabled' : 'disabled'}`)))) - .catch((error) => this.worker.developmentMode.asConsole((console) => { - console.warn(error); - console.warn(`Caught error (see previous error) <- Navigator.enableExtensionIfNuxeoServerTab(${JSON.stringify(info)})`); + .log(`Handled activation of ${JSON.stringify(info)} <- rootUrl=${rootUrl}, extension=${isEnabled ? 'enabled' : 'disabled'}`))) + .then(() => rootUrl)) + .catch((cause) => this.worker.developmentMode.asConsole((console) => { + console.warn(`Caught error (see previous error) <- Navigator.enableExtensionIfNuxeoServerTab(${JSON.stringify(info)})`, cause); })); } @@ -172,8 +185,7 @@ class Navigator { message: 'You are not authenticated. Please log in and try again.', iconUrl: '../images/access_denied.png', }); - this.reloadServerTab({ rootUrl, tabInfo }, 0); - throw new Error(`Not logged in : ${tabInfo.url}...`); + return this.reloadServerTab({ rootUrl, tabInfo }, 0); }) .then((response) => { if (response.ok) return response; @@ -201,36 +213,31 @@ class Navigator { if (!tabInfo) { throw new Error('No nuxeo server tab info selected'); } - return new Promise((resolve, reject) => { - const maxAttempts = 10; - let attempts = 0; - - const checkStatus = () => { - attempts += 1; - if (attempts > maxAttempts) { - reject(new Error('Maximum number of attempts reached')); - return; - } - - fetch(rootUrl) - .then((response) => { - if (response.ok) { - chrome.tabs.reload(tabInfo.id); - resolve(tabInfo); - } else { - // If the status page is not available, check again after a delay - setTimeout(checkStatus, waitingTime); - } - }) - .catch(() => { - // If the request failed, check again after a delay - setTimeout(checkStatus, waitingTime); - }); - }; - - // Start checking the status - checkStatus(); - }); + return { rootUrl, tabInfo }; + }).then(({ rootUrl, tabInfo }) => { + const runnningstatusUrl = `${rootUrl}/runningstatus`; + const maxAttempts = 10; + let attempts = 0; + const checkStatus = () => { + attempts += 1; + if (attempts > maxAttempts) { + throw new Error(`Maximum number of attempts reached on ${rootUrl}...`); + } + return fetch(runnningstatusUrl) + .then((response) => { + if (!response.ok) { + // If the status page is not available, check again after a delay + return new Promise((resolve) => setTimeout(resolve, waitingTime)) + .then(checkStatus); + } + chrome.tabs.reload(tabInfo.id); + return tabInfo; + }) + .catch(() => new Promise((resolve) => setTimeout(resolve, waitingTime)) + .then(checkStatus)); + }; + // Start checking the status + return checkStatus(); }); } @@ -260,4 +267,4 @@ class Navigator { } } -export default Navigator; +export default TabNavigationHandler; diff --git a/src/popup/index.js b/src/popup/index.js index 4c161405..8acfecf7 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -95,11 +95,9 @@ function loadPage(worker) { $('#designer-live-preview-button').toggleClass('enabled', isEnabled); $('#designer-live-preview-button').toggleClass('disabled', !isEnabled); }; - const toogleDesignerLivePreviewMessage = () => { + const toogleDesignerLivePreviewMessage = (cause) => { $('#designer-livepreview-message').css('display', 'block'); - // setTimeout(() => { - // $('#designer-livepreview-message').css('display', 'none'); - // }, 5000); + $('#designer-livepreview-message a').text(cause.message); }; $('#no-studio-buttons').css('display', 'none'); $('#studio').css('display', 'flex'); @@ -107,9 +105,9 @@ function loadPage(worker) { worker.designerLivePreview .isEnabled(packageName) .then((isEnabled) => toogleDesignerLivePreviewButton(isEnabled)) - .catch((error) => { - console.log('Error getting designer live preview status', error); - toogleDesignerLivePreviewMessage(); + .catch((cause) => { + console.log('Error getting designer live preview status', cause); + toogleDesignerLivePreviewMessage(cause); }); const packageLocation = new URL( @@ -121,18 +119,20 @@ function loadPage(worker) { new URL(packageLocation, connectUrl.href).toString() ); $('#studio').click(() => { - worker.browserNavigator.loadNewExtensionTab(packageLocation); + worker.tabNavigationHandler.loadNewExtensionTab(packageLocation); }); $('#hot-reload-button').click(() => { startLoadingHR() .then(() => worker.studioHotReloader.reload()) .then(stopLoading) + .then(() => worker.tabNavigationHandler.reloadServerTab()) .catch(stopLoading); }); $('#designer-live-preview-button').click(() => { worker.designerLivePreview .toggle(packageName) .then((isEnabled) => toogleDesignerLivePreviewButton(isEnabled)) + .then(() => worker.tabNavigationHandler.reloadServerTab()) .catch(() => toogleDesignerLivePreviewMessage()); }); $('#force-hot-reload-button').click(() => { @@ -171,7 +171,7 @@ function loadPage(worker) { const registerLink = (element, url) => { $(element).click(() => { - worker.browserNavigator.loadNewExtensionTab(url); + worker.tabNavigationHandler.loadNewExtensionTab(url); }); }; @@ -329,7 +329,7 @@ function loadPage(worker) { .then(openJsonWindow); } else { const jsonPath = `api/v1/repo/${repository}/${path}?enrichers.document=acls,permissions&properties=*`; - worker.browserNavigator.loadNewExtensionTab(jsonPath, true); + worker.tabNavigationHandler.loadNewExtensionTab(jsonPath, true); } }); } @@ -594,7 +594,7 @@ function loadPage(worker) { }); $('.doc-title').click((event) => { const docPath = onUI ? `ui/#!/doc/${event.currentTarget.id}` : `nxdoc/default/${event.currentTarget.id}/view_documents`; - worker.browserNavigator.loadNewExtensionTab(docPath, true); + worker.tabNavigationHandler.loadNewExtensionTab(docPath, true); }); $('.json-icon').click((event) => { event.preventDefault(); @@ -626,7 +626,7 @@ function loadPage(worker) { let openJsonWindow = (jsonObject) => { const jsonString = JSON.stringify(jsonObject, undefined, 2); worker.jsonHighlighter.input(DOMPurify.sanitize(jsonString)); - worker.browserNavigator.loadNewExtensionTab('json/index.html'); + worker.tabNavigationHandler.loadNewExtensionTab('json/index.html'); }; $('#restart-button').on('click', () => { @@ -817,13 +817,13 @@ function loadPage(worker) { } new ServiceWorkerBridge() - .asPromise() + .bootstrap() .then((worker) => { worker.developmentMode .asPromise().then(() => { - window.nuxeoWebExensions = worker; - window.nuxeoWebExensions.reloadPopup = () => worker.asPromise().then(loadPage); - window.worker = worker; + // can be used in development mode from the console for now + worker.reloadPopup = () => worker.asPromise().then(loadPage); + window.nuxeoWebExtension = worker; }) .catch((error) => console.error(error)); return worker; diff --git a/src/service-worker-bridge.js b/src/service-worker-bridge.js index 0596afe5..e0135f9d 100644 --- a/src/service-worker-bridge.js +++ b/src/service-worker-bridge.js @@ -1,30 +1,37 @@ /* eslint-disable comma-dangle */ class ServiceWorkerBridge { - constructor() { - const services = [ - 'buildInfo', - 'browserNavigator', - 'browserStore', - 'chromeNotifier', - 'connectLocator', - 'declarativeNetEngine', - 'designerLivePreview', - 'developmentMode', - 'jsonHighlighter', - 'repositoryIndexer', - 'serverConnector', - 'studioHotReloader', - 'documentBrowser' - ]; + bootstrap() { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { + extension: 'nuxeo-web-extension', + component: 'componentInventory', + action: 'list', + params: [], + }, + (response) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + if (response && response.error) { + return reject(response.error); + } + this.createProxies(response); + return resolve(this); + } + ); + }); + } - services.forEach((service) => { - this[service] = new Proxy({}, { + createProxies(components) { + components.forEach((component) => { + this[component] = new Proxy({}, { get: (target, action) => (...params) => new Promise((resolve, reject) => { chrome.runtime.sendMessage( { extension: 'nuxeo-web-extension', - service: `${service}`, + component: `${component}`, action, params, }, @@ -46,10 +53,6 @@ class ServiceWorkerBridge { }); }); } - - asPromise() { - return new Promise((resolve) => resolve(this)); - } } export default ServiceWorkerBridge;