Tobias Jeger pushed to branch feature/cmng-psp1 at cms-community / hippo-addon-channel-manager
Commits: 84490f44 by Tobias Jeger at 2016-03-21T12:33:06+01:00 CHANNELMGR-500: Add support for publishing and discarding changes. - - - - - 11 changed files: - frontend-ng/src/angularjs/api/channel.service.js - frontend-ng/src/angularjs/api/channel.service.spec.js - frontend-ng/src/angularjs/channel/channel.controller.js - frontend-ng/src/angularjs/channel/channel.controller.spec.js - frontend-ng/src/angularjs/channel/channel.html - frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js - frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.spec.js - frontend-ng/src/angularjs/channel/page/pageStructure.service.js - frontend-ng/src/angularjs/channel/page/pageStructure.service.spec.js - frontend-ng/src/i18n/hippo-cm.en.json - frontend-ng/src/i18n/hippo-cm.nl.json Changes: ===================================== frontend-ng/src/angularjs/api/channel.service.js ===================================== --- a/frontend-ng/src/angularjs/api/channel.service.js +++ b/frontend-ng/src/angularjs/api/channel.service.js @@ -103,7 +103,35 @@ export class ChannelService { return this.CatalogService.getComponents(); } + recordOwnChange() { + const user = this.ConfigService.cmsUser; + + if (this.channel.changedBySet.indexOf(user) === -1) { + this.channel.changedBySet.push(user); + } + } + + publishOwnChanges() { + return this.HstService.doPost(null, this._getMountId(), 'publish') + .then((response) => { + this._resetOwnChange(); + return response; + }); + } + + discardOwnChanges() { + return this.HstService.doPost(null, this._getMountId(), 'discard') + .then((response) => { + this._resetOwnChange(); + return response; + }); + } + _getMountId() { return this.channel.mountId; } + + _resetOwnChange() { + this.channel.changedBySet.splice(this.channel.changedBySet.indexOf(this.ConfigService.cmsUser), 1); + } } ===================================== frontend-ng/src/angularjs/api/channel.service.spec.js ===================================== --- a/frontend-ng/src/angularjs/api/channel.service.spec.js +++ b/frontend-ng/src/angularjs/api/channel.service.spec.js @@ -32,6 +32,7 @@ describe('ChannelService', () => { channelMock = { contextPath: '/testContextPath', hostname: 'test.host.name', + mountId: 'mountId', }; SessionServiceMock = { @@ -46,8 +47,9 @@ describe('ChannelService', () => { ConfigServiceMock = jasmine.createSpyObj('ConfigService', ['setContextPath']); ConfigServiceMock.apiUrlPrefix = '/testApiUrlPrefix'; ConfigServiceMock.rootUuid = 'testRootUuid'; + ConfigServiceMock.cmsUser = 'testUser'; - HstServiceMock = jasmine.createSpyObj('HstService', ['setContextPath', 'getChannel']); + HstServiceMock = jasmine.createSpyObj('HstService', ['setContextPath', 'getChannel', 'doPost']); module(($provide) => { $provide.value('SessionService', SessionServiceMock); @@ -185,4 +187,60 @@ describe('ChannelService', () => { CatalogServiceMock.getComponents.and.returnValue(mockCatalog); expect(ChannelService.getCatalog()).toEqual(mockCatalog); }); + + it('should publish own changes', () => { + channelMock.changedBySet = ['testUser']; + ChannelService.load(channelMock); + $rootScope.$digest(); + + const deferred = $q.defer(); + HstServiceMock.doPost.and.returnValue(deferred.promise); + ChannelService.publishOwnChanges().then((response) => { + expect(response).toBe('pass-through'); + }); + + deferred.resolve('pass-through'); + $rootScope.$digest(); + + expect(HstServiceMock.doPost).toHaveBeenCalledWith(null, 'mountId', 'publish'); + expect(channelMock.changedBySet).toEqual([]); + }); + + it('should discard own changes', () => { + channelMock.changedBySet = ['testUser']; + ChannelService.load(channelMock); + $rootScope.$digest(); + + const deferred = $q.defer(); + HstServiceMock.doPost.and.returnValue(deferred.promise); + ChannelService.discardOwnChanges().then((response) => { + expect(response).toBe('pass-through'); + }); + + deferred.resolve('pass-through'); + $rootScope.$digest(); + + expect(HstServiceMock.doPost).toHaveBeenCalledWith(null, 'mountId', 'discard'); + expect(channelMock.changedBySet).toEqual([]); + }); + + it('records own changes', () => { + channelMock.changedBySet = ['tobi', 'obiwan']; + ChannelService.load(channelMock); + $rootScope.$digest(); + + ChannelService.recordOwnChange(); + + expect(channelMock.changedBySet).toEqual(['tobi', 'obiwan', 'testUser']); + }); + + it('recognizes changes already pending', () => { + channelMock.changedBySet = ['tobi', 'testUser', 'obiwan']; + ChannelService.load(channelMock); + $rootScope.$digest(); + + ChannelService.recordOwnChange(); + + expect(channelMock.changedBySet).toEqual(['tobi', 'testUser', 'obiwan']); + }); }); ===================================== frontend-ng/src/angularjs/channel/channel.controller.js ===================================== --- a/frontend-ng/src/angularjs/channel/channel.controller.js +++ b/frontend-ng/src/angularjs/channel/channel.controller.js @@ -18,15 +18,18 @@ const SIDENAVS = ['components']; export class ChannelCtrl { - constructor($log, $scope, $mdSidenav, ChannelService, ScalingService, SessionService, ComponentAdderService) { + constructor($log, $scope, $translate, $mdDialog, $mdSidenav, ChannelService, ScalingService, SessionService, ComponentAdderService, ConfigService) { 'ngInject'; this.$log = $log; this.$scope = $scope; + this.$translate = $translate; + this.$mdDialog = $mdDialog; this.$mdSidenav = $mdSidenav; this.ChannelService = ChannelService; this.ScalingService = ScalingService; this.SessionService = SessionService; + this.ConfigService = ConfigService; this.iframeUrl = ChannelService.getUrl(); this.isEditMode = false; @@ -65,6 +68,9 @@ export class ChannelCtrl { this.isCreatingPreview = true; this.ChannelService.createPreviewConfiguration() .then(() => { +// this._reloadPage(); // reload page to keep UUIDs in sync with preview config + // TODO: this is first stab at reloading a page. I guess we need a better way. + // reloading the page here works in the app, but once we tell Ext which component to render, ext doesn't seem to "get it" yet. this.isEditMode = true; }) // TODO: handle error response @@ -87,7 +93,44 @@ export class ChannelCtrl { return this.ChannelService.getCatalog(); } + hasChanges() { + return this.ChannelService.getChannel().changedBySet.indexOf(this.ConfigService.cmsUser) !== -1; + } + + publish() { + this.ChannelService.publishOwnChanges(); + // TODO: what if this fails? + } + + discard() { + this._confirmDiscard().then(() => { + this.ChannelService.discardOwnChanges().then(() => this._reloadPage()); + // TODO: what if this fails? + }); + } + _isSidenavOpen(name) { return this.$mdSidenav(name).isOpen(); } + + _confirmDiscard() { + const confirm = this.$mdDialog + .confirm() + .title(this.$translate.instant('CONFIRM_DISCARD_OWN_CHANGES_TITLE')) + .textContent(this.$translate.instant('CONFIRM_DISCARD_OWN_CHANGES_MESSAGE')) + .ok(this.$translate.instant('BUTTON_YES')) + .cancel(this.$translate.instant('BUTTON_NO')); + + return this.$mdDialog.show(confirm); + } + + _reloadPage() { + // TODO: this should probably go into the hippoIframe. + const iframe = $('iframe'); + if (iframe.length > 0) { + const currentPage = iframe[0].contentWindow.location.pathname; + + iframe.attr('src', currentPage); + } + } } ===================================== frontend-ng/src/angularjs/channel/channel.controller.spec.js ===================================== --- a/frontend-ng/src/angularjs/channel/channel.controller.spec.js +++ b/frontend-ng/src/angularjs/channel/channel.controller.spec.js @@ -22,23 +22,30 @@ describe('ChannelCtrl', () => { let ChannelService; let ComponentsService; let ScalingService; + let ConfigService; let ChannelCtrl; let $rootScope; let $q; + let $mdDialog; beforeEach(() => { module('hippo-cm'); - inject(($controller, _$rootScope_, _$q_) => { + inject(($controller, _$rootScope_, _$q_, _$mdDialog_, _ConfigService_) => { const resolvedPromise = _$q_.when(); $rootScope = _$rootScope_; $q = _$q_; + $mdDialog = _$mdDialog_; + ConfigService = _ConfigService_; ChannelService = jasmine.createSpyObj('ChannelService', [ 'getUrl', 'hasPreviewConfiguration', 'createPreviewConfiguration', + 'getChannel', + 'publishOwnChanges', + 'discardOwnChanges', ]); ChannelService.getUrl.and.returnValue('/test/url'); ChannelService.createPreviewConfiguration.and.returnValue(resolvedPromise); @@ -116,4 +123,49 @@ describe('ChannelCtrl', () => { ChannelCtrl.toggleEditMode(); expect(ChannelService.createPreviewConfiguration).not.toHaveBeenCalled(); }); + + it('detects that the current user has pending changes', () => { + ConfigService.cmsUser = 'testUser'; + ChannelService.getChannel.and.returnValue({ changedBySet: ['tobi', 'testUser', 'obiwan'] }); + + expect(ChannelCtrl.hasChanges()).toBe(true); + }); + + it('detects that the current user has no pending changes', () => { + ConfigService.cmsUser = 'testUser'; + ChannelService.getChannel.and.returnValue({ changedBySet: ['tobi', 'obiwan'] }); + + expect(ChannelCtrl.hasChanges()).toBe(false); + }); + + it('publishes changes', () => { + ChannelCtrl.publish(); + + expect(ChannelService.publishOwnChanges).toHaveBeenCalled(); + }); + + it('discards changes', () => { + ChannelService.discardOwnChanges.and.returnValue($q.resolve()); + spyOn($mdDialog, 'show').and.returnValue($q.resolve()); + spyOn($mdDialog, 'confirm').and.callThrough(); + + ChannelCtrl.discard(); + $rootScope.$digest(); + + expect($mdDialog.confirm).toHaveBeenCalled(); + expect($mdDialog.show).toHaveBeenCalled(); + expect(ChannelService.discardOwnChanges).toHaveBeenCalled(); + }); + + it('does not discard changes if not confirmed', () => { + spyOn($mdDialog, 'show').and.returnValue($q.reject()); + spyOn($mdDialog, 'confirm').and.callThrough(); + + ChannelCtrl.discard(); + $rootScope.$digest(); + + expect($mdDialog.confirm).toHaveBeenCalled(); + expect($mdDialog.show).toHaveBeenCalled(); + expect(ChannelService.discardOwnChanges).not.toHaveBeenCalled(); + }); }); ===================================== frontend-ng/src/angularjs/channel/channel.html ===================================== --- a/frontend-ng/src/angularjs/channel/channel.html +++ b/frontend-ng/src/angularjs/channel/channel.html @@ -22,6 +22,28 @@ {{ 'TOOLBAR_BUTTON_COMPONENTS' | translate }} </md-button> <span flex></span> + <md-menu ng-if="channelCtrl.hasChanges()"> + <md-button aria-label="{{ 'TOOLBAR_BUTTON_CHANGES' | translate }}" + ng-click="$mdOpenMenu($event)"> + <md-icon class="material-icons">change_history</md-icon> + {{ 'TOOLBAR_BUTTON_CHANGES' | translate }} + </md-button> + <md-menu-content width="4"> + <md-menu-item> + <md-button aria-label="{{ 'TOOLBAR_MENU_CHANGES_PUBLISH' | translate }}" + ng-click="channelCtrl.publish()"> + {{ 'TOOLBAR_MENU_CHANGES_PUBLISH' | translate }} + </md-button> + </md-menu-item> + <md-menu-item> + <md-button aria-label="{{ 'TOOLBAR_MENU_CHANGES_DISCARD' | translate }}" + ng-click="channelCtrl.discard()"> + {{ 'TOOLBAR_MENU_CHANGES_DISCARD' | translate }} + </md-button> + </md-menu-item> + </md-menu-content> + </md-menu> + <md-button ng-if="channelCtrl.isEditable()" ng-disabled="channelCtrl.isCreatingPreview" ng-click="channelCtrl.toggleEditMode()"> ===================================== frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js ===================================== --- a/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js +++ b/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js @@ -16,7 +16,7 @@ export class DragDropService { - constructor($rootScope, $q, DomService, HstService, PageStructureService, ScalingService) { + constructor($rootScope, $q, DomService, HstService, PageStructureService, ScalingService, ChannelService) { 'ngInject'; this.$rootScope = $rootScope; @@ -25,6 +25,7 @@ export class DragDropService { this.HstService = HstService; this.PageStructureService = PageStructureService; this.ScalingService = ScalingService; + this.ChannelService = ChannelService; this.dragging = false; } @@ -129,6 +130,8 @@ export class DragDropService { if (sourceContainer.getId() !== targetContainer.getId()) { this._updateContainer(targetContainer); } + + this.ChannelService.recordOwnChange(); } _updateContainer(container) { ===================================== frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.spec.js ===================================== --- a/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.spec.js +++ b/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.spec.js @@ -21,6 +21,7 @@ describe('DragDropService', () => { let ScalingService; let PageStructureService; let HstService; + let ChannelService; let iframe; let base; @@ -33,13 +34,15 @@ describe('DragDropService', () => { beforeEach(() => { module('hippo-cm.channel.hippoIframe'); - inject((_DragDropService_, _ScalingService_, _PageStructureService_, _HstService_) => { + inject((_DragDropService_, _ScalingService_, _PageStructureService_, _HstService_, _ChannelService_) => { DragDropService = _DragDropService_; ScalingService = _ScalingService_; PageStructureService = _PageStructureService_; HstService = _HstService_; + ChannelService = _ChannelService_; }); + spyOn(ChannelService, 'recordOwnChange'); jasmine.getFixtures().load('channel/hippoIframe/dragDrop.service.fixture.html'); iframe = $j('#testIframe'); ===================================== frontend-ng/src/angularjs/channel/page/pageStructure.service.js ===================================== --- a/frontend-ng/src/angularjs/channel/page/pageStructure.service.js +++ b/frontend-ng/src/angularjs/channel/page/pageStructure.service.js @@ -110,7 +110,10 @@ export class PageStructureService { if (component) { const container = component.getContainer(); this.HstService.removeHstComponent(container.getId(), componentId) - .then(() => this._renderContainer(container)); + .then(() => { + this.ChannelService.recordOwnChange(); + this._renderContainer(container); + }); // TODO handle error } else { this.$log.debug(`Was asked to remove component with ID '${componentId}', but couldn't find it in the page structure.`); @@ -192,7 +195,10 @@ export class PageStructureService { if (container) { this.HstService.addHstComponent(catalogComponent, container.getId()) - .then(() => this._renderContainer(container)); + .then(() => { + this.ChannelService.recordOwnChange(); + this._renderContainer(container); + }); // TODO: handle error } else { console.log('container not found'); ===================================== frontend-ng/src/angularjs/channel/page/pageStructure.service.spec.js ===================================== --- a/frontend-ng/src/angularjs/channel/page/pageStructure.service.spec.js +++ b/frontend-ng/src/angularjs/channel/page/pageStructure.service.spec.js @@ -44,6 +44,8 @@ describe('PageStructureService', () => { HstService = _HstService_; RenderingService = _RenderingService_; }); + + spyOn(ChannelService, 'recordOwnChange'); }); beforeEach(() => { @@ -299,6 +301,7 @@ describe('PageStructureService', () => { $rootScope.$digest(); expect(HstService.removeHstComponent).toHaveBeenCalledWith('container-vbox', 'aaaa'); + expect(ChannelService.recordOwnChange).toHaveBeenCalled(); }); it('removes a valid component but fails to call HST', () => { ===================================== frontend-ng/src/i18n/hippo-cm.en.json ===================================== --- a/frontend-ng/src/i18n/hippo-cm.en.json +++ b/frontend-ng/src/i18n/hippo-cm.en.json @@ -1,11 +1,16 @@ { "TOOLBAR_BUTTON_COMPONENTS": "Components", + "TOOLBAR_BUTTON_CHANGES": "Changes", + "TOOLBAR_MENU_CHANGES_PUBLISH": "Publish", + "TOOLBAR_MENU_CHANGES_DISCARD": "Discard", "TOOLBAR_SWITCH_VIEWER_MODE": "Viewer mode", "TOOLBAR_SWITCH_VIEWER_MODE_EDIT": "Edit", "TOOLBAR_SWITCH_VIEWER_MODE_VIEW": "View", "CONFIRM_OPEN_EXTERNAL_LINK": "This external link will be opened in a new tab.\nDo you want to continue?", "CONFIRM_DELETE_COMPONENT_TITLE": "Confirm remove", "CONFIRM_DELETE_COMPONENT_MESSAGE": "Are you sure you want to remove the component '{{component}}' from this template?", + "CONFIRM_DISCARD_OWN_CHANGES_TITLE": "Confirm discard changes", + "CONFIRM_DISCARD_OWN_CHANGES_MESSAGE": "Are you sure you want to discard all your unpublished changes to this channel?", "BUTTON_YES": "Yes", "BUTTON_NO": "No" } ===================================== frontend-ng/src/i18n/hippo-cm.nl.json ===================================== --- a/frontend-ng/src/i18n/hippo-cm.nl.json +++ b/frontend-ng/src/i18n/hippo-cm.nl.json @@ -1,11 +1,16 @@ { "TOOLBAR_BUTTON_COMPONENTS": "Componenten", + "TOOLBAR_BUTTON_CHANGES": "Wijzigingen", + "TOOLBAR_MENU_CHANGES_PUBLISH": "Publiceer", + "TOOLBAR_MENU_CHANGES_DISCARD": "Maak ongedaan", "TOOLBAR_SWITCH_VIEWER_MODE": "Viewer modus", "TOOLBAR_SWITCH_VIEWER_MODE_EDIT": "Bewerk", "TOOLBAR_SWITCH_VIEWER_MODE_VIEW": "Bekijk", "CONFIRM_OPEN_EXTERNAL_LINK": "Deze externe link zal in een nieuwe tab worden geopened.\nWilt u doorgaan?", "CONFIRM_DELETE_COMPONENT_TITLE": "Bevestig het verwijderen", "CONFIRM_DELETE_COMPONENT_MESSAGE": "Weet u zeker dat u {0} wilt verwijderen?", + "CONFIRM_DISCARD_OWN_CHANGES_TITLE": "Bevestig ongedaan maken", + "CONFIRM_DISCARD_OWN_CHANGES_MESSAGE": "Weet u zeker dat u al uw ongepubliceerde veranderingen in deze channel ongedaan wilt maken?", "BUTTON_YES": "Ja", "BUTTON_NO": "Nee" } View it on GitLab: https://code.onehippo.org/cms-community/hippo-addon-channel-manager/commit/84490f446f4051220fd4e1e51144cf08a12bf9bb
_______________________________________________ Hippocms-svn mailing list Hippocms-svn@lists.onehippo.org https://lists.onehippo.org/mailman/listinfo/hippocms-svn