Commit 8e4b6103 authored by benjaminParisel's avatar benjaminParisel Committed by GitHub

feat(switch): be available to switch a widget to another (#2701)

* First use of webcomponent in UID
* replace phantomJS by karma-chrome-launcher

Covers [UID-56](https://bonitasoft.atlassian.net/browse/UID-56)
parent f89f6754
......@@ -57,7 +57,7 @@
"karma-phantomjs-launcher": "1.0.4",
"log4js": "0.6.27",
"optimist": "0.6.1",
"phantomjs-prebuilt": "2.1.8",
"phantomjs-prebuilt": "2.1.16",
"through": "^2.3.6",
"through2": "^0.6.5",
"widget-builder": "bonitasoft/widget-builder#2.0.1"
......
......@@ -23,7 +23,7 @@
"defaultValue": false
},
{
"label": "label Hidden",
"label": "Label hidden",
"name": "labelHidden",
"type": "boolean",
"defaultValue": false,
......
This diff is collapsed.
......@@ -4,7 +4,6 @@
<meta charset="utf-8">
<meta name="version" content="1.5-SNAPSHOT">
<title>UI Designer</title>
<link rel="icon" href="favicon.ico" type="image/x-icon">
<!-- build:css -->
<link rel="stylesheet" href="css/main.css"/>
......
......@@ -17,7 +17,7 @@
* common functions to the directives used inside the page.
*/
angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($scope, $state, $stateParams, $window, artifactRepo, resolutions, artifact, mode, arrays, componentUtils, keyBindingService, $uibModal, utils, whiteboardService, $timeout, widgetRepo) {
angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($scope, $state, $stateParams, $window, artifactRepo, resolutions, artifact, mode, arrays, componentUtils, keyBindingService, $uibModal, utils, whiteboardService, $timeout, widgetRepo, editorService, gettextCatalog) {
'use strict';
......@@ -189,6 +189,7 @@ angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($
*/
$scope.removeCurrentComponent = function(item, destinationRow) {
var component = $scope.currentComponent || item;
var currentRow = component.$$parentContainerRow.row;
var componentIndex = currentRow.indexOf(component);
currentRow.splice(componentIndex, 1);
......@@ -201,6 +202,63 @@ angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($
$scope.currentComponent = null;
};
/**
*
* @returns {boolean}
*/
function isNotSupportedBrowsers() {
let ua = navigator.userAgent;
/* MSIE used to detect old browsers and Trident used to newer ones*/
return ua.indexOf('MSIE ') > -1 || ua.indexOf('Trident/') > -1 || ua.indexOf('Edge/') > -1;
}
$scope.switchCurrentComponent = function(item) {
if (isNotSupportedBrowsers()) {
$uibModal.open({
templateUrl: 'js/editor/whiteboard/switchComponent/not-supported-browser-popup.html',
size: 'small',
backdrop: 'true'
});
return;
}
let component = $scope.currentComponent || item;
let modalInstance = $uibModal.open({
templateUrl: 'js/editor/whiteboard/switchComponent/switch-component-popup.html',
controller: 'SwitchComponentPopupController',
controllerAs: 'ctrl',
size: 'lg',
resolve: {
widgets: widgetRepo.all().then(items => items),
widgetFrom: angular.copy(component),
dictionary: gettextCatalog.strings
}
});
modalInstance.result.then(switchComponent.bind(null, component));
};
function switchComponent(component, resultData) {
let widgetTo = JSON.parse(resultData.mapping);
let row = component.$$parentContainerRow.row;
let index = row.findIndex(p => p.$$hashKey === component.$$hashKey);
widgetRepo.load(widgetTo.id).then(response => {
let newWidget = response.data;
$scope.removeCurrentComponent(component, row);
let compo = editorService.createWidgetWrapper(newWidget);
let newComponent = compo.create(component.$$parentContainerRow);
Object.keys(newComponent.propertyValues).forEach(p => {
newComponent.propertyValues[p] = widgetTo.options[p];
});
newComponent.dimension = resultData.dimension;
arrays.insertAtPosition(newComponent, index, row);
$scope.selectComponent(newComponent);
newComponent.triggerAdded();
});
}
/**
* Remove the row at the end of the current digest loop
* to avoid messing up row indexes during drag and drop.
......@@ -293,6 +351,7 @@ angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($
};
$scope.isPropertyPanelClosed = false;
function togglePropertyPanel() {
$scope.isPropertyPanelClosed = !$scope.isPropertyPanelClosed;
}
......@@ -310,6 +369,7 @@ angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($
componentClasses: $scope.componentClasses,
removeCurrentRow: $scope.removeCurrentRow,
removeCurrentComponent: $scope.removeCurrentComponent,
switchCurrentComponent: $scope.switchCurrentComponent,
rowSize: $scope.rowSize,
isCurrentRow: $scope.isCurrentRow,
isCurrentComponent: $scope.isCurrentComponent,
......@@ -327,11 +387,13 @@ angular.module('bonitasoft.designer.editor').controller('EditorCtrl', function($
var unregisterWidget = $scope.$watch(
'currentComponent.$$widget',
function() { $scope.widgetHelpPopover.isOpen = false; }
function() {
$scope.widgetHelpPopover.isOpen = false;
}
);
$scope.$on('$destroy', unregisterWidget);
$scope.widgetHelpTemplateURL = 'js/editor/properties-panel/widgetHelpTemplate.html';
$scope.widgetHelpTemplateURL = 'js/editor/properties-panel/widgetHelpTemplate.html';
$scope.triggerWidgetHelp = function() {
if ($scope.widgetHelpPopover.isOpen) {
......
......@@ -59,3 +59,58 @@
.display-widget > div {
display: none;
}
#select-from {
display: grid;
grid-template-columns: auto 2fr auto;
align-items: center;
}
#header-switch {
display: grid;
grid-template-columns: 2fr 1fr;
}
#reset-btn {
justify-self: right;
}
#widgetsFilterValue{
flex: 1;
width: 100%;
}
.switch-widget-img{
width: 100%;
margin-bottom: 10px;
}
.show-properties-btn {
margin-left: 5px;
}
.show-properties-btn {
margin-left: 5px;
}
.switch-config{
min-height: 300px;
max-height: 500px;
overflow-y: auto;
background-color: #f9f9f9;
padding: 5px;
}
.filter-input {
position: relative;
padding-left: 15px;
}
.filter-clearButton {
border: none;
background: none;
padding: 0;
position: absolute;
right: .7em;
top: .7em;
line-height: 1;
}
......@@ -29,7 +29,8 @@
addPalette: addPalette,
initialize: initialize,
addWidgetAssetsToPage,
removeAssetsFromPage
removeAssetsFromPage,
createWidgetWrapper: createWidgetWrapper
};
function addPalette(key, repository) {
......@@ -82,6 +83,14 @@
components.register(customWidgets);
}
function createWidgetWrapper(component) {
let extended = properties.addCommonPropertiesTo(component);
return {
component: extended,
create: createWidget.bind(null, extended)
};
}
function paletteWidgetWrapper(name, order, component) {
var extended = properties.addCommonPropertiesTo(component);
return {
......
......@@ -14,6 +14,8 @@
class="clickable PropertyPanel-widget-action-item"
ng-click="saveAndEditCustomWidget(currentComponent.$$widget.id)" translate>Edit...</a></li>
<li><a id="switchAction"
class="clickable PropertyPanel-widget-action-item" ng-click="" translate>Switch...</a></li>
class="clickable PropertyPanel-widget-action-item"
ng-click="switchCurrentComponent(currentComponent)"
translate>Switch...</a></li>
</ul>
</div>
......@@ -21,7 +21,8 @@ angular.module('bonitasoft.designer.editor.whiteboard').directive('componentMove
replace: true,
scope: {
component: '=',
onDelete: '&'
onDelete: '&',
onSwitch: '&'
},
templateUrl: 'js/editor/whiteboard/component-mover.html',
controller: 'ComponentMoverDirectiveCtrl'
......
......@@ -2,5 +2,6 @@
{{component.$$widget.name}}
<button ng-if="moveLeftVisible()" ng-click="moveLeft()" class="fa fa-arrow-circle-left btn-caption move-left" title="Move the component to the left"></button>
<button ng-if="moveRightVisible()" ng-click="moveRight()" class="fa fa-arrow-circle-right btn-caption move-right" title="Move the component to the right"></button>
<button ng-if="component.$$widget.type === 'widget'" ng-click="onSwitch()" class="fa fa-exchange btn-caption" title="Switch widget"></button>
<button ng-click="onDelete()" class="fa fa-times-circle btn-caption" title="Delete the component"></button>
</section>
<div class="widget-wrapper">
<component-mover class="component-mover" component="component" on-delete="editor.removeCurrentComponent()" ng-if="editor.isCurrentComponent(component)"></component-mover>
<component-mover class="component-mover" component="component" on-delete="editor.removeCurrentComponent()" on-switch="editor.switchCurrentComponent()" ng-if="editor.isCurrentComponent(component)"></component-mover>
<div class="widget-content"></div>
<drop-zone editor="editor" row="row" container="container" component-index="componentIndex"></drop-zone>
</div>
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title" translate>Look what you're missing</h3>
</div>
<div class="modal-body">
<img class="switch-widget-img"
src="img/switchWidgetFeature.jpg"
alt="Example of switch widget feature in available browser.">
<p translate>
This feature is not available on IE11 nor Microsoft Edge for the moment.
</p>
<p translate>
To switch widget, use another browser: Google Chrome or Mozilla Firefox.
</p>
</div>
<div class="modal-footer">
<button class="btn btn-link EditAssetPopUp-dismissBtn" type="button" ng-click="$dismiss()">
<span class="u-fadeIn" translate>Close</span>
</button>
</div>
</div>
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
class SwitchComponentPopupController {
constructor($uibModalInstance, widgets, widgetFrom, properties, dictionary) {
this.$uibModalInstance = $uibModalInstance;
this.widgets = widgets;
this.widgetFrom = widgetFrom;
this.propertiesService = properties;
this.selectedWidget = '';
this.widgetsToDisplay = this.widgets.sort((a, b) => a.name.localeCompare(b.name)).filter(w => w.id !== this.widgetFrom.$$widget.id && w.type !== 'container').map(item => {
return { id: item.id, name: item.name, custom: item.custom };
});
this.propertiesFrom = this._extractFromProperties(this.widgetFrom);
// Get dictionary to give it on webComponent
let dictionaryValues = Object.values(dictionary);
this.dictionary = this._attachDictionary(dictionaryValues[0]);
}
_attachDictionary(dictionary) {
let dico = {};
if (dictionary) {
Object.keys(dictionary).forEach(key => {
dico[key] = dictionary[key].$$noContext[0];
});
}
return dico;
}
_extractFromProperties(widget) {
let options = {};
let widgetPropertyLabel = widget.$$widget.properties;
Object.keys(widget.propertyValues).forEach(property => {
let labelProperty = widgetPropertyLabel.filter(p => p.name === property);
if (labelProperty.length > 0) {
options[property] = widget.propertyValues[property];
options[property].label = labelProperty[0].label;
}
});
return { name: widget.$$widget.name, options: options };
}
getWidget(widgetId) {
let widgets = this.widgets.filter(w => w.id === widgetId);
let widget = widgets[0];
widget = this.propertiesService.addCommonPropertiesTo(widget);
let options = {};
widget.properties.forEach(w => {
options[w.name] = w;
});
return { id: widget.id, name: widget.name, options: options };
}
isClearButtonVisible() {
return (this.selectedWidget || '').trim().length > 0;
}
clearValue() {
this.selectedWidget = '';
}
showProperties() {
if (!this.selectedWidget) {
this.selectedWidget = '';
return;
}
let selectedWidget = this.widgetsToDisplay.filter(w => w.name.toLowerCase() === this.selectedWidget.toLowerCase());
if (selectedWidget.length > 0) {
this.propertiesTo = this.getWidget(selectedWidget[0].id);
}
}
applyConfig() {
let switchComponentConfig = document.getElementById('switch-component-config');
this.$uibModalInstance.close({
'mapping': switchComponentConfig.result,
'dimension': this.widgetFrom.dimension
}, this.widgetFrom);
}
reset() {
let switchComponentConfig = document.getElementById('switch-component-config');
switchComponentConfig.resetMapping();
}
}
angular.module('bonitasoft.designer.editor.whiteboard').controller('SwitchComponentPopupController', SwitchComponentPopupController);
})();
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title" translate>Switch {{ctrl.propertiesFrom.name}} {{ctrl.widgetFrom.$$widget.type}} </h3>
</div>
<div class="modal-body">
<div class="select-widget-to">
<div id="header-switch">
<div id="select-from">
<label for="widgetsFilterValue" translate>Switch to</label>
<div class="filter-input">
<input id="widgetsFilterValue" type="text" list="widgetsDisplay"
ng-model="ctrl.selectedWidget"
class="form-control Search-input"
placeholder="{{ 'Select a widget or start typing' | translate}}"
autocomplete=off
/>
<button type="button" ng-if="ctrl.isClearButtonVisible()"
tabindex="-1"
ng-click="ctrl.clearValue()"
class="filter-clearButton"
title="{{ 'Clear' | translate }}"
aria-label="{{ 'Clear' | translate }}">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
<datalist id="widgetsDisplay">
<option ng-repeat="widget in ctrl.widgetsToDisplay"
value="{{widget.name}}"></option>
</datalist>
</div>
<button
id="show-properties-btn"
type="button"
ng-click="ctrl.showProperties()"
ng-disabled="!ctrl.selectedWidget"
class="btn btn-primary show-properties-btn"
title="{{ 'Show properties' | translate }}"
translate>Show properties
</button>
</div>
<button
id="reset-btn"
type="button"
ng-click="ctrl.reset()"
class="btn btn-link"
title="{{ 'Reset mapping to default' | translate }}"
translate>Reset mapping
</button>
</div>
</div>
<br/>
<p translate>
When possible, properties of the target widget are mapped with similar properties of the source widget. You can edit that and map more.
</p>
<div class="switch-config">
<bo-switch-config
id="switch-component-config"
dictionary="{{ctrl.dictionary}}"
from="{{ ctrl.propertiesFrom }}"
to="{{ ctrl.propertiesTo}}"
></bo-switch-config>
</div>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-primary"
ng-click="ctrl.applyConfig()"
translate>Switch
</button>
<button class="btn btn-link EditAssetPopUp-dismissBtn" type="button" ng-click="$dismiss()">
<span class="u-fadeIn" translate>Cancel</span>
</button>
</div>
......@@ -183,11 +183,15 @@ module.exports = function (gulp, config) {
});
gulp.task('jshint', function () {
function notMinified(file) {
return !/(src-min|\.min\.js)/.test(file.path);
}
return gulp.src(paths.js)
.pipe(jshint())
.pipe(gulpIf(notMinified, jshint()))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'))
.pipe(jscs())
.pipe(gulpIf(notMinified, jscs()))
.pipe(jscs.reporter())
.pipe(jscs.reporter('fail'));
});
......
......@@ -36,7 +36,9 @@ var paths = {
'node_modules/angular-switcher/dist/angular-switcher.min.js',
'node_modules/ngstorage/ngStorage.min.js',
'node_modules/angular-resizable/angular-resizable.min.js',
'node_modules/angular-animate/angular-animate.min.js'
'node_modules/angular-animate/angular-animate.min.js',
'node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js',
'node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'
],
css: [
'node_modules/angular-switcher/dist/angular-switcher.min.css',
......
......@@ -7,6 +7,7 @@
"npm": ">=2.0.0"
},
"dependencies": {
"@webcomponents/webcomponentsjs": "^2.2.7",
"angular-animate": "1.4.7",
"angular-resizable": "1.2.0",
"angular-switcher": "0.2.7",
......@@ -56,22 +57,21 @@
"jshint-stylish": "~1.0.0",
"karma": "1.3.0",
"karma-babel-preprocessor": "6.0.1",
"karma-chrome-launcher": "~0.1.4",
"karma-cli": "0.0.4",
"karma-commonjs": "0.0.13",
"karma-coverage": "~0.2.6",
"karma-chrome-launcher": "^2.2.0",
"karma-jasmine": "0.3.8",
"karma-junit-reporter": "~0.2.2",
"karma-ng-html2js-preprocessor": "^0.1.0",
"karma-phantomjs-launcher": "1.0.4",
"karma-source-map-support": "1.1.0",
"lodash": "~2.4.1",
"merge-stream": "^0.1.7",
"mkdirp": "^0.5.1",
"multiparty": "^4.1.2",
"npm-run-all": "^4.1.2",
"phantomjs-prebuilt": "2.1.8",
"protractor": "5.2.0",
"puppeteer": "1.15.0",
"run-sequence": "~0.3.7",
"serve-static": "~1.6.2",
"source-map-support": "0.4.0",
......
/* globals process */
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
'use strict';
config.set({
plugin:[
require('karma-chrome-launcher'),
],
// base path, that will be used to resolve files and exclude
basePath: '..',
......@@ -98,15 +102,14 @@ module.exports = function(config) {
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
browsers: ['ChromeWithoutSecurity'],
// you can define custom flags
customLaunchers: {
ChromeWithoutSecurity: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
......
describe('CustomWidgetEditorCtrl', function() {
describe('CustomWidgetEditorCtrl', function () {
var $window, browserHistoryService;
beforeEach(angular.mock.module('bonitasoft.designer.common.services'));
beforeEach(inject(function($rootScope, _$window_, _browserHistoryService_) {
beforeEach(inject(function ($rootScope, _$window_, _browserHistoryService_) {
$window = _$window_;
browserHistoryService = _browserHistoryService_;
$window.history = {
back: jasmine.createSpy(),
};
}));
it('should navigate to home page', function() {
$window.history.length = 1;
it('should navigate to home page', function () {
let fallback = jasmine.createSpy();
browserHistoryService.back(fallback);
expect($window.history.back).not.toHaveBeenCalled();
expect(fallback).toHaveBeenCalled();
});
it('should navigate back', function() {
$window.history.length = 4;
browserHistoryService.back();
expect($window.history.back).toHaveBeenCalled();
it('should navigate back', function () {
let newState = {
foo: 'bar'
};
$window.history.pushState(newState, 'new state entry', 'fake-page.html');
let fallback = jasmine.createSpy();
browserHistoryService.back(fallback);
expect(fallback).not.toHaveBeenCalled();
});
});
import widgets from './widgets-mock';
import widget from './widgetsFrom-mock';
describe('switchComponentPopupController', function () {
let $scope;
let widgetRepo;
let modalInstance;
let controller;
beforeEach(angular.mock.module('bonitasoft.designer.editor.whiteboard', 'bonitasoft.designer.common.repositories', 'mock.modal'));
beforeEach(inject(function ($rootScope, $controller, _widgetRepo_, _$uibModalInstance_, $q, _properties_) {
$scope = $rootScope.$new();
widgetRepo = _widgetRepo_;
spyOn(widgetRepo, 'all').and.returnValue($q.when(widgets));
modalInstance = _$uibModalInstance_.create();
controller = $controller('SwitchComponentPopupController', {
$uibModalInstance: modalInstance,
widgetFrom: widget,
widgets: widgets,
properties: _properties_,
dictionary: {}
});
}));
it('should contains widgets available without widgetFrom name when user open popup', function () {
expect(controller.widgetsToDisplay).toEqual([{
id: 'pbAutocomplete',
name: 'Autocomplete',
custom: false
}]);
});
it('should format widgetFrom properties when user open popup', function () {
expect(controller.propertiesFrom).toEqual({
name: 'Input',
options: Object({
cssClasses: Object({type: 'constant', value: '', label: 'CSS classes'}),
hidden: Object({type: 'constant', value: false, label: 'Hidden'}),
required: Object({type: 'constant', value: false, label: 'Required'}),
minLength: Object({type: 'constant', value: '', label: 'Value min length'}),
maxLength: Object({type: 'constant', value: '', label: 'Value max length'}),
readOnly: Object({type: 'constant', value: false, label: 'Read-only'}),
labelHidden: Object({type: 'constant', value: false, label: 'Label hidden'}),
label: Object({type: 'interpolation', value: 'Default label', label: 'Label'}),
labelPosition: Object({type: 'constant', value: 'top', label: 'Label position'}),
labelWidth: Object({type: 'constant', value: 4, label: 'Label width'}),
placeholder: Object({type: 'interpolation', label: 'Placeholder'}),
value: Object({type: 'variable', value: '', label: 'Value'}),
type: Object({type: 'constant', value: 'text', label: 'Type'}),
min: Object({type: 'constant', label: 'Min value'}),
max: Object({type: 'constant', label: 'Max value'}),
step: Object({type: 'constant', value: 1, label: 'Step'})
})
});
it('should display by alphabetical sort widgets when user open popup', function () {
expect(controller.widgetsToDisplay).toEqual([{
id: 'pbAutocomplete',
name: 'Autocomplete',
custom: false
}, {id: 'pbInput', name: 'Input', custom: true}]);
});
});
it('should get widgetTo properties when user select a widget', function () {
let expected = {
id: 'pbInput',
name: 'Input',