diff --git a/src/styles/toolbar.scss b/src/styles/toolbar.scss new file mode 100644 index 0000000000..73c99afa23 --- /dev/null +++ b/src/styles/toolbar.scss @@ -0,0 +1,18 @@ +.dropdown-kebab-pf { + .dropdown-submenu { + .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + + &:after, &:before { + border: none; + } + + a:empty { + display: none; + } + } + } +} diff --git a/src/styles/ui-components.scss b/src/styles/ui-components.scss index 764eff8abd..2ea1f3122a 100644 --- a/src/styles/ui-components.scss +++ b/src/styles/ui-components.scss @@ -2,6 +2,7 @@ @import 'dialog-editor'; @import 'dialog-editor-toolbox'; @import 'dialog-editor-boxes'; +@import 'toolbar'; .miq-sand-paper, .miq-sand-paper > div { /* emulates "cards-pf" background color */ background: #f5f5f5; @@ -34,7 +35,7 @@ .miq-custom-html { .form-group.text, .form-group.has-clear { - padding-right: 20px; + padding-right: 20px; } } diff --git a/src/toolbar/components/toolbar-menu/index.ts b/src/toolbar/components/toolbar-menu/index.ts index 5ddfc5eca7..2f4a834410 100644 --- a/src/toolbar/components/toolbar-menu/index.ts +++ b/src/toolbar/components/toolbar-menu/index.ts @@ -2,10 +2,14 @@ import Toolbar from './toolbarComponent'; import ToolbarButton from './toolbarButtonDirective'; import ToolbarList from './toolbarListComponent'; import ToolbarView from './toolbarViewComponent'; +import ToolbarKebab from './toolbarKebabComponent'; +import ToolbarClick from './toolbarClickDirective'; export default (module: ng.IModule) => { module.component('miqToolbarMenu', new Toolbar); module.component('miqToolbarList', new ToolbarList); module.component('miqToolbarView', new ToolbarView); + module.component('miqToolbarKebab', new ToolbarKebab); + module.directive('miqToolbarClick', ToolbarClick.Factory()); module.directive('miqToolbarButton', ToolbarButton.Factory()); }; diff --git a/src/toolbar/components/toolbar-menu/toolbar-item-click.html b/src/toolbar/components/toolbar-menu/toolbar-item-click.html new file mode 100644 index 0000000000..bd3c87f99c --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbar-item-click.html @@ -0,0 +1,26 @@ + + + + + {{item.text}} + diff --git a/src/toolbar/components/toolbar-menu/toolbar-kebab.html b/src/toolbar/components/toolbar-menu/toolbar-kebab.html new file mode 100644 index 0000000000..4445497e19 --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbar-kebab.html @@ -0,0 +1,29 @@ + diff --git a/src/toolbar/components/toolbar-menu/toolbar-list.html b/src/toolbar/components/toolbar-menu/toolbar-list.html index 3fe9071013..d2a818bf38 100644 --- a/src/toolbar/components/toolbar-menu/toolbar-list.html +++ b/src/toolbar/components/toolbar-menu/toolbar-list.html @@ -9,32 +9,10 @@ diff --git a/src/toolbar/components/toolbar-menu/toolbar-menu.html b/src/toolbar/components/toolbar-menu/toolbar-menu.html index 8bceb6ca1f..7f99437f0c 100644 --- a/src/toolbar/components/toolbar-menu/toolbar-menu.html +++ b/src/toolbar/components/toolbar-menu/toolbar-menu.html @@ -16,6 +16,11 @@ toolbar-list="item" on-item-click="vm.onItemClick(item, $event)"> + + +
diff --git a/src/toolbar/components/toolbar-menu/toolbarClickDirective.spec.ts b/src/toolbar/components/toolbar-menu/toolbarClickDirective.spec.ts new file mode 100644 index 0000000000..1fda204dbb --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbarClickDirective.spec.ts @@ -0,0 +1,96 @@ +import * as angular from 'angular'; + +describe('Action click test', () => { + const onItemClick = jasmine.createSpy('onItemClick', (item, $event) => undefined); + const item = { + title: 'title', + text: 'someText', + hidden: false, + explorer: false, + confirm: 'confirm-text', + data: { + 'function': 'someJsFunction', + 'function-data': 'dataToPass', + target: 'targetData', + toggle: 'toggleData', + }, + id: 'someId', + url_parms: 'urlParms', + 'send_checked': true, + prompt: 'promptData', + popup: 'popupData', + url: 'urlString', + icon: 'fa', + img_url: 'someUrl' + }; + + describe('markup', () => { + let scope, compile, compiledElement; + beforeEach(() => { + angular.mock.module('miqStaticAssets.toolbar'); + angular.mock.inject(($rootScope, $compile: ng.ICompileService) => { + scope = $rootScope.$new(); + compile = $compile; + }); + + scope.item = item; + scope.onItemClick = onItemClick; + compiledElement = compile( + angular.element( + `` + ))(scope); + scope.$digest(); + }); + + it('should render all data', () => { + expect(compiledElement.attr('title')).toBe('title'); + expect(compiledElement.attr('data-explorer')).toBe('false'); + expect(compiledElement.attr('data-confirm-tb')).toBe('confirm-text'); + expect(compiledElement.attr('data-function')).toBe('someJsFunction'); + expect(compiledElement.attr('data-function-data')).toBe('dataToPass'); + expect(compiledElement.attr('data-target')).toBe('targetData'); + expect(compiledElement.attr('data-toggle')).toBe('toggleData'); + expect(compiledElement.attr('data-click')).toBe('someId'); + expect(compiledElement.attr('name')).toBe('someId'); + expect(compiledElement.attr('id')).toBe('someId'); + expect(compiledElement.attr('data-url_parms')).toBe('urlParms'); + expect(compiledElement.attr('data-send_checked')).toBe('true'); + expect(compiledElement.attr('data-prompt')).toBe('promptData'); + expect(compiledElement.attr('data-popup')).toBe('popupData'); + expect(compiledElement.attr('data-url')).toBe('urlString'); + }); + + it('should render icon', () => { + expect(compiledElement.find('i').length).toBe(1); + expect(compiledElement.find('img').length).toBe(0); + expect(compiledElement.find('i').attr('style')).toBe('margin-right: 5px;'); + console.log(); + }); + + it('should render aligned icon', () => { + scope.item.text = ''; + scope.$apply(); + expect(compiledElement.find('i').attr('style')).toBe(undefined); + }); + + it('should render image', () => { + scope.item.icon = ''; + scope.$apply(); + expect(compiledElement.find('img').length).toBe(1); + expect(compiledElement.find('i').length).toBe(0); + }); + + it('should not render image or icon', () => { + scope.item.icon = ''; + scope.item.img_url = ''; + scope.$apply(); + expect(compiledElement.find('img').length).toBe(0); + expect(compiledElement.find('i').length).toBe(0); + }); + + it('should call onItemClick with arguments', () => { + compiledElement[0].click(); + expect(onItemClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/toolbar/components/toolbar-menu/toolbarClickDirective.ts b/src/toolbar/components/toolbar-menu/toolbarClickDirective.ts new file mode 100644 index 0000000000..84f3e9b9a0 --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbarClickDirective.ts @@ -0,0 +1,15 @@ +export default class ToolbarClick implements ng.IDirective { + public replace: boolean = true; + public template = require('./toolbar-item-click.html'); + public controllerAs: string = 'vm'; + public scope: any = { + item: '<', + onItemClick: '&' + }; + + public static Factory = () => { + let directive: ng.IDirectiveFactory = () => new ToolbarClick(); + directive.$inject = []; + return directive; + } +} diff --git a/src/toolbar/components/toolbar-menu/toolbarComponent.spec.ts b/src/toolbar/components/toolbar-menu/toolbarComponent.spec.ts index 9088b780dd..7c46fa386c 100644 --- a/src/toolbar/components/toolbar-menu/toolbarComponent.spec.ts +++ b/src/toolbar/components/toolbar-menu/toolbarComponent.spec.ts @@ -76,7 +76,7 @@ describe('Toolbar test', () => { compile, compiledElement; - const toolbarData = require('../../../../demo/data/toolbar.json'); + let toolbarData = require('../../../../demo/data/toolbar.json'); beforeEach(() => { angular.mock.module('miqStaticAssets.toolbar'); @@ -85,10 +85,10 @@ describe('Toolbar test', () => { compile = $compile; }); - scope.toolbar = toolbarData; + scope.toolbarData = toolbarData; compiledElement = compile( angular.element( - `` + `` ))(scope); scope.$digest(); }); diff --git a/src/toolbar/components/toolbar-menu/toolbarComponent.ts b/src/toolbar/components/toolbar-menu/toolbarComponent.ts index 5af1a1781f..47e3358aa2 100644 --- a/src/toolbar/components/toolbar-menu/toolbarComponent.ts +++ b/src/toolbar/components/toolbar-menu/toolbarComponent.ts @@ -1,6 +1,9 @@ import {IToolbarItem} from '../../interfaces/toolbar'; import {ToolbarType} from '../../interfaces/toolbarType'; import * as _ from 'lodash'; + +const CUSTOM_ID = 'custom_'; + /** * @memberof miqStaticAssets * @ngdoc controller @@ -105,6 +108,10 @@ export class ToolbarController { return ToolbarType.BUTTON; } + public getToolbarKebabType(): string { + return ToolbarType.KEBAB; + } + /** * Helper method for getting string value of {@link ToolbarType.CUSTOM} * @memberof ToolbarController @@ -119,6 +126,25 @@ export class ToolbarController { return ToolbarType.BUTTON_TWO_STATE; } + public collapseButtons() { + let buttonsIndex; + if (this.toolbarItems) { + buttonsIndex = _.findLastIndex( + this.toolbarItems, + (itemGroup: any) => itemGroup.filter(item => item.id.includes(CUSTOM_ID) !== false).length !== 0 + ); + if(buttonsIndex !== -1) { + this.toolbarItems[buttonsIndex] = ToolbarController.createKebabFromItems(this.toolbarItems[buttonsIndex]); + } + } + } + + private $onChanges(changesObj) { + if (changesObj.toolbarItems) { + this.collapseButtons(); + } + } + /** * Private static function for decoding html. * @memberof ToolbarController @@ -157,6 +183,7 @@ export class ToolbarController { (ToolbarController.isButtonSelect(item) && item.items && item.items.length !== 0) || ToolbarController.isButton(item) || ToolbarController.isButtonTwoState(item) + || ToolbarController.isKebabMenu(item) ); } @@ -175,6 +202,10 @@ export class ToolbarController { return item.type === ToolbarType.BUTTON_SELECT; } + private static isKebabMenu(item: IToolbarItem): boolean { + return item.type === ToolbarType.KEBAB; + } + /** * Private static function for checking if toolbar item type is button. * @memberof ToolbarController @@ -185,6 +216,16 @@ export class ToolbarController { private static isButton(item): boolean { return item.type === ToolbarType.BUTTON; } + + private static createKebabFromItems(itemsGroup: any[]) { + if (itemsGroup.length > 3) { + return itemsGroup.reduce((acc, curr) => { + curr.id.includes(CUSTOM_ID) ? acc[0].items.push(curr) : acc.push(curr); + return acc; + }, [{type: ToolbarType.KEBAB, items: []}]); + } + return itemsGroup; + } } /** diff --git a/src/toolbar/components/toolbar-menu/toolbarKebabComponent.spec.ts b/src/toolbar/components/toolbar-menu/toolbarKebabComponent.spec.ts new file mode 100644 index 0000000000..be15d0e3c7 --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbarKebabComponent.spec.ts @@ -0,0 +1,88 @@ +import * as angular from 'angular'; + +describe('Kebab test', () => { + const item = { + items: [{ + type: 'button', + text: 'Something' + }, { + type: 'buttonSelect', + text: 'Something select', + items: [{ + type: 'button', + text: 'Something 2' + }, { + type: 'button', + text: 'Something 3' + }] + }], + }; + + describe('controller', () => { + let toolbarKebabController, bindings, onItemClick; + + beforeEach(() => { + onItemClick = jasmine.createSpy('onItemClick', (item, $scope) => undefined); + bindings = { + kebabItem: item, + onItemClick: onItemClick + }; + angular.mock.module('miqStaticAssets.toolbar'); + angular.mock.inject(($componentController) => { + toolbarKebabController = $componentController('miqToolbarKebab', null, bindings); + }); + }); + + it('should create correctly controller', () => { + expect(toolbarKebabController).toBeDefined(); + }); + + it('should set component\'s items', () => { + expect(angular.equals(toolbarKebabController.kebabItem, item)).toBeTruthy(); + expect(angular.equals(toolbarKebabController.onItemClick, onItemClick)).toBeTruthy(); + }); + }); + + describe('markup', () => { + let scope, compile, compiledElement, onItemClick; + beforeEach(() => { + onItemClick = jasmine.createSpy('onItemClick', (item, $scope) => undefined); + angular.mock.module('miqStaticAssets.toolbar'); + angular.mock.inject(($rootScope, $compile: ng.ICompileService) => { + scope = $rootScope.$new(); + compile = $compile; + }); + + scope.kebabItem = item; + scope.onItemClick = onItemClick; + compiledElement = compile( + angular.element( + `` + ))(scope); + scope.$digest(); + }); + + describe('kebab menu', () => { + it('should render dropdown menu', () => { + expect(compiledElement.find('button[uib-dropdown-toggle=""]').length).toBe(1); + expect(compiledElement.find('ul')).toBeDefined(); + }); + + it('should render button and button select', () => { + expect(compiledElement.find('.dropdown-submenu').length).toBe(1); + expect(compiledElement.find('.dropdown-submenu ul').length).toBe(1); + expect(compiledElement.find('.dropdown-submenu ul li').length).toBe(2); + }); + }); + + it('should call on click', () => { + compiledElement.find('li>a')[0].click(); + expect(onItemClick).toHaveBeenCalled(); + }); + + it('should not call buttonSelect', () => { + compiledElement.find('li.dropdown-submenu>a')[0].click(); + expect(onItemClick).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/toolbar/components/toolbar-menu/toolbarKebabComponent.ts b/src/toolbar/components/toolbar-menu/toolbarKebabComponent.ts new file mode 100644 index 0000000000..9177bdf7e3 --- /dev/null +++ b/src/toolbar/components/toolbar-menu/toolbarKebabComponent.ts @@ -0,0 +1,14 @@ +class ToolbarKebabController { + public items; + public onItemClick: (args: {item: any, $event: any}) => void; +} + +export default class ToolbarKebab { + public template = require('./toolbar-kebab.html'); + public controller: any = ToolbarKebabController; + public controllerAs: string = 'vm'; + public bindings: any = { + kebabItem: '<', + onItemClick: '&' + }; +} diff --git a/src/toolbar/interfaces/toolbarType.ts b/src/toolbar/interfaces/toolbarType.ts index a18ebd15bd..94fa84f31f 100644 --- a/src/toolbar/interfaces/toolbarType.ts +++ b/src/toolbar/interfaces/toolbarType.ts @@ -30,5 +30,11 @@ export const ToolbarType = { * Separator type: `separator` * @type {string} */ - SEPARATOR: 'separator' + SEPARATOR: 'separator', + + /** + * Kebab type: `kebab` + * @type {string} + */ + KEBAB: 'kebab' };