Merge branch 'master' into jcCompactList Project: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/commit/55ae032e Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/tree/55ae032e Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/diff/55ae032e
Branch: refs/heads/master Commit: 55ae032efe88a02120deae27dddb14485b6bbbf3 Parents: cb074a5 43c154c Author: Juan Cabrerizo <[email protected]> Authored: Thu Nov 8 16:37:16 2018 +0000 Committer: GitHub <[email protected]> Committed: Thu Nov 8 16:37:16 2018 +0000 ---------------------------------------------------------------------- .../blueprint-data-manager.directive.js | 28 +- .../breacrumbs/breadcrumbs.directive.js | 44 +- .../catalog-saver/catalog-saver.directive.js | 22 +- .../catalog-selector.directive.js | 580 +++++++++++-------- .../catalog-selector/catalog-selector.less | 41 ++ .../catalog-selector.template.html | 59 +- .../suggestion-dropdown.html | 2 +- .../custom-config-widget/suggestion-dropdown.js | 13 +- .../components/designer/designer.directive.js | 27 +- .../app/components/dsl-editor/dsl-editor.js | 12 +- .../app/components/dsl-viewer/dsl-viewer.js | 12 +- .../factories/object-cache.factory.js | 9 + .../factories/recursion-helper.factory.js | 61 -- .../app/components/filters/entity.filter.js | 17 +- .../app/components/filters/locations.filter.js | 9 + .../providers/action-service.provider.js | 11 +- .../providers/blueprint-service.provider.js | 8 + .../providers/dsl-service.provider.js | 7 + .../providers/palette-dragndrop.provider.js | 7 + .../providers/recently-used-service.provider.js | 8 + .../spec-editor/spec-editor.directive.js | 8 +- .../spec-editor/spec-editor.template.html | 50 +- ui-modules/blueprint-composer/app/index.js | 55 +- .../app/views/main/graphical/edit/add/add.html | 2 +- .../app/views/main/graphical/edit/add/add.js | 11 + .../views/main/graphical/graphical.state.html | 4 +- .../app/views/main/graphical/graphical.state.js | 28 +- 27 files changed, 697 insertions(+), 438 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/55ae032e/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js ---------------------------------------------------------------------- diff --cc ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js index 298ba80,2cad3ef..3d90b92 --- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js +++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js @@@ -32,11 -37,11 +37,12 @@@ const PALETTE_VIEW_ORDERS = }; const PALETTE_VIEW_MODES = { - compact: { name: "Compact", classes: "col-xs-2 item-compact", itemsPerRow: 6, rowHeightPx: 75, hideName: true }, - normal: { name: "Normal", classes: "col-xs-3", itemsPerRow: 4 }, - large: { name: "Large", classes: "col-xs-4", itemsPerRow: 3 }, + tiny: { name: "Tiny", classes: "col-xs-2 item-compact", itemsPerRow: 6, rowHeightPx: 75, hideName: true }, + compact: { name: "Compact", classes: "col-xs-3", itemsPerRow: 4 }, + normal: { name: "Normal", classes: "col-xs-4", itemsPerRow: 3 }, + large: { name: "Large", classes: "col-xs-6", itemsPerRow: 2 }, list: { name: "List", classes: "col-xs-12 item-full-width", itemsPerRow: 1 }, + compactList: { name: "Compact list", classes: "col-xs-12 item-compact-list", itemsPerRow: 1, rowHeightPx: 30 }, }; // fields in either bundle or type record: @@@ -56,33 -74,288 +75,287 @@@ export function catalogSelectorDirectiv controller: ['$scope', '$element', '$timeout', '$q', '$uibModal', '$log', '$templateCache', 'paletteApi', 'paletteDragAndDropService', 'iconGenerator', 'composerOverrides', 'recentlyUsedService', controller], link: link, }; - } - function link($scope, $element, attrs, controller) { - let main = angular.element($element[0].querySelector(".catalog-palette-main")); - + function link($scope, $element, attrs, controller) { + let main = angular.element($element[0].querySelector(".catalog-palette-main")); - // repaginate when load completes (and items are shown), or it is resized - $scope.$watchGroup( - [ () => $scope.isLoading, () => main[0].offsetHeight, () => $scope.state.viewMode.name ], - (values) => controller.$timeout( () => repaginate($scope, $element) ) ); - // also repaginate on window resize - angular.element(window).bind('resize', () => repaginate($scope, $element)); - } + // repaginate when load completes (and items are shown), or it is resized + $scope.$watchGroup( - [() => $scope.isLoading, () => main[0].offsetHeight, () => $scope.state.viewMode.itemsPerRow], ++ [() => $scope.isLoading, () => main[0].offsetHeight, () => $scope.state.viewMode.name], + (values) => controller.$timeout(() => repaginate($scope, $element))); + // also repaginate on window resize + angular.element(window).bind('resize', () => repaginate($scope, $element)); - function repaginate($scope, $element) { - let rowsPerPage = $scope.rowsPerPage; - if (!rowsPerPage) { - let main = angular.element($element[0].querySelector(".catalog-palette-main")); - if (!main || main[0].offsetHeight==0) { - // no main, or hidden, or items per page fixed - return; + $scope.templateUrls = { + subhead: TEMPLATE_SUBHEAD_URL, + footer: TEMPLATE_FOOTER_URL } - let header = angular.element(main[0].querySelector(".catalog-palette-header")); - let footer = angular.element(main[0].querySelector(".catalog-palette-footer")); - rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor( (main[0].offsetHeight - header[0].offsetHeight - footer[0].offsetHeight - 16) / ($scope.state.viewMode.rowHeightPx || 96)) ); } - $scope.$apply( () => $scope.pagination.itemsPerPage = rowsPerPage * $scope.state.viewMode.itemsPerRow ); + + function controller($scope, $element, $timeout, $q, $uibModal, $log, $templateCache, paletteApi, paletteDragAndDropService, iconGenerator, composerOverrides, recentlyUsedService) { + this.$timeout = $timeout; + + $scope.viewModes = PALETTE_VIEW_MODES; + $scope.viewOrders = PALETTE_VIEW_ORDERS; + + if (!$scope.state) $scope.state = {}; + if (!$scope.state.viewMode) $scope.state.viewMode = PALETTE_VIEW_MODES.normal; + + $scope.pagination = { + page: 1, + itemsPerPage: $scope.state.viewMode.itemsPerRow * ($scope.rowsPerPage || 1) // will fill out after load + }; + + $scope.getEntityNameForPalette = function(item, entityName) { + return (composerOverrides.getEntityNameForPalette || + // above can be overridden with function of signature below to customize display name in palette + function(item, entityName, scope) { return entityName; } + )(item, entityName, $scope); + }; + + $scope.getPlaceHolder = function () { + return 'Search'; + }; + + $scope.isLoading = true; + + $scope.$watch('search', () => { + $scope.freeFormTile = { + symbolicName: $scope.search, + name: $scope.search, + displayName: $scope.search, + supertypes: ($scope.family ? [ $scope.family.superType ] : []), + }; + }); + + $scope.getItems = function (search) { + let defer = $q.resolve([]); + + switch ($scope.family) { + case EntityFamily.ENTITY: + case EntityFamily.SPEC: + defer = paletteApi.getTypes({params: {supertype: 'entity', fragment: search}}); + break; + case EntityFamily.POLICY: + defer = paletteApi.getTypes({params: {supertype: 'policy', fragment: search}}); + break; + case EntityFamily.ENRICHER: + defer = paletteApi.getTypes({params: {supertype: 'enricher', fragment: search}}); + break; + case EntityFamily.LOCATION: + defer = paletteApi.getLocations(); + break; + } + + return defer.then(data => { + data = $scope.filterPaletteItemsForMode(data, $scope); + data.forEach( recentlyUsedService.embellish ); + return data; + + }).catch(error => { + return []; + }).finally(() => { + $scope.isLoading = false; + }); + }; + function tryMarkUsed(item) { + try { + recentlyUsedService.markUsed(item); + } catch (e) { + // session storage can get full; usually the culprit is icons not this, + // but we may wish to clear out old items to ensure we don't bleed here + $log.warn("Could not mark item as used: "+item, e); + } + } + $scope.mouseInfoPopover = (item, enter) => { + if ($scope.popoverModal && $scope.popoverVisible && $scope.popover==item) { + // ignore if modal + return; + } + $scope.popoverModal = false; + if (enter) { + $scope.popover = item; + $scope.popoverVisible = true; + } else { + $scope.popoverVisible = false; + } + }; + $scope.onClickItem = (item, isInfoIcon, $event) => { + if (!isInfoIcon && $scope.iconSelects) { + $scope.onSelectItem(item); + } else if ($scope.popoverModal && $scope.popoverVisible && $scope.popover == item) { + $scope.closePopover(); + } else { + $scope.popover = item; + $scope.popoverVisible = true; + $scope.popoverModal = true; + } + $event.stopPropagation(); + }; + $scope.closePopover = () => { + $scope.popoverVisible = false; + $scope.popoverModal = false; + }; + $scope.getOnSelectText = function (item) { + if (!($scope.onSelectText)) return "Select"; + return $scope.onSelectText({item: item}); + }; + $scope.onSelectItem = function (item) { + $scope.closePopover(); + if (angular.isFunction($scope.onSelect)) { + tryMarkUsed(item); + $scope.onSelect({item: item}); + } + $scope.search = ''; + }; + $scope.onDragItem = function (item, event) { + let frame = document.createElement('div'); + frame.classList.add('drag-frame'); + event.target.appendChild(frame); + setTimeout(function() { + // can remove at end of this cycle, browser will have grabbed its drag image + frame.parentNode.removeChild(frame); + }, 0); + /* have tried many other ways to get a nice drag image; + this seems to work best, adding an empty div which forces the size to be larger, + so when grabbing the image it grabs the drop-shadow. + things that _didn't_ work include: + - styling event.target now then unstyling (normally this would work, in posts on the web, but it doesn't here; angular?) + - make a restyled cloned copy offscreen (this comes so close but remote img srcs aren't loaded + */ + + paletteDragAndDropService.dragStart(item); + }; + $scope.onDragEnd = function (item, event) { + paletteDragAndDropService.dragEnd(); + tryMarkUsed(item); + }; + + $scope.getOpenCatalogLink = (item) => { + return "/brooklyn-ui-catalog/#!/bundles/"+item.containingBundle.replace(":","/")+"/types/"+item.symbolicName+"/"+item.version; + }; + $scope.sortBy = function (order) { + let newFirst = {}; + if (order) { + newFirst[order.id] = order; + } + $scope.state.currentOrder = Object.assign(newFirst, $scope.state.currentOrder, newFirst); + $scope.state.currentOrderFields = []; + $scope.state.currentOrderValues = []; + Object.values($scope.state.currentOrder).forEach( it => { + $scope.state.currentOrderValues.push(it); + $scope.state.currentOrderFields.push(it.field); + }); + }; + if (!$scope.state.currentOrder) $scope.state.currentOrder = Object.assign({}, PALETTE_VIEW_ORDERS); + $scope.sortBy(); + + $scope.allowFreeForm = function () { + return [ + EntityFamily.LOCATION + ].indexOf($scope.family) > -1; + }; + $scope.isReserved = function () { + if (!$scope.reservedKeys || !angular.isArray($scope.reservedKeys)) { + return false; + } + return $scope.reservedKeys.indexOf($scope.search) > -1; + }; + $scope.onImageError = (scope, el, attrs) => { + $log.warn("Icon for "+attrs.itemId+" at "+angular.element(el).attr("src")+" could not be loaded; generating icon"); + angular.element(el).attr("src", iconGenerator(attrs.itemId)); + }; + + // Init + $scope.items = []; + function getDisplayTags(tags) { + if (!tags || !tags.length || !tags.reduce) return tags; + return tags.reduce((result, tag) => { + if (!(/[=:\[\]()]/.exec(tag))) { + result.push(tag); + } + return result; + }, []); + } + $scope.getItems().then((items)=> { + // add displayTags, as any tag that doesn't contain = : or ( ) [ ] + // any tag that is an object will be eliminated as it is toStringed to make [ object object ] + items.forEach(item => { + if (item.tags) { + item.displayTags = getDisplayTags(item.tags); + } + }); + $scope.items = items; + }); + $scope.lastUsedText = (item) => { + if (item==null) return ""; + let l = (Number)(item.lastUsed); + if (!l || isNaN(l) || l<=0) return ""; + if (l < 100000) return 'Preselected for inclusion in "Recent" filter.'; + return 'Last used: ' + distanceInWordsToNow(l, { includeSeconds: true, addSuffix: true }); + }; + + $scope.showPaletteControls = false; + $scope.onFiltersShown = () => { + $timeout( () => { + // check do we need to show the multiline + let filters = angular.element($element[0].querySelector(".filters")); + $scope.$apply( () => $scope.filterSettings.filtersMultilineAvailable = filters[0].scrollHeight > filters[0].offsetHeight + 6 ); + + repaginate($scope, $element); + } ); + }; + $scope.togglePaletteControls = () => { + $scope.showPaletteControls = !$scope.showPaletteControls; + $timeout( () => repaginate($scope, $element) ); + }; + $scope.toggleShowAllFilters = () => { + $scope.filterSettings.showAllFilters = !$scope.filterSettings.showAllFilters; + $timeout( () => repaginate($scope, $element) ); + }; + $scope.filterSettings = {}; + + $scope.filters = [ + { label: 'Recent', icon: 'clock-o', title: "Recently used and standard favorites", limitToOnePage: true, + filterInit: items => { + $scope.recentItems = items.filter( i => i.lastUsed && i.lastUsed>0 ); + $scope.recentItems.sort( (a,b) => b.lastUsed - a.lastUsed ); + return $scope.recentItems; + }, enabled: false }, + ]; + $scope.disableFilters = (showFilters) => { + $scope.filters.forEach( f => f.enabled = false ); + if (showFilters !== false) { + $scope.showPaletteControls = true; + } + }; + + // can be overridden to disable "open in catalog" button + $scope.allowOpenInCatalog = true; + + // this can be overridden for palette sections/modes which show a subset of the types returned by the server; + // this is applied when the data is received from the server. + // it is used by catalogSelectorFiltersFilter; + $scope.filterPaletteItemsForMode = (items) => items; + + // allow downstream to configure this controller and/or scope + (composerOverrides.configurePaletteController || function() {})(this, $scope, $element); + } + + function repaginate($scope, $element) { + let rowsPerPage = $scope.rowsPerPage; + if (!rowsPerPage) { + let main = angular.element($element[0].querySelector(".catalog-palette-main")); + if (!main || main[0].offsetHeight == 0) { + // no main, or hidden, or items per page fixed + return; + } + let header = angular.element(main[0].querySelector(".catalog-palette-header")); + let footer = angular.element(main[0].querySelector(".catalog-palette-footer")); + rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((main[0].offsetHeight - header[0].offsetHeight - footer[0].offsetHeight - 16) / ($scope.state.viewMode.rowHeightPx || 96))); + } + $scope.$apply(() => $scope.pagination.itemsPerPage = rowsPerPage * $scope.state.viewMode.itemsPerRow); + } } export function catalogSelectorSearchFilter() { http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/55ae032e/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less ----------------------------------------------------------------------
