...@@ -81,7 +81,7 @@ export default class ProjectFindFile { ...@@ -81,7 +81,7 @@ export default class ProjectFindFile {
// find file // find file
} }
// files paths load // files pathes load
load(url) { load(url) {
axios axios
.get(url) .get(url)
... ...
......
import $ from 'jquery'; import $ from 'jquery';
import '~/gl_dropdown';
class RefSelectDropdown { class RefSelectDropdown {
constructor($dropdownButton, availableRefs) { constructor($dropdownButton, availableRefs) {
... ...
......
<script> <script>
// import { sprintf, __ } from '~/locale'; // import { sprintf, __ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import { fieldTypes } from '../constants'; import { fieldTypes } from '../constants';
export default { export default {
components: { components: {
Modal: DeprecatedModal2, Modal,
LoadingButton, LoadingButton,
CodeBlock, CodeBlock,
}, },
... ...
......
<script> <script>
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
name: 'AssigneeTitle', name: 'AssigneeTitle',
...@@ -29,6 +30,11 @@ export default { ...@@ -29,6 +30,11 @@ export default {
return n__('Assignee', `%d Assignees`, assignees); return n__('Assignee', `%d Assignees`, assignees);
}, },
}, },
methods: {
trackEdit() {
trackEvent('click_edit_button', 'assignee');
},
},
}; };
</script> </script>
<template> <template>
...@@ -39,9 +45,7 @@ export default { ...@@ -39,9 +45,7 @@ export default {
v-if="editable" v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right" class="js-sidebar-dropdown-toggle edit-link float-right"
href="#" href="#"
data-track-event="click_edit_button" @click.prevent="trackEdit"
data-track-label="right_sidebar"
data-track-property="assignee"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
... ...
......
...@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; ...@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
components: { components: {
...@@ -51,6 +52,11 @@ export default { ...@@ -51,6 +52,11 @@ export default {
toggleForm() { toggleForm() {
this.edit = !this.edit; this.edit = !this.edit;
}, },
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'confidentiality');
},
updateConfidentialAttribute(confidential) { updateConfidentialAttribute(confidential) {
this.service this.service
.update('issue', { confidential }) .update('issue', { confidential })
...@@ -82,10 +88,7 @@ export default { ...@@ -82,10 +88,7 @@ export default {
v-if="isEditable" v-if="isEditable"
class="float-right confidential-edit" class="float-right confidential-edit"
href="#" href="#"
data-track-event="click_edit_button" @click.prevent="onEditClick"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
... ...
......
...@@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable'; ...@@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
components: { components: {
...@@ -65,6 +66,11 @@ export default { ...@@ -65,6 +66,11 @@ export default {
toggleForm() { toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
}, },
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'lock_issue');
},
updateLockedAttribute(locked) { updateLockedAttribute(locked) {
this.mediator.service this.mediator.service
.update(this.issuableType, { .update(this.issuableType, {
...@@ -108,10 +114,7 @@ export default { ...@@ -108,10 +114,7 @@ export default {
v-if="isEditable" v-if="isEditable"
class="float-right lock-edit" class="float-right lock-edit"
type="button" type="button"
data-track-event="click_edit_button" @click.prevent="onEditClick"
data-track-label="right_sidebar"
data-track-property="lock_issue"
@click.prevent="toggleForm"
> >
{{ __('Edit') }} {{ __('Edit') }}
</button> </button>
... ...
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import Tracking from '~/tracking';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications'; const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off'; const ICON_OFF = 'notifications-off';
...@@ -19,7 +19,6 @@ export default { ...@@ -19,7 +19,6 @@ export default {
icon, icon,
toggleButton, toggleButton,
}, },
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
...@@ -66,10 +65,7 @@ export default { ...@@ -66,10 +65,7 @@ export default {
// Component event emission. // Component event emission.
this.$emit('toggleSubscription', this.id); this.$emit('toggleSubscription', this.id);
this.track('toggle_button', { trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
property: 'notifications',
value: this.subscribed ? 0 : 1,
});
}, },
onClickCollapsedIcon() { onClickCollapsedIcon() {
this.$emit('toggleSidebar'); this.$emit('toggleSidebar');
... ...
......
import $ from 'jquery'; import $ from 'jquery';
import '~/gl_dropdown';
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
... ...
......
import _ from 'underscore'; import $ from 'jquery';
const DEFAULT_SNOWPLOW_OPTIONS = { const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl', namespace: 'gl',
...@@ -14,31 +14,18 @@ const DEFAULT_SNOWPLOW_OPTIONS = { ...@@ -14,31 +14,18 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
linkClickTracking: false, linkClickTracking: false,
}; };
const eventHandler = (e, func, opts = {}) => { const extractData = (el, opts = {}) => {
const el = e.target.closest('[data-track-event]'); const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset;
const action = el && el.dataset.trackEvent; let trackValue = el.dataset.trackValue || el.value || '';
if (!action) return; if (el.type === 'checkbox' && !el.checked) trackValue = false;
return [
let value = el.dataset.trackValue || el.value || undefined; trackEvent + (opts.suffix || ''),
if (el.type === 'checkbox' && !el.checked) value = false; {
label: trackLabel,
const data = { property: trackProperty,
label: el.dataset.trackLabel, value: trackValue,
property: el.dataset.trackProperty, },
value, ];
context: el.dataset.trackContext,
};
func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined));
};
const eventHandlers = (category, func) => {
const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts });
const handlers = [];
handlers.push({ name: 'click', func: handler() });
handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
return handlers;
}; };
export default class Tracking { export default class Tracking {
...@@ -52,43 +39,49 @@ export default class Tracking { ...@@ -52,43 +39,49 @@ export default class Tracking {
return typeof window.snowplow === 'function' && this.trackable(); return typeof window.snowplow === 'function' && this.trackable();
} }
static event(category = document.body.dataset.page, action = 'generic', data = {}) { static event(category = document.body.dataset.page, event = 'generic', data = {}) {
if (!this.enabled()) return false; if (!this.enabled()) return false;
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.'); if (!category) throw new Error('Tracking: no category provided for tracking.');
const { label, property, value, context } = data; return window.snowplow(
const contexts = context ? [context] : undefined; 'trackStructEvent',
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts); category,
event,
Object.assign({}, { label: '', property: '', value: '' }, data),
);
} }
static bindDocument(category = document.body.dataset.page, documentOverride = null) { constructor(category = document.body.dataset.page) {
const el = documentOverride || document; this.category = category;
if (!this.enabled() || el.trackingBound) return []; }
el.trackingBound = true; bind(container = document) {
if (!this.constructor.enabled()) return;
container.querySelectorAll(`[data-track-event]`).forEach(el => {
if (this.customHandlingFor(el)) return;
// jquery is required for select2, so we use it always
// see: https://github.com/select2/select2/issues/4686
$(el).on('click', this.eventHandler(this.category));
});
}
const handlers = eventHandlers(category, (...args) => this.event(...args)); customHandlingFor(el) {
handlers.forEach(event => el.addEventListener(event.name, event.func)); const classes = el.classList;
return handlers;
// bootstrap dropdowns
if (classes.contains('dropdown')) {
$(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' }));
$(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' }));
return true;
} }
static mixin(opts) { return false;
return { }
data() {
return { eventHandler(category = null, opts = {}) {
tracking: { return e => {
// eslint-disable-next-line no-underscore-dangle this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts));
category: this.$options.name || this.$options._componentTag,
},
};
},
methods: {
track(action, data) {
const category = opts.category || data.category || this.tracking.category;
Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data });
},
},
}; };
} }
} }
...@@ -96,7 +89,7 @@ export default class Tracking { ...@@ -96,7 +89,7 @@ export default class Tracking {
export function initUserTracking() { export function initUserTracking() {
if (!Tracking.enabled()) return; if (!Tracking.enabled()) return;
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions }; const opts = Object.assign({}, DEFAULT_SNOWPLOW_OPTIONS, window.snowplowOptions);
window.snowplow('newTracker', opts.namespace, opts.hostname, opts); window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
window.snowplow('enableActivityTracking', 30, 30); window.snowplow('enableActivityTracking', 30, 30);
...@@ -104,6 +97,4 @@ export function initUserTracking() { ...@@ -104,6 +97,4 @@ export function initUserTracking() {
if (opts.formTracking) window.snowplow('enableFormTracking'); if (opts.formTracking) window.snowplow('enableFormTracking');
if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
} }
<script>
import $ from 'jquery';
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default {
name: 'DeprecatedModal2', // use GlModal instead
props: {
id: {
type: String,
required: false,
default: null,
},
modalSize: {
type: String,
required: false,
default: 'md',
validator: value => sizeVariants.includes(value),
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.includes(value),
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
computed: {
modalSizeClass() {
return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
},
},
mounted() {
$(this.$el)
.on('shown.bs.modal', this.opened)
.on('hidden.bs.modal', this.closed);
},
beforeDestroy() {
$(this.$el)
.off('shown.bs.modal', this.opened)
.off('hidden.bs.modal', this.closed);
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
opened() {
this.$emit('open');
},
closed() {
this.$emit('closed');
},
},
};
</script>
<template>
<div :id="id" class="modal fade" tabindex="-1" role="dialog">
<div :class="modalSizeClass" class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title">
<slot name="title"> {{ headerTitleText }} </slot>
</h4>
<button
:aria-label="s__('Modal|Close')"
type="button"
class="close js-modal-close-action"
data-dismiss="modal"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body"><slot></slot></div>
<div class="modal-footer">
<slot name="footer">
<button
type="button"
class="btn js-modal-cancel-action qa-modal-cancel-button"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
<button
:class="`btn-${footerPrimaryButtonVariant}`"
type="button"
class="btn js-modal-primary-action qa-modal-primary-button"
data-dismiss="modal"
@click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script> <script>
// This file was only introduced to not break master and shall be delete soon. import $ from 'jquery';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
export default DeprecatedModal2; const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default {
name: 'GlModal',
props: {
id: {
type: String,
required: false,
default: null,
},
modalSize: {
type: String,
required: false,
default: 'md',
validator: value => sizeVariants.includes(value),
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.includes(value),
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
computed: {
modalSizeClass() {
return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
},
},
mounted() {
$(this.$el)
.on('shown.bs.modal', this.opened)
.on('hidden.bs.modal', this.closed);
},
beforeDestroy() {
$(this.$el)
.off('shown.bs.modal', this.opened)
.off('hidden.bs.modal', this.closed);
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
opened() {
this.$emit('open');
},
closed() {
this.$emit('closed');
},
},
};
</script> </script>
<template>
<div :id="id" class="modal fade" tabindex="-1" role="dialog">
<div :class="modalSizeClass" class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title">
<slot name="title"> {{ headerTitleText }} </slot>
</h4>
<button
:aria-label="s__('Modal|Close')"
type="button"
class="close js-modal-close-action"
data-dismiss="modal"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body"><slot></slot></div>
<div class="modal-footer">
<slot name="footer">
<button
type="button"
class="btn js-modal-cancel-action qa-modal-cancel-button"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
<button
:class="`btn-${footerPrimaryButtonVariant}`"
type="button"
class="btn js-modal-primary-action qa-modal-primary-button"
data-dismiss="modal"
@click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
...@@ -27,7 +27,8 @@ export default { ...@@ -27,7 +27,8 @@ export default {
/** /**
pageInfo will come from the headers of the API call pageInfo will come from the headers of the API call
there should be a function that constructs the pageInfo for this component in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component
This is an example: This is an example:
... ...
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
this.$emit('projectClicked', project); this.$emit('projectClicked', project);
}, },
isSelected(project) { isSelected(project) {
return Boolean(_.find(this.selectedProjects, { id: project.id })); return Boolean(_.findWhere(this.selectedProjects, { id: project.id }));
}, },
onInput: _.debounce(function debouncedOnInput() { onInput: _.debounce(function debouncedOnInput() {
this.$emit('searched', this.searchQuery); this.$emit('searched', this.searchQuery);
... ...
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import csrf from '../lib/utils/csrf';
Vue.use(VueResource);
// Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb
Vue.http.interceptors.push((request, next) => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;
next(() => {
window.activeVueResources -= 1;
});
});
// Inject CSRF token and parse headers.
// New Vue Resource version uses Headers, we are expecting a plain object to render pagination
// and polling.
Vue.http.interceptors.push((request, next) => {
request.headers.set(csrf.headerKey, csrf.token);
next(response => {
// Headers object has a `forEach` property that iterates through all values.
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// eslint-disable-next-line no-param-reassign
response.headers = headers;
});
});
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
@import 'framework/issue_box'; @import 'framework/issue_box';
@import 'framework/lists'; @import 'framework/lists';
@import 'framework/logo'; @import 'framework/logo';
@import 'framework/job_log';
@import 'framework/markdown_area'; @import 'framework/markdown_area';
@import 'framework/media_object'; @import 'framework/media_object';
@import 'framework/modal'; @import 'framework/modal';
... ...
......
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
} }
.sidebar-top-level-items > li > a { .sidebar-top-level-items > li > a {
min-height: 45px; min-height: 44px;
} }
.fly-out-top-item { .fly-out-top-item {
... ...
......
...@@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); ...@@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: $flash-container-top; top: $flash-container-top;
z-index: 251; z-index: 200;
.flash-content { .flash-content {
box-shadow: 0 2px 4px 0 $notification-box-shadow-color; box-shadow: 0 2px 4px 0 $notification-box-shadow-color;
... ...
......
...@@ -245,7 +245,7 @@ ...@@ -245,7 +245,7 @@
.select2-highlighted { .select2-highlighted {
.group-result { .group-result {
.group-path { .group-path {
color: $gray-800; color: $white-light;
} }
} }
} }
... ...
......
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
position: absolute; position: absolute;
top: $gl-padding; top: $gl-padding;
bottom: $gl-padding; bottom: $gl-padding;
left: map-get($spacers, 2) - px-to-rem(1px); left: map-get($spacers, 2) - 1px;
} }
&-row { &-row {
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
* 2px extra is to give a little more height than needed * 2px extra is to give a little more height than needed
* to hide timeline line before/after the element starts/ends * to hide timeline line before/after the element starts/ends
*/ */
height: map-get($spacers, 4) + px-to-rem(2px); height: map-get($spacers, 4) + 2px;
z-index: 1; z-index: 1;
position: relative; position: relative;
top: -3px; top: -3px;
... ...
......
...@@ -877,16 +877,11 @@ pre.light-well { ...@@ -877,16 +877,11 @@ pre.light-well {
flex-direction: column; flex-direction: column;
// Disable Flexbox for admin page // Disable Flexbox for admin page
&.admin-projects, &.admin-projects {
&.group-settings-projects {
display: block; display: block;
.project-row { .project-row {
display: block; display: block;
.description > p {
margin-bottom: 0;
}
} }
} }
... ...
......