From 8406bda019d59bd2f804e74474445a6be93c690f Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 8 Nov 2017 13:40:50 -0600 Subject: [PATCH 01/13] Replace editors with components --- .../app/frontend/controllers/editor.js | 351 ++++++++---------- .../app/frontend/models/app/component.js | 33 +- .../app/services/componentManager.js | 25 +- .../services/directives/views/editorMenu.js | 9 +- .../directives/views/permissionsModal.js | 3 +- .../frontend/directives/editor-menu.html.haml | 12 +- .../templates/frontend/editor.html.haml | 12 +- 7 files changed, 220 insertions(+), 225 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 8634475d1..20f80d8ca 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -20,24 +20,10 @@ angular.module('app.frontend') ctrl.noteDidChange(note, oldNote); } }); - - scope.$watch('ctrl.note.text', function(newText){ - if(!ctrl.note) { - return; - } - - // ignore this change if it originated from here - if(ctrl.changingTextFromEditor) { - ctrl.changingTextFromEditor = false; - return; - } - - ctrl.postNoteToExternalEditor(ctrl.note); - }) } } }) - .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager, storageManager) { + .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, themeManager, componentManager, storageManager) { this.componentManager = componentManager; this.componentStack = []; @@ -58,140 +44,25 @@ angular.module('app.frontend') this.loadTagsString(); }.bind(this)); - componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack"], activationHandler: function(component){ - - if(!component.active) { - return; - } - - if(component.area === "note-tags") { - this.tagsComponent = component; - } else { - // stack - if(!_.find(this.componentStack, component)) { - this.componentStack.push(component); - } - } - - $timeout(function(){ - var iframe = componentManager.iframeForComponent(component); - if(iframe) { - iframe.onload = function() { - componentManager.registerComponentWindow(component, iframe.contentWindow); - }.bind(this); - } - }.bind(this)); - - }.bind(this), contextRequestHandler: function(component){ - return this.note; - }.bind(this), actionHandler: function(component, action, data){ - if(action === "set-size") { - var setSize = function(element, size) { - var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`; - var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`; - element.setAttribute("style", `width:${widthString}; height:${heightString}; `); - } - - if(data.type === "content") { - var iframe = componentManager.iframeForComponent(component); - var width = data.width; - var height = data.height; - iframe.width = width; - iframe.height = height; - - setSize(iframe, data); - } else { - if(component.area == "note-tags") { - var container = document.getElementById("note-tags-component-container"); - setSize(container, data); - } else { - var container = document.getElementById("component-" + component.uuid); - setSize(container, data); - } - } - } - - else if(action === "associate-item") { - if(data.item.content_type == "Tag") { - var tag = modelManager.findItem(data.item.uuid); - this.addTag(tag); - } - } - - else if(action === "deassociate-item") { - var tag = modelManager.findItem(data.item.uuid); - this.removeTag(tag); - } - - }.bind(this)}); - - window.addEventListener("message", function(event){ - if(event.data.status) { - this.postNoteToExternalEditor(); - } else if(!event.data.api) { - // console.log("Received message", event.data); - var id = event.data.id; - var text = event.data.text; - var data = event.data.data; - - if(this.note.uuid === id) { - // to ignore $watch events - this.changingTextFromEditor = true; - this.note.text = text; - if(data) { - var changesMade = this.editor.setData(id, data); - if(changesMade) { - this.editor.setDirty(true); - } - } - this.changesMade(); - } - } - }.bind(this), false); - this.noteDidChange = function(note, oldNote) { this.setNote(note, oldNote); - for(var component of this.componentStack) { - componentManager.setEventFlowForComponent(component, component.isActiveForItem(this.note)); - } - componentManager.contextItemDidChangeInArea("note-tags"); - componentManager.contextItemDidChangeInArea("editor-stack"); + this.reloadComponentContext(); } this.setNote = function(note, oldNote) { - var currentEditor = this.editor; - this.editor = null; this.showExtensions = false; this.showMenu = false; this.loadTagsString(); - var setEditor = function(editor) { - this.editor = editor; - this.postNoteToExternalEditor(); - this.noteReady = true; - }.bind(this) - - var editor = this.editorForNote(note); - if(editor && !editor.systemEditor) { - // setting note to not ready will remove the editor from view in a flash, - // so we only want to do this if switching between external editors - this.noteReady = false; - } - if(editor) { - if(currentEditor !== editor) { - // switch after timeout, so that note data isnt posted to current editor - $timeout(function(){ - setEditor(editor); - }.bind(this)); - } else { - // switch immediately - setEditor(editor); - } - } else { - this.editor = null; - this.noteReady = true; + let associatedEditor = this.editorForNote(note); + if(associatedEditor) { + componentManager.activateComponent(associatedEditor); + } else if(this.editorComponent) { + componentManager.deactivateComponent(this.editorComponent); + this.editorComponent = null; } + this.noteReady = true; if(note.safeText().length == 0 && note.dummy) { this.focusTitle(100); @@ -206,62 +77,27 @@ angular.module('app.frontend') } } - this.selectedEditor = function(editor) { - this.showEditorMenu = false; - - if(this.editor && editor !== this.editor) { - this.editor.removeItemAsRelationship(this.note); - this.editor.setDirty(true); - } - - editor.addItemAsRelationship(this.note); - editor.setDirty(true); - - syncManager.sync(); - - this.editor = editor; - }.bind(this) - this.editorForNote = function(note) { - var editors = modelManager.itemsForContentType("SN|Editor"); + let editors = componentManager.componentsForArea("editor-editor"); for(var editor of editors) { - if(_.includes(editor.notes, note)) { + if(editor.isActiveForItem(note)) { return editor; } } - return _.find(editors, {default: true}); } - this.postDataToExternalEditor = function(data) { - var externalEditorElement = document.getElementById("editor-iframe"); - if(externalEditorElement) { - externalEditorElement.contentWindow.postMessage(data, '*'); + this.selectedEditor = function(editorComponent) { + this.showEditorMenu = false; + if(editorComponent) { + this.enableComponentForCurrentItem(editorComponent); + } else { + // Use plain system editor + if(this.editorComponent) { + this.disableComponentForCurrentItem(this.editorComponent); + } } - } - - function themeData() { - return { - themes: [themeManager.currentTheme ? themeManager.currentTheme.url : null] - } - } - - this.postThemeToExternalEditor = function() { - this.postDataToExternalEditor(themeData()) - } - - this.postNoteToExternalEditor = function() { - if(!this.editor) { - return; - } - - var data = { - text: this.note.text, - data: this.editor.dataForKey(this.note.uuid), - id: this.note.uuid, - } - _.merge(data, themeData()); - this.postDataToExternalEditor(data); - } + this.editorComponent = editorComponent; + }.bind(this) this.hasAvailableExtensions = function() { return extensionManager.extensionsInContextOfItem(this.note).length > 0; @@ -295,20 +131,16 @@ angular.module('app.frontend') if(success) { if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ - var status = "All changes saved"; - if(authManager.offline()) { - status += " (offline)"; - } this.saveError = false; this.syncTakingTooLong = false; - this.noteStatus = $sce.trustAsHtml(status); + this.showAllChangesSavedStatus(); }.bind(this), 200) } else { if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ this.saveError = true; this.syncTakingTooLong = false; - this.noteStatus = $sce.trustAsHtml("Error syncing
(changes saved offline)") + this.showErrorStatus(); }.bind(this), 200) } }.bind(this)); @@ -328,11 +160,26 @@ angular.module('app.frontend') if(saveTimeout) $timeout.cancel(saveTimeout); if(statusTimeout) $timeout.cancel(statusTimeout); saveTimeout = $timeout(function(){ - this.noteStatus = $sce.trustAsHtml("Saving..."); + this.showSavingStatus(); this.saveNote(); }.bind(this), 275) } + this.showSavingStatus = function() { + this.noteStatus = $sce.trustAsHtml("Saving..."); + } + + this.showAllChangesSavedStatus = function() { + var status = "All changes saved"; + if(authManager.offline()) { + status += " (offline)"; + } + this.noteStatus = $sce.trustAsHtml(status); + } + + this.showErrorStatus = function() { + this.noteStatus = $sce.trustAsHtml("Error syncing
(changes saved offline)") + } this.contentChanged = function() { this.changesMade(); @@ -388,7 +235,6 @@ angular.module('app.frontend') } this.clickedEditNote = function() { - this.editorMode = 'edit'; this.focusEditor(100); } @@ -454,18 +300,121 @@ angular.module('app.frontend') Components */ - let alertKey = "displayed-component-disable-alert"; + componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack", "editor-editor"], activationHandler: function(component){ - this.disableComponent = function(component) { - componentManager.disableComponentForItem(component, this.note); - componentManager.setEventFlowForComponent(component, false); - if(!storageManager.getItem(alertKey)) { + if(component.area === "note-tags") { + // Autocomplete Tags + this.tagsComponent = component.active ? component : null; + } else if(component.area == "editor-stack") { + // Stack + if(component.active) { + if(!_.find(this.componentStack, component)) { + this.componentStack.push(component); + } + } else { + _.pull(this.componentStack, component); + } + } else { + // Editor + if(component.active && this.note && component.isActiveForItem(this.note)) { + this.editorComponent = component; + } else { + this.editorComponent = null; + } + } + + if(component.active) { + $timeout(function(){ + var iframe = componentManager.iframeForComponent(component); + if(iframe) { + iframe.onload = function() { + componentManager.registerComponentWindow(component, iframe.contentWindow); + }.bind(this); + } + }.bind(this)); + } + + }.bind(this), contextRequestHandler: function(component){ + return this.note; + }.bind(this), actionHandler: function(component, action, data){ + if(action === "set-size") { + var setSize = function(element, size) { + var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`; + var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`; + element.setAttribute("style", `width:${widthString}; height:${heightString}; `); + } + + if(data.type === "content") { + var iframe = componentManager.iframeForComponent(component); + var width = data.width; + var height = data.height; + iframe.width = width; + iframe.height = height; + + setSize(iframe, data); + } else { + if(component.area == "note-tags") { + var container = document.getElementById("note-tags-component-container"); + setSize(container, data); + } else { + var container = document.getElementById("component-" + component.uuid); + setSize(container, data); + } + } + } + + else if(action === "associate-item") { + if(data.item.content_type == "Tag") { + var tag = modelManager.findItem(data.item.uuid); + this.addTag(tag); + } + } + + else if(action === "deassociate-item") { + var tag = modelManager.findItem(data.item.uuid); + this.removeTag(tag); + } + + else if(action === "save-items" || action === "save-success" || action == "save-error") { + if(data.items.map((item) => {return item.uuid}).includes(this.note.uuid)) { + if(action == "save-items") { + this.showSavingStatus(); + } else if(action == "save-success") { + $timeout(this.showAllChangesSavedStatus.bind(this), 200); + } else { + $timeout(this.showErrorStatus.bind(this), 200); + } + } + } + }.bind(this)}); + + this.reloadComponentContext = function() { + for(var component of this.componentStack) { + componentManager.setEventFlowForComponent(component, component.isActiveForItem(this.note)); + } + + componentManager.contextItemDidChangeInArea("note-tags"); + componentManager.contextItemDidChangeInArea("editor-stack"); + componentManager.contextItemDidChangeInArea("editor-editor"); + } + + this.enableComponentForCurrentItem = function(component) { + componentManager.activateComponent(component); + componentManager.associateComponentWithItem(component, this.note); + componentManager.setEventFlowForComponent(component, 1); + } + + let alertKey = "displayed-component-disable-alert"; + this.disableComponentForCurrentItem = function(component, showAlert) { + componentManager.disassociateComponentWithItem(component, this.note); + componentManager.setEventFlowForComponent(component, 0); + if(showAlert && !storageManager.getItem(alertKey)) { alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane."); storageManager.setItem(alertKey, true); } } - this.hasDisabledComponents = function() { + this.hasDisabledStackComponents = function() { for(var component of this.componentStack) { if(component.ignoreEvents) { return true; @@ -475,7 +424,7 @@ angular.module('app.frontend') return false; } - this.restoreDisabledComponents = function() { + this.restoreDisabledStackComponents = function() { var relevantComponents = this.componentStack.filter(function(component){ return component.ignoreEvents; }) diff --git a/app/assets/javascripts/app/frontend/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index c0108469c..d7fa7f48f 100644 --- a/app/assets/javascripts/app/frontend/models/app/component.js +++ b/app/assets/javascripts/app/frontend/models/app/component.js @@ -10,6 +10,10 @@ class Component extends Item { if(!this.disassociatedItemIds) { this.disassociatedItemIds = []; } + + if(!this.associatedItemIds) { + this.associatedItemIds = []; + } } mapContentToLocalProperties(content) { @@ -28,6 +32,9 @@ class Component extends Item { // items that have requested a component to be disabled in its context this.disassociatedItemIds = content.disassociatedItemIds || []; + + // items that have requested a component to be enabled in its context + this.associatedItemIds = content.associatedItemIds || []; } structureParams() { @@ -38,7 +45,8 @@ class Component extends Item { permissions: this.permissions, active: this.active, componentData: this.componentData, - disassociatedItemIds: this.disassociatedItemIds + disassociatedItemIds: this.disassociatedItemIds, + associatedItemIds: this.associatedItemIds, }; _.merge(params, super.structureParams()); @@ -53,7 +61,28 @@ class Component extends Item { return "SN|Component"; } + + /* + An associative component depends on being explicitly activated for a given item, compared to a dissaciative component, + which is enabled by default in areas unrelated to a certain item. + */ + static associativeAreas() { + return ["editor-editor"]; + } + + isAssociative() { + return Component.associativeAreas().includes(this.area); + } + + associateWithItem(item) { + this.associatedItemIds.push(item.uuid); + } + isActiveForItem(item) { - return this.disassociatedItemIds.indexOf(item.uuid) === -1; + if(this.isAssociative()) { + return this.associatedItemIds.indexOf(item.uuid) !== -1; + } else { + return this.disassociatedItemIds.indexOf(item.uuid) === -1; + } } } diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 026aeb79a..e0c51b000 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -11,7 +11,7 @@ class ComponentManager { this.contextStreamObservers = []; this.activeComponents = []; - // this.loggingEnabled = true; + this.loggingEnabled = true; this.permissionDialogs = []; @@ -140,9 +140,9 @@ class ComponentManager { return this.modelManager.itemsForContentType("SN|Component"); } - componentsForStack(stack) { + componentsForArea(area) { return this.components.filter(function(component){ - return component.area === stack; + return component.area === area; }) } @@ -219,7 +219,12 @@ class ComponentManager { _.merge(item.content, responseItem.content); item.setDirty(true); } - this.syncManager.sync(); + this.syncManager.sync((response) => { + // Allow handlers to be notified when a save begins and ends, to update the UI + var saveMessage = Object.assign({}, message); + saveMessage.action = response && response.error ? "save-error" : "save-success"; + this.handleMessage(component, saveMessage); + }); } for(let handler of this.handlers) { @@ -378,7 +383,7 @@ class ComponentManager { sendMessageToComponent(component, message) { if(component.ignoreEvents && message.action !== "component-registered") { if(this.loggingEnabled) { - console.log("Component disabled for current item, not sending any messages."); + console.log("Component disabled for current item, not sending any messages.", component.name); } return; } @@ -466,15 +471,23 @@ class ComponentManager { return component.active; } - disableComponentForItem(component, item) { + disassociateComponentWithItem(component, item) { if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) { return; } + _.pull(component.associatedItemIds, item.uuid); component.disassociatedItemIds.push(item.uuid); component.setDirty(true); this.syncManager.sync(); } + associateComponentWithItem(component, item) { + _.pull(component.disassociatedItemIds, item.uuid); + component.associatedItemIds.push(item.uuid); + component.setDirty(true); + this.syncManager.sync(); + } + enableComponentsForItem(components, item) { for(var component of components) { _.pull(component.disassociatedItemIds, item.uuid); diff --git a/app/assets/javascripts/app/services/directives/views/editorMenu.js b/app/assets/javascripts/app/services/directives/views/editorMenu.js index c10ff2c20..008c5fadc 100644 --- a/app/assets/javascripts/app/services/directives/views/editorMenu.js +++ b/app/assets/javascripts/app/services/directives/views/editorMenu.js @@ -9,14 +9,17 @@ class EditorMenu { }; } - controller($scope, editorManager) { + controller($scope, componentManager) { 'ngInject'; $scope.formData = {}; - $scope.editorManager = editorManager; + + $scope.editors = componentManager.componentsForArea("editor-editor"); $scope.selectEditor = function($event, editor) { - editor.conflict_of = null; // clear conflict if applicable + if(editor) { + editor.conflict_of = null; // clear conflict if applicable + } $scope.callback()(editor); } diff --git a/app/assets/javascripts/app/services/directives/views/permissionsModal.js b/app/assets/javascripts/app/services/directives/views/permissionsModal.js index ce72dfbdb..e2a1f0a07 100644 --- a/app/assets/javascripts/app/services/directives/views/permissionsModal.js +++ b/app/assets/javascripts/app/services/directives/views/permissionsModal.js @@ -57,7 +57,8 @@ class PermissionsModal { } else if(permission.name === "stream-context-item") { var mapping = { "editor-stack" : "working note", - "note-tags" : "working note" + "note-tags" : "working note", + "editor-editor": "working note" } return "Access to " + mapping[$scope.component.area]; } diff --git a/app/assets/templates/frontend/directives/editor-menu.html.haml b/app/assets/templates/frontend/directives/editor-menu.html.haml index b6165f264..b028399c7 100644 --- a/app/assets/templates/frontend/directives/editor-menu.html.haml +++ b/app/assets/templates/frontend/directives/editor-menu.html.haml @@ -1,17 +1,17 @@ %ul.dropdown-menu.sectioned-menu .header - .title System Editors + .title System Editor %ul - %li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, editor)"} - %span.pull-left.mr-10{"ng-if" => "selectedEditor === editor"} ✓ - %label.menu-item-title.pull-left {{editor.name}} + %li.menu-item{"ng-click" => "selectEditor($event, null)"} + %span.pull-left.mr-10{"ng-if" => "selectedEditor == null"} ✓ + %label.menu-item-title.pull-left Plain - %div{"ng-if" => "editorManager.externalEditors.length > 0"} + %div{"ng-if" => "editors.length > 0"} .header .title External Editors .subtitle Can access your current note decrypted. %ul - %li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"} + %li.menu-item{"ng-repeat" => "editor in editors", "ng-click" => "selectEditor($event, editor)"} %strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy %label.menu-item-title {{editor.name}} diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index 3da918fdd..a33cb2a5a 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -34,21 +34,21 @@ %i.icon.ion-arrow-expand Toggle Fullscreen - %li{"ng-if" => "ctrl.hasDisabledComponents()"} - %label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components + %li{"ng-if" => "ctrl.hasDisabledStackComponents()"} + %label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledStackComponents()"} Restore Disabled Components %li{"ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"} %label{"ng-click" => "ctrl.showEditorMenu = !ctrl.showEditorMenu; ctrl.showMenu = false; ctrl.showExtensions = false;"} Editor - %editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.selectedEditor", "selected-editor" => "ctrl.editor"} + %editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.selectedEditor", "selected-editor" => "ctrl.editorComponent"} %li{"ng-class" => "{'selected' : ctrl.showExtensions}", "ng-if" => "ctrl.hasAvailableExtensions()", "click-outside" => "ctrl.showExtensions = false;", "is-open" => "ctrl.showExtensions"} %label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions %contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"} .editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} - %iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;"} + %iframe#editor-iframe{"ng-if" => "ctrl.editorComponent && ctrl.editorComponent.active", "ng-src" => "{{ctrl.editorComponent.url | trusted}}", "data-component-id" => "{{ctrl.editorComponent.uuid}}", "frameBorder" => "0", "style" => "width: 100%;"} Loading - %textarea.editable#note-text-editor{"ng-if" => "!ctrl.editor || ctrl.editor.systemEditor", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text", + %textarea.editable#note-text-editor{"ng-if" => "!ctrl.editorComponent", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text", "ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()", "dir" => "auto"} {{ctrl.onSystemEditorLoad()}} @@ -58,5 +58,5 @@ #editor-pane-component-stack .component.component-stack-border{"ng-repeat" => "component in ctrl.componentStack", "ng-if" => "component.active", "ng-show" => "!component.ignoreEvents", "id" => "{{'component-' + component.uuid}}", "ng-mouseover" => "component.showExit = true", "ng-mouseleave" => "component.showExit = false"} - .exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponent(component)"} × + .exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponentForCurrentItem(component, true)"} × %iframe#note-tags-iframe{"ng-src" => "{{component.url | trusted}}", "frameBorder" => "0", "sandbox" => "allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-modals", "data-component-id" => "{{component.uuid}}"} From 092ece56a34c3a9ccef766ee597abf3d9b49f8ff Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 8 Nov 2017 21:06:29 -0600 Subject: [PATCH 02/13] Updates --- .../app/services/componentManager.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index e0c51b000..5f166eb93 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -122,6 +122,7 @@ class ComponentManager { } sendItemsInReply(component, items, message) { + if(this.loggingEnabled) {console.log("Web|componentManager|sendItemsInReply", component, items, message)}; var response = {items: {}}; var mapped = items.map(function(item) { return this.jsonForItem(item); @@ -132,6 +133,7 @@ class ComponentManager { } sendContextItemInReply(component, item, originalMessage) { + if(this.loggingEnabled) {console.log("Web|componentManager|sendContextItemInReply", component, item, originalMessage)}; var response = {item: this.jsonForItem(item)}; this.replyToMessage(component, originalMessage, response); } @@ -387,6 +389,9 @@ class ComponentManager { } return; } + if(this.loggingEnabled) { + console.log("Web|sendMessageToComponent", component, message); + } component.window.postMessage(message, "*"); } @@ -420,7 +425,9 @@ class ComponentManager { this.syncManager.sync(); } - this.activeComponents.push(component); + if(!this.activeComponents.includes(component)) { + this.activeComponents.push(component); + } } registerHandler(handler) { @@ -429,6 +436,15 @@ class ComponentManager { // Called by other views when the iframe is ready registerComponentWindow(component, componentWindow) { + if(component.window === componentWindow) { + if(this.loggingEnabled) { + console.log("Web|componentManager", "attempting to re-register same component window.") + } + } + + if(this.loggingEnabled) { + console.log("Web|componentManager|registerComponentWindow", component); + } component.window = componentWindow; component.sessionKey = Neeto.crypto.generateUUID(); this.sendMessageToComponent(component, {action: "component-registered", sessionKey: component.sessionKey, componentData: component.componentData}); From 8767f1c5ec9ae9854ca36ea823f77650901b0dbf Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 8 Nov 2017 22:21:19 -0600 Subject: [PATCH 03/13] Deactivate old editor on new select --- .../app/frontend/controllers/editor.js | 16 ++++++++++++---- .../javascripts/app/services/componentManager.js | 11 ++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 20f80d8ca..558172e8d 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -55,13 +55,18 @@ angular.module('app.frontend') this.loadTagsString(); let associatedEditor = this.editorForNote(note); - if(associatedEditor) { - componentManager.activateComponent(associatedEditor); - } else if(this.editorComponent) { + if(this.editorComponent && this.editorComponent != associatedEditor) { + // Deactivate old editor componentManager.deactivateComponent(this.editorComponent); - this.editorComponent = null; } + // Activate new editor if it's different from the one currently activated + if(associatedEditor && associatedEditor != this.editorComponent) { + componentManager.activateComponent(associatedEditor); + } + + this.editorComponent = associatedEditor; + this.noteReady = true; if(note.safeText().length == 0 && note.dummy) { @@ -93,7 +98,10 @@ angular.module('app.frontend') } else { // Use plain system editor if(this.editorComponent) { + // This disassociates the editor from the note, but the component itself still needs to be deactivated this.disableComponentForCurrentItem(this.editorComponent); + // Now deactivate the component + componentManager.deactivateComponent(this.editorComponent); } } this.editorComponent = editorComponent; diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 5f166eb93..330790d59 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -11,7 +11,7 @@ class ComponentManager { this.contextStreamObservers = []; this.activeComponents = []; - this.loggingEnabled = true; + // this.loggingEnabled = true; this.permissionDialogs = []; @@ -62,10 +62,11 @@ class ComponentManager { name: "stream-context-item" } ]; + for(let observer of this.contextStreamObservers) { this.runWithPermissions(observer.component, requiredContextPermissions, observer.originalMessage.permissions, function(){ for(let handler of this.handlers) { - if(handler.areas.includes(observer.component.area) === false) { + if(!handler.areas.includes(observer.component.area)) { continue; } var itemInContext = handler.contextRequestHandler(observer.component); @@ -78,7 +79,6 @@ class ComponentManager { } }.bind(this)) } - }.bind(this)) } @@ -214,6 +214,11 @@ class ComponentManager { else if(message.action === "save-items") { var responseItems = message.data.items; + + /* + We map the items here because modelManager is what updatese the UI. If you were to instead get the items directly, + this would update them server side via sync, but would never make its way back to the UI. + */ var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems); for(var item of localItems) { From 4e39a5e1b9c321a075a9af85ef913395c6bad6b5 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 9 Nov 2017 17:20:13 -0600 Subject: [PATCH 04/13] Migrate editors to components, disable previous editor when selecting new one --- .../app/frontend/controllers/editor.js | 17 +-- .../app/frontend/controllers/home.js | 2 +- .../app/services/componentManager.js | 17 ++- .../directives/views/globalExtensionsMenu.js | 25 +---- .../javascripts/app/services/editorManager.js | 100 ------------------ .../app/services/migrationManager.js | 62 +++++++++++ .../global-extensions-menu.html.haml | 21 +--- 7 files changed, 90 insertions(+), 154 deletions(-) delete mode 100644 app/assets/javascripts/app/services/editorManager.js create mode 100644 app/assets/javascripts/app/services/migrationManager.js diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 558172e8d..d560a0abf 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -93,17 +93,18 @@ angular.module('app.frontend') this.selectedEditor = function(editorComponent) { this.showEditorMenu = false; + + if(this.editorComponent && this.editorComponent !== editorComponent) { + // This disassociates the editor from the note, but the component itself still needs to be deactivated + this.disableComponentForCurrentItem(this.editorComponent); + // Now deactivate the component + componentManager.deactivateComponent(this.editorComponent); + } + if(editorComponent) { this.enableComponentForCurrentItem(editorComponent); - } else { - // Use plain system editor - if(this.editorComponent) { - // This disassociates the editor from the note, but the component itself still needs to be deactivated - this.disableComponentForCurrentItem(this.editorComponent); - // Now deactivate the component - componentManager.deactivateComponent(this.editorComponent); - } } + this.editorComponent = editorComponent; }.bind(this) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index f72cedcfd..830479df3 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -1,6 +1,6 @@ angular.module('app.frontend') .controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, - dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager) { + dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) { storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession()); diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 330790d59..b1692592d 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -148,6 +148,12 @@ class ComponentManager { }) } + componentForUrl(url) { + return this.components.filter(function(component){ + return component.url === url; + })[0]; + } + componentForSessionKey(key) { return _.find(this.components, {sessionKey: key}); } @@ -493,18 +499,27 @@ class ComponentManager { } disassociateComponentWithItem(component, item) { + _.pull(component.associatedItemIds, item.uuid); + if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) { return; } - _.pull(component.associatedItemIds, item.uuid); + component.disassociatedItemIds.push(item.uuid); + component.setDirty(true); this.syncManager.sync(); } associateComponentWithItem(component, item) { _.pull(component.disassociatedItemIds, item.uuid); + + if(component.associatedItemIds.includes(item.uuid)) { + return; + } + component.associatedItemIds.push(item.uuid); + component.setDirty(true); this.syncManager.sync(); } diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index fa29c8d4d..42bfa6f00 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -7,14 +7,13 @@ class GlobalExtensionsMenu { }; } - controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager, componentManager) { + controller($scope, extensionManager, syncManager, modelManager, themeManager, componentManager) { 'ngInject'; $scope.formData = {}; $scope.extensionManager = extensionManager; $scope.themeManager = themeManager; - $scope.editorManager = editorManager; $scope.componentManager = componentManager; $scope.serverExtensions = modelManager.itemsForContentType("SF|Extension"); @@ -120,23 +119,6 @@ class GlobalExtensionsMenu { } - // Editors - - $scope.deleteEditor = function(editor) { - if(confirm("Are you sure you want to delete this editor?")) { - editorManager.deleteEditor(editor); - } - } - - $scope.setDefaultEditor = function(editor) { - editorManager.setDefaultEditor(editor); - } - - $scope.removeDefaultEditor = function(editor) { - editorManager.removeDefaultEditor(editor); - } - - // Components $scope.revokePermissions = function(component) { @@ -219,11 +201,6 @@ class GlobalExtensionsMenu { } } - $scope.handleEditorLink = function(link, completion) { - editorManager.addNewEditorFromURL(link); - completion(); - } - } } diff --git a/app/assets/javascripts/app/services/editorManager.js b/app/assets/javascripts/app/services/editorManager.js deleted file mode 100644 index 1b9ee579b..000000000 --- a/app/assets/javascripts/app/services/editorManager.js +++ /dev/null @@ -1,100 +0,0 @@ -class EditorManager { - - constructor($rootScope, modelManager, syncManager) { - this.syncManager = syncManager; - this.modelManager = modelManager; - - this.editorType = "SN|Editor"; - this._systemEditor = { - systemEditor: true, - name: "Plain" - } - - $rootScope.$on("sync:completed", function(){ - // we want to wait for sync completion before creating a syncable system editor - // we need to sync the system editor so that we can assign note preferences to it - // that is, when a user selects Plain for a note, we need to remember that - if(this.systemEditor.uuid) { - return; - } - - var liveSysEditor = _.find(this.allEditors, {systemEditor: true}); - if(liveSysEditor) { - this._systemEditor = liveSysEditor; - } else { - this._systemEditor = modelManager.createItem({ - content_type: this.editorType, - systemEditor: true, - name: "Plain" - }) - modelManager.addItem(this._systemEditor); - this._systemEditor.setDirty(true); - syncManager.sync(); - } - }.bind(this)) - } - - get allEditors() { - return this.modelManager.itemsForContentType(this.editorType); - } - - get externalEditors() { - return this.allEditors.filter(function(editor){ - return !editor.systemEditor; - }) - } - - get systemEditors() { - return [this.systemEditor]; - } - - get systemEditor() { - return this._systemEditor; - } - - get defaultEditor() { - return _.find(this.externalEditors, {default: true}); - } - - editorForUrl(url) { - return this.externalEditors.filter(function(editor){return editor.url == url})[0]; - } - - setDefaultEditor(editor) { - var defaultEditor = this.defaultEditor; - if(defaultEditor) { - defaultEditor.default = false; - defaultEditor.setDirty(true); - } - editor.default = true; - editor.setDirty(true); - this.syncManager.sync(); - } - - removeDefaultEditor(editor) { - editor.default = false; - editor.setDirty(true); - this.syncManager.sync(); - } - - addNewEditorFromURL(url) { - var name = getParameterByName("name", url); - var editor = this.modelManager.createItem({ - content_type: this.editorType, - url: url, - name: name - }) - - this.modelManager.addItem(editor); - editor.setDirty(true); - this.syncManager.sync(); - } - - deleteEditor(editor) { - this.modelManager.setItemToBeDeleted(editor); - this.syncManager.sync(); - } - -} - -angular.module('app.frontend').service('editorManager', EditorManager); diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/app/services/migrationManager.js new file mode 100644 index 000000000..b7f252727 --- /dev/null +++ b/app/assets/javascripts/app/services/migrationManager.js @@ -0,0 +1,62 @@ +class MigrationManager { + + constructor($rootScope, modelManager, syncManager, componentManager) { + this.$rootScope = $rootScope; + this.modelManager = modelManager; + this.syncManager = syncManager; + this.componentManager = componentManager; + + this.migrators = []; + + this.addEditorToComponentMigrator(); + + this.modelManager.addItemSyncObserver("migration-manager", "*", (allItems, validItems, deletedItems) => { + for(var migrator of this.migrators) { + var items = allItems.filter((item) => {return item.content_type == migrator.content_type}); + if(items.length > 0) { + migrator.handler(items); + } + } + }); + } + + /* + Migrate SN|Editor to SN|Component. Editors are deprecated as of November 2017. Editors using old APIs must + convert to using the new component API. + */ + + addEditorToComponentMigrator() { + this.migrators.push({ + content_type: "SN|Editor", + + handler: (editors) => { + // Convert editors to components + for(var editor of editors) { + // If there's already a component for this url, then skip this editor + if(editor.url && !this.componentManager.componentForUrl(editor.url)) { + var component = this.modelManager.createItem({ + content_type: "SN|Component", + url: editor.url, + name: editor.name, + area: "editor-editor" + }) + component.setAppDataItem("data", editor.data); + component.setDirty(true); + this.modelManager.addItem(component); + console.log("Created component", component, "From editor", editor); + } + } + + for(let editor of editors) { + editor.setItemToBeDeleted(); + } + + this.syncManager.sync(); + } + }) + } + + +} + +angular.module('app.frontend').service('migrationManager', MigrationManager); diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 3a55fab07..4183d255b 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -4,7 +4,7 @@ .float-group.h20 %h1.tinted.pull-left Extensions %a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard - %div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"} + %div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !componentManager.components.length"} %p Customize your experience with editors, themes, and actions. .tinted-box.mt-10 %h3 Available as part of the Extended subscription. @@ -34,25 +34,6 @@ %p.small.selectable.wrap{"ng-if" => "theme.showLink"} {{theme.url}} - %div{"ng-if" => "editorManager.externalEditors.length > 0"} - .header.container.section-margin - %h2 Editors - %p{"style" => "margin-top: 3px;"} Choose "Editor" in the note menu to use an editor for a specific note. - %ul - %li{"ng-repeat" => "editor in editorManager.externalEditors | orderBy: 'name'", "ng-click" => "clickedExtension(editor)"} - .container - %strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy - %h3 - %input.bold{"ng-if" => "editor.rename", "ng-model" => "editor.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(editor);", "mb-autofocus" => "true", "should-focus" => "true"} - %span{"ng-if" => "!editor.rename"} {{editor.name}} - %div.mt-5{"ng-if" => "editor.showDetails"} - .link-group - %a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default - %a.tinted{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default - %a{"ng-click" => "renameExtension(editor); $event.stopPropagation();"} Rename - %a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link - %a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete - .wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}} %div{"ng-if" => "extensionManager.extensions.length"} .header.container.section-margin From d9b20c03a8098735f31a531e44fe2e74f043dcd7 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 9 Nov 2017 20:57:16 -0600 Subject: [PATCH 05/13] Component clientData --- .../app/frontend/models/api/item.js | 18 +++++--- .../app/services/componentManager.js | 44 ++++++++++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/api/item.js b/app/assets/javascripts/app/frontend/models/api/item.js index af65d5aa3..1c3dcff1d 100644 --- a/app/assets/javascripts/app/frontend/models/api/item.js +++ b/app/assets/javascripts/app/frontend/models/api/item.js @@ -151,17 +151,17 @@ class Item { App Data */ - setAppDataItem(key, value) { - var data = this.appData[AppDomain]; + setDomainDataItem(key, value, domain) { + var data = this.appData[domain]; if(!data) { data = {} } data[key] = value; - this.appData[AppDomain] = data; + this.appData[domain] = data; } - getAppDataItem(key) { - var data = this.appData[AppDomain]; + getDomainDataItem(key, domain) { + var data = this.appData[domain]; if(data) { return data[key]; } else { @@ -169,6 +169,14 @@ class Item { } } + setAppDataItem(key, value) { + this.setDomainDataItem(key, value, AppDomain); + } + + getAppDataItem(key) { + return this.getDomainDataItem(key, AppDomain); + } + get pinned() { return this.getAppDataItem("pinned"); } diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index b1692592d..da97f6231 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -1,5 +1,8 @@ class ComponentManager { + /* This domain will be used to save context item client data */ + let ClientDataDomain = "org.standardnotes.sn.components"; + constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) { this.$compile = $compile; this.$rootScope = $rootScope; @@ -115,9 +118,11 @@ class ComponentManager { } } - jsonForItem(item) { + jsonForItem(item, component) { var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted}; params.content = item.createContentJSONFromProperties(); + params.clientData = item.getDomainDataItem(component.url, ClientDataDomain); + this.removePrivatePropertiesFromResponseItems([params]); return params; } @@ -125,7 +130,7 @@ class ComponentManager { if(this.loggingEnabled) {console.log("Web|componentManager|sendItemsInReply", component, items, message)}; var response = {items: {}}; var mapped = items.map(function(item) { - return this.jsonForItem(item); + return this.jsonForItem(item, component); }.bind(this)); response.items = mapped; @@ -134,7 +139,7 @@ class ComponentManager { sendContextItemInReply(component, item, originalMessage) { if(this.loggingEnabled) {console.log("Web|componentManager|sendContextItemInReply", component, item, originalMessage)}; - var response = {item: this.jsonForItem(item)}; + var response = {item: this.jsonForItem(item, component)}; this.replyToMessage(component, originalMessage, response); } @@ -180,6 +185,8 @@ class ComponentManager { create-item delete-items set-component-data + save-context-client-data + get-context-client-data */ if(message.action === "stream-items") { @@ -210,17 +217,24 @@ class ComponentManager { } else if(message.action === "create-item") { - var item = this.modelManager.createItem(message.data.item); + var responseItem = message.data.item; + this.removePrivatePropertiesFromResponseItems([responseItem]); + var item = this.modelManager.createItem(responseItem); + if(responseItem.clientData) { + item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain); + } this.modelManager.addItem(item); this.modelManager.resolveReferencesForItem(item); item.setDirty(true); this.syncManager.sync(); - this.replyToMessage(component, message, {item: this.jsonForItem(item)}) + this.replyToMessage(component, message, {item: this.jsonForItem(item, component)}) } else if(message.action === "save-items") { var responseItems = message.data.items; + this.removePrivatePropertiesFromResponseItems(responseItems); + /* We map the items here because modelManager is what updatese the UI. If you were to instead get the items directly, this would update them server side via sync, but would never make its way back to the UI. @@ -230,6 +244,9 @@ class ComponentManager { for(var item of localItems) { var responseItem = _.find(responseItems, {uuid: item.uuid}); _.merge(item.content, responseItem.content); + if(responseItem.clientData) { + item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain); + } item.setDirty(true); } this.syncManager.sync((response) => { @@ -249,6 +266,21 @@ class ComponentManager { } } + removePrivatePropertiesFromResponseItems(responseItems) { + // Don't allow component to overwrite these properties. + let privateProperties = ["appData"]; + for(var responseItem of responseItems) { + + // Do not pass in actual items here, otherwise that would be destructive. + // Instead, generic JS/JSON objects should be passed. + console.assert(typeof responseItem.setDirty !== 'function'); + + for(var prop of privateProperties) { + delete responseItem[prop]; + } + } + } + handleStreamItemsMessage(component, message) { var requiredPermissions = [ { @@ -506,7 +538,7 @@ class ComponentManager { } component.disassociatedItemIds.push(item.uuid); - + component.setDirty(true); this.syncManager.sync(); } From 6f2c8f1970c2bd9f543ad0ec72b30544df0ef1db Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 10 Nov 2017 11:16:09 -0600 Subject: [PATCH 06/13] Fixes --- app/assets/javascripts/app/frontend/controllers/editor.js | 4 ---- app/assets/javascripts/app/services/componentManager.js | 8 ++++---- app/assets/javascripts/app/services/migrationManager.js | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index d560a0abf..054b067ec 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -28,10 +28,6 @@ angular.module('app.frontend') this.componentManager = componentManager; this.componentStack = []; - $rootScope.$on("theme-changed", function(){ - this.postThemeToExternalEditor(); - }.bind(this)) - $rootScope.$on("sync:taking-too-long", function(){ this.syncTakingTooLong = true; }.bind(this)); diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index da97f6231..4ce4bdc7b 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -1,7 +1,7 @@ -class ComponentManager { +/* This domain will be used to save context item client data */ +let ClientDataDomain = "org.standardnotes.sn.components"; - /* This domain will be used to save context item client data */ - let ClientDataDomain = "org.standardnotes.sn.components"; +class ComponentManager { constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) { this.$compile = $compile; @@ -121,7 +121,7 @@ class ComponentManager { jsonForItem(item, component) { var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted}; params.content = item.createContentJSONFromProperties(); - params.clientData = item.getDomainDataItem(component.url, ClientDataDomain); + params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {}; this.removePrivatePropertiesFromResponseItems([params]); return params; } diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/app/services/migrationManager.js index b7f252727..527c692bf 100644 --- a/app/assets/javascripts/app/services/migrationManager.js +++ b/app/assets/javascripts/app/services/migrationManager.js @@ -48,7 +48,7 @@ class MigrationManager { } for(let editor of editors) { - editor.setItemToBeDeleted(); + this.modelManager.setItemToBeDeleted(editor); } this.syncManager.sync(); From fc06abb1fecb04480ad4c469144d7ca3dcedfd4b Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 11 Nov 2017 13:23:12 -0600 Subject: [PATCH 07/13] Wip --- .../app/services/componentManager.js | 5 +++-- .../javascripts/app/services/modelManager.js | 22 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 4ce4bdc7b..031035b8a 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -122,6 +122,7 @@ class ComponentManager { var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted}; params.content = item.createContentJSONFromProperties(); params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {}; + params.isMetadataUpdate = item.lastTouchSaved; this.removePrivatePropertiesFromResponseItems([params]); return params; } @@ -236,10 +237,10 @@ class ComponentManager { this.removePrivatePropertiesFromResponseItems(responseItems); /* - We map the items here because modelManager is what updatese the UI. If you were to instead get the items directly, + We map the items here because modelManager is what updates the UI. If you were to instead get the items directly, this would update them server side via sync, but would never make its way back to the UI. */ - var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems); + var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, {dontNotifyObservers: true}); for(var item of localItems) { var responseItem = _.find(responseItems, {uuid: item.uuid}); diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 20ee7fa80..7d8e1df9f 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -96,11 +96,11 @@ class ModelManager { return tag; } - mapResponseItemsToLocalModels(items) { - return this.mapResponseItemsToLocalModelsOmittingFields(items, null); + mapResponseItemsToLocalModels(items, options) { + return this.mapResponseItemsToLocalModelsOmittingFields(items, null, options); } - mapResponseItemsToLocalModelsOmittingFields(items, omitFields) { + mapResponseItemsToLocalModelsOmittingFields(items, omitFields, options) { var models = [], processedObjects = [], modelsToNotifyObserversOf = []; // first loop should add and process items @@ -136,6 +136,14 @@ class ModelManager { item = this.createItem(json_obj); } + /* If content is being omitted from the json_obj, this means this is a metadata save only. + This happens by the sync manager on sync completion when processing saved items to update + their meta fields, like updated_at that comes from the server. We omit content in such a case + because content may be outdated from the time a sync begins to when it completes (user performed action in between). + So we will only ever update content from a remote source when it is retrieved by the server (serverResponse.retrieved_items) + */ + item.lastTouchSaved = omitFields && omitFields.includes("content"); + this.addItem(item); modelsToNotifyObserversOf.push(item); @@ -151,7 +159,13 @@ class ModelManager { } } - this.notifySyncObserversOfModels(modelsToNotifyObserversOf); + /* Sometimes, a controller will want to map incoming state to the UI, but without yet notifiny all observers of an item change + Particulary, the componentManager sets dontNotifyObservers to true when it receives an update from a component and wnats + to update the remaining UI, but without receiving a (useless) syncObserverCallback immediately. + */ + if(!(options && options.dontNotifyObservers)) { + this.notifySyncObserversOfModels(modelsToNotifyObserversOf); + } return models; } From 909d66067b3f747c92f4f2540deabc305a0c7dd4 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 13 Nov 2017 10:12:07 -0600 Subject: [PATCH 08/13] ModelManager mapping sources --- .../app/services/componentManager.js | 34 ++++++++++++++----- .../services/directives/views/accountMenu.js | 2 +- .../app/services/extensionManager.js | 2 +- .../javascripts/app/services/modelManager.js | 33 +++++++----------- .../javascripts/app/services/syncManager.js | 10 +++--- 5 files changed, 45 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 031035b8a..c22cdbd85 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -31,7 +31,15 @@ class ComponentManager { this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data); }.bind(this), false); - this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems) { + this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems, source) { + + /* If the source of these new or updated items is from a Component itself saving items, we don't need to notify + components again of the same item. Regarding notifying other components than the issuing component, other mapping sources + will take care of that, like ModelManager.MappingSourceRemoteSaved + */ + if(source == ModelManager.MappingSourceComponentRetrieved) { + return; + } var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" }); for(var component of syncedComponents) { @@ -76,7 +84,7 @@ class ComponentManager { if(itemInContext) { var matchingItem = _.find(allItems, {uuid: itemInContext.uuid}); if(matchingItem) { - this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage); + this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source); } } } @@ -118,29 +126,37 @@ class ComponentManager { } } - jsonForItem(item, component) { + jsonForItem(item, component, source) { var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted}; params.content = item.createContentJSONFromProperties(); params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {}; - params.isMetadataUpdate = item.lastTouchSaved; + + /* This means the this function is being triggered through a remote Saving response, which should not update + actual local content values. The reason is, Save responses may be delayed, and a user may have changed some values + in between the Save was initiated, and the time it completes. So we only want to update actual content values (and not just metadata) + when its another source, like ModelManager.MappingSourceRemoteRetrieved. + */ + if(source && source == ModelManager.MappingSourceRemoteSaved) { + params.isMetadataUpdate = true; + } this.removePrivatePropertiesFromResponseItems([params]); return params; } - sendItemsInReply(component, items, message) { + sendItemsInReply(component, items, message, source) { if(this.loggingEnabled) {console.log("Web|componentManager|sendItemsInReply", component, items, message)}; var response = {items: {}}; var mapped = items.map(function(item) { - return this.jsonForItem(item, component); + return this.jsonForItem(item, component, source); }.bind(this)); response.items = mapped; this.replyToMessage(component, message, response); } - sendContextItemInReply(component, item, originalMessage) { + sendContextItemInReply(component, item, originalMessage, source) { if(this.loggingEnabled) {console.log("Web|componentManager|sendContextItemInReply", component, item, originalMessage)}; - var response = {item: this.jsonForItem(item, component)}; + var response = {item: this.jsonForItem(item, component, source)}; this.replyToMessage(component, originalMessage, response); } @@ -240,7 +256,7 @@ class ComponentManager { We map the items here because modelManager is what updates the UI. If you were to instead get the items directly, this would update them server side via sync, but would never make its way back to the UI. */ - var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, {dontNotifyObservers: true}); + var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved); for(var item of localItems) { var responseItem = _.find(responseItems, {uuid: item.uuid}); diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 68806f322..b8a2b7ad6 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -242,7 +242,7 @@ class AccountMenu { $scope.importJSONData = function(data, password, callback) { var onDataReady = function(errorCount) { - var items = modelManager.mapResponseItemsToLocalModels(data.items); + var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport); items.forEach(function(item){ item.setDirty(true); item.deleted = false; diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 8a84d2188..ff7b66cd1 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -161,7 +161,7 @@ class ExtensionManager { action.error = false; var items = response.items || [response.item]; EncryptionHelper.decryptMultipleItems(items, this.authManager.keys()); - items = this.modelManager.mapResponseItemsToLocalModels(items); + items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved); for(var item of items) { item.setDirty(true); } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 7d8e1df9f..41861d037 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -1,6 +1,13 @@ class ModelManager { constructor(storageManager) { + ModelManager.MappingSourceRemoteRetrieved = "MappingSourceRemoteRetrieved"; + ModelManager.MappingSourceRemoteSaved = "MappingSourceRemoteSaved"; + ModelManager.MappingSourceLocalRetrieved = "MappingSourceLocalRetrieved"; + ModelManager.MappingSourceComponentRetrieved = "MappingSourceComponentRetrieved"; + ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */ + ModelManager.MappingSourceFileImport = "MappingSourceFileImport"; + this.storageManager = storageManager; this.notes = []; this.tags = []; @@ -96,11 +103,11 @@ class ModelManager { return tag; } - mapResponseItemsToLocalModels(items, options) { - return this.mapResponseItemsToLocalModelsOmittingFields(items, null, options); + mapResponseItemsToLocalModels(items, source) { + return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source); } - mapResponseItemsToLocalModelsOmittingFields(items, omitFields, options) { + mapResponseItemsToLocalModelsOmittingFields(items, omitFields, source) { var models = [], processedObjects = [], modelsToNotifyObserversOf = []; // first loop should add and process items @@ -136,14 +143,6 @@ class ModelManager { item = this.createItem(json_obj); } - /* If content is being omitted from the json_obj, this means this is a metadata save only. - This happens by the sync manager on sync completion when processing saved items to update - their meta fields, like updated_at that comes from the server. We omit content in such a case - because content may be outdated from the time a sync begins to when it completes (user performed action in between). - So we will only ever update content from a remote source when it is retrieved by the server (serverResponse.retrieved_items) - */ - item.lastTouchSaved = omitFields && omitFields.includes("content"); - this.addItem(item); modelsToNotifyObserversOf.push(item); @@ -159,18 +158,12 @@ class ModelManager { } } - /* Sometimes, a controller will want to map incoming state to the UI, but without yet notifiny all observers of an item change - Particulary, the componentManager sets dontNotifyObservers to true when it receives an update from a component and wnats - to update the remaining UI, but without receiving a (useless) syncObserverCallback immediately. - */ - if(!(options && options.dontNotifyObservers)) { - this.notifySyncObserversOfModels(modelsToNotifyObserversOf); - } + this.notifySyncObserversOfModels(modelsToNotifyObserversOf, source); return models; } - notifySyncObserversOfModels(models) { + notifySyncObserversOfModels(models, source) { for(var observer of this.itemSyncObservers) { var allRelevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"}); var validItems = [], deletedItems = []; @@ -183,7 +176,7 @@ class ModelManager { } if(allRelevantItems.length > 0) { - observer.callback(allRelevantItems, validItems, deletedItems); + observer.callback(allRelevantItems, validItems, deletedItems, source); } } } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index cac99a8dd..8e5bc90e6 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -47,7 +47,7 @@ class SyncManager { loadLocalItems(callback) { var params = this.storageManager.getAllModels(function(items){ - var items = this.handleItemsResponse(items, null); + var items = this.handleItemsResponse(items, null, ModelManager.MappingSourceLocalRetrieved); Item.sortItemsByDate(items); callback(items); }.bind(this)) @@ -267,7 +267,7 @@ class SyncManager { // Map retrieved items to local data var retrieved - = this.handleItemsResponse(response.retrieved_items, null); + = this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved); // Append items to master list of retrieved items for this ongoing sync operation this.allRetreivedItems = this.allRetreivedItems.concat(retrieved); @@ -279,7 +279,7 @@ class SyncManager { // Map saved items to local data var saved = - this.handleItemsResponse(response.saved_items, omitFields); + this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved); // Create copies of items or alternate their uuids if neccessary var unsaved = response.unsaved; @@ -355,10 +355,10 @@ class SyncManager { } } - handleItemsResponse(responseItems, omitFields) { + handleItemsResponse(responseItems, omitFields, source) { var keys = this.authManager.keys() || this.passcodeManager.keys(); EncryptionHelper.decryptMultipleItems(responseItems, keys); - var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); + var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source); return items; } From 0df6c2a4121947618cf4fbd30bfb26edcf15d049 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 13 Nov 2017 12:20:30 -0600 Subject: [PATCH 09/13] Component saving timeouts --- .../app/frontend/controllers/editor.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 054b067ec..0b04ea0c2 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -382,12 +382,19 @@ angular.module('app.frontend') else if(action === "save-items" || action === "save-success" || action == "save-error") { if(data.items.map((item) => {return item.uuid}).includes(this.note.uuid)) { + if(action == "save-items") { - this.showSavingStatus(); - } else if(action == "save-success") { - $timeout(this.showAllChangesSavedStatus.bind(this), 200); - } else { - $timeout(this.showErrorStatus.bind(this), 200); + if(this.componentSaveTimeout) $timeout.cancel(this.componentSaveTimeout); + this.componentSaveTimeout = $timeout(this.showSavingStatus.bind(this), 10); + } + + else { + if(this.componentStatusTimeout) $timeout.cancel(this.componentStatusTimeout); + if(action == "save-success") { + this.componentStatusTimeout = $timeout(this.showAllChangesSavedStatus.bind(this), 400); + } else { + this.componentStatusTimeout = $timeout(this.showErrorStatus.bind(this), 400); + } } } } From 9db343a6341f2ea4fa445a13ca775cd0f1284da0 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 13 Nov 2017 23:43:34 -0600 Subject: [PATCH 10/13] Reset notes to display length on tag change --- .../javascripts/app/frontend/controllers/notes.js | 14 ++++++++++++-- app/assets/templates/frontend/notes.html.haml | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 05a1e4cd7..2f35f5d98 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -49,9 +49,11 @@ angular.module('app.frontend') this.selectFirstNote(false); }.bind(this)) - this.notesToDisplay = 20; + this.DefaultNotesToDisplayValue = 20; + + this.notesToDisplay = this.DefaultNotesToDisplayValue; this.paginate = function() { - this.notesToDisplay += 20 + this.notesToDisplay += this.DefaultNotesToDisplayValue } this.optionsSubtitle = function() { @@ -77,6 +79,14 @@ angular.module('app.frontend') } this.tagDidChange = function(tag, oldTag) { + var scrollable = document.getElementById("notes-scrollable"); + if(scrollable) { + scrollable.scrollTop = 0; + scrollable.scrollLeft = 0; + } + + this.notesToDisplay = this.DefaultNotesToDisplayValue; + this.showMenu = false; if(this.selectedNote && this.selectedNote.dummy) { diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index 020cfa552..810763227 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -38,7 +38,7 @@ Show archived notes .scrollable - .infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"} + .infinite-scroll#notes-scrollable{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"} .note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay)) track by note.uuid", "ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} %strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy From 483ffab6bfffb4fd6fdfc3e4df2b4a2f2870da0d Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 14 Nov 2017 18:57:28 -0600 Subject: [PATCH 11/13] Default editors --- .../app/frontend/controllers/editor.js | 24 +++++++++++++++---- .../app/frontend/models/app/component.js | 8 +++++++ .../directives/views/globalExtensionsMenu.js | 17 +++++++++++++ .../frontend/directives/editor-menu.html.haml | 2 +- .../global-extensions-menu.html.haml | 9 +++++-- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 0b04ea0c2..009da5dd7 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -58,7 +58,7 @@ angular.module('app.frontend') // Activate new editor if it's different from the one currently activated if(associatedEditor && associatedEditor != this.editorComponent) { - componentManager.activateComponent(associatedEditor); + this.enableComponent(associatedEditor); } this.editorComponent = associatedEditor; @@ -85,6 +85,11 @@ angular.module('app.frontend') return editor; } } + + // No editor found for note. Use default editor, if note does not prefer system editor + if(!note.getAppDataItem("prefersPlainEditor")) { + return editors.filter((e) => {return e.isDefaultEditor()})[0]; + } } this.selectedEditor = function(editorComponent) { @@ -98,7 +103,15 @@ angular.module('app.frontend') } if(editorComponent) { - this.enableComponentForCurrentItem(editorComponent); + this.note.setAppDataItem("prefersPlainEditor", false); + this.note.setDirty(true); + this.enableComponent(editorComponent); + this.associateComponentWithCurrentItem(editorComponent); + } else { + // Note prefers plain editor + this.note.setAppDataItem("prefersPlainEditor", true); + this.note.setDirty(true); + syncManager.sync(); } this.editorComponent = editorComponent; @@ -410,12 +423,15 @@ angular.module('app.frontend') componentManager.contextItemDidChangeInArea("editor-editor"); } - this.enableComponentForCurrentItem = function(component) { + this.enableComponent = function(component) { componentManager.activateComponent(component); - componentManager.associateComponentWithItem(component, this.note); componentManager.setEventFlowForComponent(component, 1); } + this.associateComponentWithCurrentItem = function(component) { + componentManager.associateComponentWithItem(component, this.note); + } + let alertKey = "displayed-component-disable-alert"; this.disableComponentForCurrentItem = function(component, showAlert) { componentManager.disassociateComponentWithItem(component, this.note); diff --git a/app/assets/javascripts/app/frontend/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index d7fa7f48f..ac0c30d0e 100644 --- a/app/assets/javascripts/app/frontend/models/app/component.js +++ b/app/assets/javascripts/app/frontend/models/app/component.js @@ -61,6 +61,14 @@ class Component extends Item { return "SN|Component"; } + isEditor() { + return this.area == "editor-editor"; + } + + isDefaultEditor() { + return this.getAppDataItem("defaultEditor") == true; + } + /* An associative component depends on being explicitly activated for a given item, compared to a dissaciative component, diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index 42bfa6f00..f046d48d5 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -133,6 +133,23 @@ class GlobalExtensionsMenu { } } + $scope.makeEditorDefault = function(component) { + var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0]; + if(currentDefault) { + currentDefault.setAppDataItem("defaultEditor", false); + currentDefault.setDirty(true); + } + component.setAppDataItem("defaultEditor", true); + component.setDirty(true); + syncManager.sync(); + } + + $scope.removeEditorDefault = function(component) { + component.setAppDataItem("defaultEditor", false); + component.setDirty(true); + syncManager.sync(); + } + // Installation $scope.submitInstallLink = function() { diff --git a/app/assets/templates/frontend/directives/editor-menu.html.haml b/app/assets/templates/frontend/directives/editor-menu.html.haml index b028399c7..cad2f2d0b 100644 --- a/app/assets/templates/frontend/directives/editor-menu.html.haml +++ b/app/assets/templates/frontend/directives/editor-menu.html.haml @@ -14,5 +14,5 @@ %li.menu-item{"ng-repeat" => "editor in editors", "ng-click" => "selectEditor($event, editor)"} %strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy %label.menu-item-title + %span.inline.tinted.mr-10{"ng-if" => "selectedEditor === editor"} ✓ {{editor.name}} - %span.inline.tinted{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓ diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 4183d255b..fe8965304 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -99,8 +99,13 @@ %h3 %input.bold{"ng-if" => "component.rename", "ng-model" => "component.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(component);", "mb-autofocus" => "true", "should-focus" => "true"} %span{"ng-if" => "!component.rename"} {{component.name}} - %a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate - %a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate + + %div{"ng-if" => "component.isEditor()"} + %a{"ng-if" => "!component.isDefaultEditor()", "ng-click" => "makeEditorDefault(component); $event.stopPropagation();"} Make Default + %a{"ng-if" => "component.isDefaultEditor()", "ng-click" => "removeEditorDefault(component); $event.stopPropagation();"} Remove Default + %div{"ng-if" => "!component.isEditor()"} + %a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate + %a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate .mt-3{"ng-if" => "component.showDetails"} .link-group %a{"ng-click" => "renameExtension(component); $event.stopPropagation();"} Rename From cf18534c311e92824583dd626ec15b6516dcb4ed Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 16 Nov 2017 18:53:44 -0600 Subject: [PATCH 12/13] Footer bar background color and border --- app/assets/stylesheets/app/_footer.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/app/_footer.scss b/app/assets/stylesheets/app/_footer.scss index f42b9c457..ac85969c9 100644 --- a/app/assets/stylesheets/app/_footer.scss +++ b/app/assets/stylesheets/app/_footer.scss @@ -16,7 +16,8 @@ h2 { position: relative; width: 100%; padding: 5px; - background-color: #d8d7d9; + background-color: #f1f1f1; + border-top: 1px solid rgba(black, 0.04); height: $footer-height; max-height: $footer-height; z-index: 100; From 0bbcced4f05e01034665e26eb8716f29999e6c53 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 20 Nov 2017 11:27:46 -0600 Subject: [PATCH 13/13] Rm log --- app/assets/javascripts/app/services/migrationManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/app/services/migrationManager.js index 527c692bf..bb139860c 100644 --- a/app/assets/javascripts/app/services/migrationManager.js +++ b/app/assets/javascripts/app/services/migrationManager.js @@ -43,7 +43,6 @@ class MigrationManager { component.setAppDataItem("data", editor.data); component.setDirty(true); this.modelManager.addItem(component); - console.log("Created component", component, "From editor", editor); } }