Merge pull request #6903 from marshmalien/copyProjectRevisionIcon

Add copy icon and update Tooltip content to project revision sha
This commit is contained in:
Marliana Lara 2017-07-10 11:46:14 -04:00 committed by GitHub
commit 35e28e9347
17 changed files with 286 additions and 151 deletions

View File

@ -5,3 +5,4 @@
@import 'popover/_index';
@import 'tabs/_index';
@import 'utility/_index';
@import 'truncate/_index';

View File

@ -1,6 +1,6 @@
function ComponentsStrings (BaseString) {
BaseString.call(this, 'components');
let t = this.t;
let ns = this.components;
@ -42,6 +42,11 @@ function ComponentsStrings (BaseString) {
ns.lookup = {
NOT_FOUND: t('That value was not found. Please enter or select a valid value.')
};
ns.truncate = {
DEFAULT: t('Copy full revision to clipboard.'),
COPIED: t('Copied to clipboard.')
}
}
ComponentsStrings.$inject = ['BaseStringService'];

View File

@ -19,6 +19,7 @@ import panelBody from './panel/body.directive';
import popover from './popover/popover.directive';
import tab from './tabs/tab.directive';
import tabGroup from './tabs/group.directive';
import truncate from './truncate/truncate.directive';
import BaseInputController from './input/base.controller';
import ComponentsStrings from './components.strings';
@ -46,7 +47,8 @@ angular
.directive('atPopover', popover)
.directive('atTab', tab)
.directive('atTabGroup', tabGroup)
.service('BaseInputController', BaseInputController)
.service('ComponentsStrings', ComponentsStrings);
.directive('atTruncate', truncate)
.service('ComponentsStrings', ComponentsStrings)
.service('BaseInputController', BaseInputController);

View File

@ -7,13 +7,19 @@
}
.at-Popover-icon {
.at-mixin-ButtonIcon();
.at-mixin-ButtonIcon();
color: @at-color-icon-popover;
font-size: @at-font-size-icon;
padding: 0;
padding: 1px;
margin: 0;
}
.at-Popover-icon--defaultCursor {
i > {
cursor: default;
}
}
.at-Popover-container {
visibility: hidden;
opacity: 0;
@ -24,7 +30,7 @@
height: auto;
position: fixed;
z-index: 2000;
margin: 0 0 0 18px;
margin: 0;
border-radius: @at-border-radius;
box-shadow: 0 5px 10px rgba(0,0,0, 0.2);
transition: opacity .15s linear;
@ -36,7 +42,7 @@
position: fixed;
z-index: 1999;
padding: 0;
margin: 8px 0 0 3px;
margin: 0;
}
.at-Popover-title {
@ -48,5 +54,5 @@
.at-Popover-text {
margin: 0;
padding: 0;
font-size: @at-font-size-body;
font-size: @at-font-size;
}

View File

@ -1,10 +1,22 @@
const DEFAULT_POSITION = 'right';
const DEFAULT_ACTION = 'click';
const DEFAULT_ICON = 'fa fa-question-circle';
const DEFAULT_ALIGNMENT = 'inline';
const DEFAULT_ARROW_HEIGHT = 14;
const DEFAULT_PADDING = 10;
const DEFAULT_REFRESH_DELAY = 50;
const DEFAULT_RESET_ON_EXIT = false;
function atPopoverLink (scope, el, attr, controllers) {
let popoverController = controllers[0];
let container = el[0];
let popover = container.getElementsByClassName('at-Popover-container')[0];
let icon = container.getElementsByTagName('i')[0];
popoverController.init(scope, container, icon, popover);
let done = scope.$watch('state', () => {
popoverController.init(scope, container, icon, popover);
done();
});
}
function AtPopoverController () {
@ -13,13 +25,35 @@ function AtPopoverController () {
let container;
let icon;
let popover;
let scope;
vm.init = (scope, _container_, _icon_, _popover_) => {
vm.init = (_scope_, _container_, _icon_, _popover_) => {
scope = _scope_;
icon = _icon_;
popover = _popover_;
scope.inline = true;
icon.addEventListener('click', vm.createDisplayListener());
scope.popover = scope.state.popover || {};
scope.popover.text = scope.state.help_text || scope.popover.text;
scope.popover.title = scope.state.label || scope.popover.title;
scope.popover.inline = scope.popover.inline || DEFAULT_ALIGNMENT;
scope.popover.position = scope.popover.position || DEFAULT_POSITION;
scope.popover.icon = scope.popover.icon || DEFAULT_ICON;
scope.popover.on = scope.popover.on || DEFAULT_ACTION;
scope.popover.resetOnExit = scope.popover.resetOnExit || DEFAULT_RESET_ON_EXIT;
if (scope.popover.resetOnExit) {
scope.originalText = scope.popover.text;
scope.originalTitle = scope.popover.title;
}
icon.addEventListener(scope.popover.on, vm.createDisplayListener());
scope.$watch('popover.text', vm.refresh);
if (scope.popover.click) {
icon.addEventListener('click', scope.popover.click);
}
};
vm.createDismissListener = (createEvent) => {
@ -30,16 +64,30 @@ function AtPopoverController () {
return;
}
vm.open = false;
vm.dismiss();
popover.style.visibility = 'hidden';
popover.style.opacity = 0;
if (scope.popover.on === 'mouseenter') {
icon.removeEventListener('mouseleave', vm.dismissListener);
} else {
window.addEventListener(scope.popover.on, vm.dismissListener);
}
window.removeEventListener('click', vm.dismissListener);
window.removeEventListener('resize', vm.dismissListener);
};
};
vm.dismiss = (refresh) => {
if (!refresh && scope.popover.resetOnExit) {
scope.popover.text = scope.originalText;
scope.popover.title = scope.originalTitle;
}
vm.open = false;
popover.style.visibility = 'hidden';
popover.style.opacity = 0;
};
vm.isClickWithinPopover = (event, popover) => {
let box = popover.getBoundingClientRect();
@ -61,38 +109,96 @@ function AtPopoverController () {
event.stopPropagation();
vm.open = true;
let arrow = popover.getElementsByClassName('at-Popover-arrow')[0];
let iPos = icon.getBoundingClientRect();
let pPos = popover.getBoundingClientRect();
let wHeight = window.clientHeight;
let pHeight = pPos.height;
let cx = Math.floor(iPos.left + (iPos.width / 2));
let cy = Math.floor(iPos.top + (iPos.height / 2));
arrow.style.top = (iPos.top - iPos.height) + 'px';
arrow.style.left = iPos.right + 'px';
if (cy < (pHeight / 2)) {
popover.style.top = '10px';
} else {
popover.style.top = (cy - pHeight / 2) + 'px';
}
popover.style.left = cx + 'px';
popover.style.visibility = 'visible';
popover.style.opacity = 1;
vm.display();
vm.dismissListener = vm.createDismissListener(event);
window.addEventListener('click', vm.dismissListener);
if (scope.popover.on === 'mouseenter') {
icon.addEventListener('mouseleave', vm.dismissListener);
} else {
window.addEventListener(scope.popover.on, vm.dismissListener);
}
window.addEventListener('resize', vm.dismissListener);
};
};
vm.refresh = () => {
if (!vm.open) {
return;
}
vm.dismiss(true);
window.setTimeout(vm.display, DEFAULT_REFRESH_DELAY);
};
vm.getPositions = () => {
let arrow = popover.getElementsByClassName('at-Popover-arrow')[0];
arrow.style.lineHeight = `${DEFAULT_ARROW_HEIGHT}px`;
arrow.children[0].style.lineHeight = `${DEFAULT_ARROW_HEIGHT}px`;
let data = {
arrow,
icon: icon.getBoundingClientRect(),
popover: popover.getBoundingClientRect(),
windowHeight: window.innerHeight
};
data.cx = Math.floor(data.icon.left + (data.icon.width / 2));
data.cy = Math.floor(data.icon.top + (data.icon.height / 2));
return data;
};
vm.display = () => {
vm.open = true;
let positions = vm.getPositions();
popover.style.visibility = 'visible';
popover.style.opacity = 1;
if (scope.popover.position === 'right') {
vm.displayRight(positions);
} else if (scope.popover.position === 'top') {
vm.displayTop(positions);
}
};
vm.displayRight = (pos) => {
let arrowTop = Math.floor((pos.cy - (pos.icon.height / 2)));
let arrowLeft = pos.cx + DEFAULT_PADDING;
let popoverTop;
let popoverLeft = arrowLeft + DEFAULT_PADDING - 1;
if (pos.cy < (pos.popover.height / 2)) {
popoverTop = DEFAULT_PADDING;
} else {
popoverTop = Math.floor((pos.cy - pos.popover.height / 2));
}
pos.arrow.style.top = `${arrowTop}px`;
pos.arrow.style.left = `${arrowLeft}px`;
popover.style.top = `${popoverTop}px`;
popover.style.left = `${popoverLeft}px`;
};
vm.displayTop = (pos) => {
let arrowTop = pos.icon.top - pos.icon.height - DEFAULT_PADDING;
let arrowLeft = Math.floor(pos.icon.right - pos.icon.width - (pos.arrow.style.width / 2));
let popoverTop = pos.icon.top - pos.popover.height - pos.icon.height - 5;
let popoverLeft = Math.floor(pos.cx - (pos.popover.width / 2));
pos.arrow.style.top = `${arrowTop}px`;
pos.arrow.style.left = `${arrowLeft}px`;
popover.style.top = `${popoverTop}px`;
popover.style.left = `${popoverLeft}px`;
};
}
function atPopover (pathService) {
@ -115,4 +221,4 @@ atPopover.$inject = [
'PathService'
];
export default atPopover;
export default atPopover;

View File

@ -1,16 +1,16 @@
<div ng-show="state.help_text"
class="at-Popover"
ng-class="{ 'at-Popover--inline': inline }">
<span class="at-Popover-icon">
<i class="fa fa-question-circle"></i>
<div ng-show="popover.text" class="at-Popover" ng-class="{ 'at-Popover--inline': popover.inline }">
<span class="at-Popover-icon"
ng-class="{ 'at-Popover-icon--defaultCursor': popover.on === 'mouseenter' && !popover.click }">
<i class="fa {{ popover.icon }}"></i>
</span>
<div class="at-Popover-container">
<div class="at-Popover-arrow">
<i class="fa fa-caret-left fa-2x"></i>
<i ng-if="popover.position === 'right'" class="fa fa-caret-left fa-2x"></i>
<i ng-if="popover.position === 'top'" class="fa fa-caret-down fa-2x"></i>
</div>
<div class="at-Popover-content">
<h4 class="at-Popover-title">{{::state.label}}</h4>
<p class="at-Popover-text">{{::state.help_text}}</p>
<h4 ng-if="popover.title" class="at-Popover-title">{{ popover.title }}</h4>
<p ng-if="popover.text" class="at-Popover-text">{{ popover.text }}</p>
</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
.at-Truncate {
display: flex;
align-items: center;
.at-Truncate-text {
font-family: monospace;
}
.at-Truncate-copy {
color: @at-gray-dark-2x;
cursor: pointer;
margin-left: 10px;
i:hover {
color: @at-blue;
}
}
.at-Truncate-textarea {
background: transparent;
border: none;
box-shadow: none;
height: 2em;
left: 0px;
outline: none;
padding: 0px;
position: fixed;
top: 0px;
width: 2em;
}
}

View File

@ -0,0 +1,68 @@
function atTruncateLink (scope, el, attr, ctrl) {
let truncateController = ctrl;
let string = attr.string;
let maxlength = attr.maxlength;
truncateController.init(el, string, maxlength);
}
function AtTruncateController (strings) {
let vm = this;
let el;
let string;
let maxlength;
vm.strings = strings;
vm.init = (_el_, _string_, _maxlength_) => {
el = _el_;
string = _string_;
maxlength = _maxlength_;
vm.truncatedString = string.substring(0, maxlength);
};
vm.tooltip = {
popover: {
text: vm.strings.components.truncate.DEFAULT,
on: 'mouseover',
position: 'top',
icon: 'fa fa-clone',
resetOnExit: true,
click: copyToClipboard
}
};
function copyToClipboard() {
vm.tooltip.popover.text = vm.strings.components.truncate.COPIED;
let textarea = el[0].getElementsByClassName('at-Truncate-textarea')[0];
textarea.value = string;
textarea.select();
document.execCommand('copy');
};
}
AtTruncateController.$inject = ['ComponentsStrings'];
function atTruncate(pathService) {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl: pathService.getPartialPath('components/truncate/truncate'),
controller: AtTruncateController,
controllerAs: 'vm',
link: atTruncateLink,
scope: {
state: '=',
maxLength: '@',
string: '@'
}
}
}
atTruncate.$inject = [
'PathService'
];
export default atTruncate;

View File

@ -0,0 +1,9 @@
<div class="at-Truncate">
<div class="at-Truncate-tag">
<span class="at-Truncate-text">{{vm.truncatedString}}</span>
</div>
<div class="at-Truncate-copy" data-placement="top">
<at-popover position="top" on="mouseover" state="vm.tooltip"></at-popover>
</div>
<textarea class="at-Truncate-textarea"></textarea>
</div>

View File

@ -108,10 +108,6 @@
margin-left: 10px;
}
.JobResults-resultRowText--revision{
font-family: monospace;
}
.JobResults-resultRowText--instanceGroup {
display: flex;
}

View File

@ -247,9 +247,8 @@
<label class="JobResults-resultRowLabel">
Revision
</label>
<div class="JobResults-resultRowText JobResults-resultRowText--revision">
{{ job.scm_revision }}
</div>
<at-truncate string="{{job.scm_revision}}" maxLength="7" class="JobResults-resultRowText">
</at-truncate>
</div>
<!-- PLAYBOOK DETAIL -->

View File

@ -13,10 +13,9 @@ import { N_ } from '../i18n';
import GetProjectPath from './factories/get-project-path.factory';
import GetProjectIcon from './factories/get-project-icon.factory';
import GetProjectToolTip from './factories/get-project-tool-tip.factory';
import revisions from './revisions/main';
export default
angular.module('Projects', [revisions.name])
angular.module('Projects', [])
.controller('ProjectsList', ProjectsList)
.controller('ProjectsAdd', ProjectsAdd)
.controller('ProjectsEdit', ProjectsEdit)

View File

@ -1,11 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import revisions from './revisions.directive';
export default
angular.module('revisions', [])
.directive('revisions', revisions);

View File

@ -1,22 +0,0 @@
@import "./client/src/shared/branding/colors.default.less";
.RevisionHash {
display: flex;
align-items: center;
}
.RevisionHash-name {
font-family: monospace;
}
.RevisionHash-copy {
color: @default-link;
text-transform: uppercase;
cursor: pointer;
font-size: 11px;
margin-left: 10px;
}
.RevisionHash-copy:hover {
color: @default-link-hov;
}

View File

@ -1,51 +0,0 @@
export default
[ 'templateUrl',
'Rest',
'$q',
'$filter',
function(templateUrl, Rest, $q, $filter) {
return {
restrict: 'E',
scope: false,
templateUrl: templateUrl('projects/revisions/revisions'),
link: function(scope) {
let full_revision = scope.project.scm_revision;
scope.revisionHash = $filter('limitTo')(full_revision, 7, 0);
scope.count = scope.project.scm_revision.length;
scope.copyRevisionHash = function() {
let textArea = document.createElement("textarea");
// Place in top-left corner of screen regardless of scroll position.
textArea.style.position = 'fixed';
textArea.style.top = 0;
textArea.style.left = 0;
// Ensure it has a small width and height. Setting to 1px / 1em
// doesn't work as this gives a negative w/h on some browsers.
textArea.style.width = '2em';
textArea.style.height = '2em';
// We don't need padding, reducing the size if it does flash render.
textArea.style.padding = 0;
// Clean up any borders.
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
// Avoid flash of white box if rendered for any reason.
textArea.style.background = 'transparent';
textArea.value = full_revision;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
};
}
};
}
];

View File

@ -1,6 +0,0 @@
<div class="RevisionHash-tag">
<span class="RevisionHash-name">{{revisionHash}}</span>
</div>
<div class="RevisionHash-copy" ng-if="count > 7" aw-tool-tip="Copy full revision to clipboard" data-placement="top" ng-click="copyRevisionHash()">
Copy
</div>

View File

@ -525,7 +525,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
Attr(field, 'columnClass') : "";
html += `
<td ${classList}>
<revisions class=\"RevisionHash\"></revisions>
<at-truncate string="{{project.scm_revision}}" maxLength="7"></at-truncate>
</td>`;
} else if (field.type === 'badgeCount') {
html = BadgeCount(params);