Merge remote-tracking branch 'origin/master' into dev-master

This commit is contained in:
DJ Mountney 2018-10-25 08:48:14 -07:00
commit fee6989fa0
221 changed files with 3511 additions and 1684 deletions

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
key: "ruby-2.4.4-debian-stretch-with-yarn"
key: "ruby-2.4.5-debian-stretch-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@ -75,11 +75,6 @@ stages:
- mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables
script:
- export RAILS5=${RAILS5}
- export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
.rails5: &rails5
allow_failure: true
only:
@ -139,7 +134,7 @@ stages:
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
- apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- chmod 755 $SCRIPT_NAME
- chmod 755 $(basename $SCRIPT_NAME)
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
@ -150,7 +145,6 @@ stages:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *rails5-variables
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@ -594,7 +588,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop"
key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
@ -723,7 +717,7 @@ gitlab:assets:compile:
- public/assets/
karma:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *dedicated-no-docs-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@ -929,3 +923,93 @@ no_ee_check:
- scripts/no-ee-check
only:
- //@gitlab-org/gitlab-ce
# GitLab Review apps
review:
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: test
allow_failure: true
before_script:
- gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG"
DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN"
GITLAB_HELM_CHART_REF: "master"
script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- source ./scripts/review_apps/review-apps.sh
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
- check_kube_domain
- download_gitlab_chart
- ensure_namespace
- install_tiller
- create_secret
- install_external_dns
- deploy
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN
on_stop: stop_review
only:
refs:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
kubernetes: active
except:
refs:
- master
- /(^docs[\/-].*|.*-docs$)/
stop_review:
<<: *single-script-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: test
allow_failure: true
cache: {}
dependencies: []
variables:
SCRIPT_NAME: "review_apps/review-apps.sh"
script:
- source $(basename "${SCRIPT_NAME}")
- delete
- cleanup
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
only:
refs:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
kubernetes: active
except:
- master
- /(^docs[\/-].*|.*-docs$)/
schedule:review_apps_cleanup:
<<: *dedicated-no-docs-pull-cache-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: build
allow_failure: true
cache: {}
dependencies: []
before_script:
- gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
environment:
name: review/auto-cleanup
action: stop
only:
refs:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
except:
- tags
- /(^docs[\/-].*|.*-docs$)/

View file

@ -16,7 +16,6 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
- [ ] Add the ~"Merge into Security" label to all of the MRs.
- [ ] Make sure all MRs have a link in the [links section](#links)
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script

View file

@ -1 +1 @@
2.4.4
2.4.5

View file

@ -1 +1 @@
0.125.1
0.126.0

View file

@ -417,8 +417,7 @@ end
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'google-protobuf', '~> 3.6'
gem 'toml-rb', '~> 1.0.0', require: false

View file

@ -303,7 +303,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1)
google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@ -1005,7 +1005,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (= 3.5.1)
google-protobuf (~> 3.6)
gpgme
grape (~> 1.1)
grape-entity (~> 0.7.1)

View file

@ -306,7 +306,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1)
google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@ -1014,7 +1014,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (= 3.5.1)
google-protobuf (~> 3.6)
gpgme
grape (~> 1.1)
grape-entity (~> 0.7.1)

View file

@ -6,10 +6,12 @@ import Pager from './pager';
import { localTimeAgo } from './lib/utils/datetime_utility';
export default class Activities {
constructor() {
Pager.init(20, true, false, data => data, this.updateTooltips);
constructor(container = '') {
this.container = container;
$('.event-filter-link').on('click', (e) => {
Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
$('.event-filter-link').on('click', e => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
this.reloadActivities();
@ -22,7 +24,7 @@ export default class Activities {
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, data => data, this.updateTooltips);
Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
}
toggleFilter(sender) {

View file

@ -1,6 +1,6 @@
import $ from 'jquery';
export const addTooltipToEl = (el) => {
export const addTooltipToEl = el => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
@ -14,17 +14,18 @@ export default () => {
const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
if (breadcrumbs) {
const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown'))
const topLevelLinks = [...breadcrumbs.children]
.filter(el => !el.classList.contains('dropdown'))
.map(el => el.querySelector('a'))
.filter(el => el);
const $expander = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach(el => addTooltipToEl(el));
$expander.closest('.dropdown')
.on('show.bs.dropdown hide.bs.dropdown', (e) => {
$('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open')
.tooltip('hide');
});
$expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => {
$('.js-breadcrumbs-collapsed-expander', e.currentTarget)
.toggleClass('open')
.tooltip('hide');
});
}
};

View file

@ -12,16 +12,16 @@ export default class BuildArtifacts {
}
// eslint-disable-next-line class-methods-use-this
disablePropagation() {
$('.top-block').on('click', '.download', function (e) {
$('.top-block').on('click', '.download', function(e) {
return e.stopPropagation();
});
return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {
return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
return e.stopImmediatePropagation();
});
}
// eslint-disable-next-line class-methods-use-this
setupEntryClick() {
return $('.tree-holder').on('click', 'tr[data-link]', function () {
return $('.tree-holder').on('click', 'tr[data-link]', function() {
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
}
@ -37,11 +37,15 @@ export default class BuildArtifacts {
// We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name
$('.js-artifact-tree-row')
.on('mouseenter', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
.on('mouseenter', e => {
$(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('show');
})
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
.on('mouseleave', e => {
$(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('hide');
});
}
}

View file

@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => `
const errorList = [].concat(errors).map(
errorString => `
<li>
${_.escape(errorString)}
</li>
`);
`,
);
return `
<p>
@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) {
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
constructor({
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
@ -58,18 +54,21 @@ export default class AjaxVariableList {
// to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, {
variables_attributes: this.variableList.getAllData(),
}, {
// We want to be able to process the `res.data` from a 400 error response
// and print the validation messages such as duplicate variable keys
validateStatus: status => (
status >= statusCodes.OK &&
status < statusCodes.MULTIPLE_CHOICES
) ||
status === statusCodes.BAD_REQUEST,
})
.then((res) => {
return axios
.patch(
this.saveEndpoint,
{
variables_attributes: this.variableList.getAllData(),
},
{
// We want to be able to process the `res.data` from a 400 error response
// and print the validation messages such as duplicate variable keys
validateStatus: status =>
(status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) ||
status === statusCodes.BAD_REQUEST,
},
)
.then(res => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
@ -90,18 +89,21 @@ export default class AjaxVariableList {
}
updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({
...variableMap,
[variable.key]: variable,
}), {});
const persistedVariableMap = [].concat(persistedVariables).reduce(
(variableMap, variable) => ({
...variableMap,
[variable.key]: variable,
}),
{},
);
this.container.querySelectorAll('.js-row').forEach((row) => {
this.container.querySelectorAll('.js-row').forEach(row => {
// If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) {
row.remove();
// Update the ID input so any future edits and `_destroy` will apply on the BE
// Update the ID input so any future edits and `_destroy` will apply on the BE
} else {
const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key];

View file

@ -16,10 +16,7 @@ function createEnvironmentItem(value) {
}
export default class VariableList {
constructor({
container,
formField,
}) {
constructor({ container, formField }) {
this.$container = $(container);
this.formField = formField;
this.environmentDropdownMap = new WeakMap();
@ -71,7 +68,7 @@ export default class VariableList {
this.initRow(rowEl);
});
this.$container.on('click', '.js-row-remove-button', (e) => {
this.$container.on('click', '.js-row-remove-button', e => {
e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row'));
});
@ -81,7 +78,7 @@ export default class VariableList {
.join(',');
// Remove any empty rows except the last row
this.$container.on('blur', inputSelector, (e) => {
this.$container.on('blur', inputSelector, e => {
const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
@ -136,7 +133,7 @@ export default class VariableList {
$rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults
Object.keys(this.inputMap).forEach((name) => {
Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default);
});
@ -171,7 +168,7 @@ export default class VariableList {
}
checkIfRowTouched($row) {
return Object.keys(this.inputMap).some((name) => {
return Object.keys(this.inputMap).some(name => {
const entry = this.inputMap[name];
const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default;
@ -190,11 +187,14 @@ export default class VariableList {
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
const validRows = this.$container
.find('.js-row')
.toArray()
.slice(0, -1);
return validRows.map((rowEl) => {
return validRows.map(rowEl => {
const resultant = {};
Object.keys(this.inputMap).forEach((name) => {
Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector);
if ($input.length) {
@ -207,11 +207,16 @@ export default class VariableList {
}
getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray()
.reduce((prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}), {});
const valueMap = this.$container
.find(this.inputMap.environment_scope.selector)
.toArray()
.reduce(
(prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}),
{},
);
return Object.keys(valueMap).map(createEnvironmentItem);
}

View file

@ -2,10 +2,7 @@ import $ from 'jquery';
import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page
export default function setupNativeFormVariableList({
container,
formField = 'variables',
}) {
export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
const $container = $(container);
const variableList = new VariableList({

View file

@ -76,12 +76,8 @@ export default class ClusterStore {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
status,
status_reason: statusReason,
} = serverAppEntry;
serverState.applications.forEach(serverAppEntry => {
const { name: appId, status, status_reason: statusReason } = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),

View file

@ -24,36 +24,44 @@ class CommentTypeToggle {
setConfig() {
const config = {
InputSetter: [{
input: this.noteTypeInput,
valueAttribute: 'data-value',
},
{
input: this.submitButton,
valueAttribute: 'data-submit-text',
}],
InputSetter: [
{
input: this.noteTypeInput,
valueAttribute: 'data-value',
},
{
input: this.submitButton,
valueAttribute: 'data-submit-text',
},
],
};
if (this.closeButton) {
config.InputSetter.push({
input: this.closeButton,
valueAttribute: 'data-close-text',
}, {
input: this.closeButton,
valueAttribute: 'data-close-text',
inputAttribute: 'data-alternative-text',
});
config.InputSetter.push(
{
input: this.closeButton,
valueAttribute: 'data-close-text',
},
{
input: this.closeButton,
valueAttribute: 'data-close-text',
inputAttribute: 'data-alternative-text',
},
);
}
if (this.reopenButton) {
config.InputSetter.push({
input: this.reopenButton,
valueAttribute: 'data-reopen-text',
}, {
input: this.reopenButton,
valueAttribute: 'data-reopen-text',
inputAttribute: 'data-alternative-text',
});
config.InputSetter.push(
{
input: this.reopenButton,
valueAttribute: 'data-reopen-text',
},
{
input: this.reopenButton,
valueAttribute: 'data-reopen-text',
inputAttribute: 'data-alternative-text',
},
);
}
return config;

View file

@ -9,44 +9,60 @@ const viewModes = ['two-up', 'swipe'];
export default class ImageFile {
constructor(file) {
this.file = file;
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
_this.initViewModes();
this.requestImageInfo(
$('.two-up.view .frame.deleted img', this.file),
(function(_this) {
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(
width,
height,
) {
_this.initViewModes();
// Load two-up view after images are loaded
// so that we can display the correct width and height information
const $images = $('.two-up.view img', _this.file);
// Load two-up view after images are loaded
// so that we can display the correct width and height information
const $images = $('.two-up.view img', _this.file);
$images.waitForImages(function() {
_this.initView('two-up');
$images.waitForImages(function() {
_this.initView('two-up');
});
});
});
};
})(this));
};
})(this),
);
}
initViewModes() {
const viewMode = viewModes[0];
$('.view-modes', this.file).removeClass('hide');
$('.view-modes-menu', this.file).on('click', 'li', (function(_this) {
return function(event) {
if (!$(event.currentTarget).hasClass('active')) {
return _this.activateViewMode(event.currentTarget.className);
}
};
})(this));
$('.view-modes-menu', this.file).on(
'click',
'li',
(function(_this) {
return function(event) {
if (!$(event.currentTarget).hasClass('active')) {
return _this.activateViewMode(event.currentTarget.className);
}
};
})(this),
);
return this.activateViewMode(viewMode);
}
activateViewMode(viewMode) {
$('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active');
return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) {
return function() {
$(".view." + viewMode, _this.file).fadeIn(200);
return _this.initView(viewMode);
};
})(this));
$('.view-modes-menu li', this.file)
.removeClass('active')
.filter('.' + viewMode)
.addClass('active');
return $('.view:visible:not(.' + viewMode + ')', this.file).fadeOut(
200,
(function(_this) {
return function() {
$('.view.' + viewMode, _this.file).fadeIn(200);
return _this.initView(viewMode);
};
})(this),
);
}
initView(viewMode) {
@ -63,135 +79,154 @@ export default class ImageFile {
$body.css('user-select', 'none');
});
$body.off('mouseup').off('mousemove').on('mouseup', function() {
dragging = false;
$body.css('user-select', '');
})
.on('mousemove', function(e) {
var left;
if (!dragging) return;
$body
.off('mouseup')
.off('mousemove')
.on('mouseup', function() {
dragging = false;
$body.css('user-select', '');
})
.on('mousemove', function(e) {
var left;
if (!dragging) return;
left = e.pageX - ($offsetEl.offset().left + padding);
left = e.pageX - ($offsetEl.offset().left + padding);
callback(e, left);
});
callback(e, left);
});
}
prepareFrames(view) {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
$('.frame', view).each((function(_this) {
return function(index, frame) {
var height, width;
width = $(frame).width();
height = $(frame).height();
maxWidth = width > maxWidth ? width : maxWidth;
return maxHeight = height > maxHeight ? height : maxHeight;
};
})(this)).css({
width: maxWidth,
height: maxHeight
});
$('.frame', view)
.each(
(function(_this) {
return function(index, frame) {
var height, width;
width = $(frame).width();
height = $(frame).height();
maxWidth = width > maxWidth ? width : maxWidth;
return (maxHeight = height > maxHeight ? height : maxHeight);
};
})(this),
)
.css({
width: maxWidth,
height: maxHeight,
});
return [maxWidth, maxHeight];
}
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) {
return function(index, wrap) {
$('img', wrap).each(function() {
var currentWidth;
currentWidth = $(this).width();
if (currentWidth > availWidth / 2) {
return $(this).width(availWidth / 2);
}
});
return _this.requestImageInfo($('img', wrap), function(width, height) {
$('.image-info .meta-width', wrap).text(width + "px");
$('.image-info .meta-height', wrap).text(height + "px");
return $('.image-info', wrap).removeClass('hide');
});
};
})(this));
return $('.two-up.view .wrap', this.file).each(
(function(_this) {
return function(index, wrap) {
$('img', wrap).each(function() {
var currentWidth;
currentWidth = $(this).width();
if (currentWidth > availWidth / 2) {
return $(this).width(availWidth / 2);
}
});
return _this.requestImageInfo($('img', wrap), function(width, height) {
$('.image-info .meta-width', wrap).text(width + 'px');
$('.image-info .meta-height', wrap).text(height + 'px');
return $('.image-info', wrap).removeClass('hide');
});
};
})(this),
);
},
'swipe': function() {
swipe() {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
return $('.swipe.view', this.file).each(
(function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
(ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
$swipeFrame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
$swipeWrap.css({
width: maxWidth + 1,
height: maxHeight + 2
});
// Set swipeBar left position to match image frame
$swipeBar.css({
left: 1
});
$swipeFrame.css({
width: maxWidth + 16,
height: maxHeight + 28,
});
$swipeWrap.css({
width: maxWidth + 1,
height: maxHeight + 2,
});
// Set swipeBar left position to match image frame
$swipeBar.css({
left: 1,
});
wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
_this.initDraggable($swipeBar, wrapPadding, function(e, left) {
if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
$swipeWrap.width((maxWidth + 1) - left);
$swipeBar.css('left', left);
}
});
};
})(this));
_this.initDraggable($swipeBar, wrapPadding, function(e, left) {
if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
$swipeWrap.width(maxWidth + 1 - left);
$swipeBar.css('left', left);
}
});
};
})(this),
);
},
'onion-skin': function() {
var dragTrackWidth, maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
$dragger = $('.dragger', $track);
return $('.onion-skin.view', this.file).each(
(function(_this) {
return function(index, view) {
var $frame,
$track,
$dragger,
$frameAdded,
framePadding,
ref,
dragging = false;
(ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
$dragger = $('.dragger', $track);
$frame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
$('.swipe-wrap', view).css({
width: maxWidth + 1,
height: maxHeight + 2
});
$dragger.css({
left: dragTrackWidth
});
$frame.css({
width: maxWidth + 16,
height: maxHeight + 28,
});
$('.swipe-wrap', view).css({
width: maxWidth + 1,
height: maxHeight + 2,
});
$dragger.css({
left: dragTrackWidth,
});
$frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
$frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) {
var opacity = left / dragTrackWidth;
_this.initDraggable($dragger, framePadding, function(e, left) {
var opacity = left / dragTrackWidth;
if (opacity >= 0 && opacity <= 1) {
$dragger.css('left', left);
$frameAdded.css('opacity', opacity);
}
});
};
})(this));
}
}
if (opacity >= 0 && opacity <= 1) {
$dragger.css('left', left);
$frameAdded.css('opacity', opacity);
}
});
};
})(this),
);
},
};
requestImageInfo(img, callback) {
const domImg = img.get(0);
@ -199,11 +234,14 @@ export default class ImageFile {
if (domImg.complete) {
return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
} else {
return img.on('load', (function(_this) {
return function() {
return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
};
})(this));
return img.on(
'load',
(function(_this) {
return function() {
return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
};
})(this),
);
}
}
}

View file

@ -19,11 +19,13 @@ export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
// Update MR and Commits tabs
pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
if (event.detail.pipelines &&
// Update MR and Commits tabs
pipelineTableViewEl.addEventListener('update-pipelines-count', event => {
if (
event.detail.pipelines &&
event.detail.pipelines.count &&
event.detail.pipelines.count.all) {
event.detail.pipelines.count.all
) {
const badge = document.querySelector('.js-pipelines-mr-count');
badge.textContent = event.detail.pipelines.count.all;

View file

@ -1,77 +1,73 @@
<script>
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default {
mixins: [
pipelinesMixin,
],
props: {
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
viewType: {
type: String,
required: false,
default: 'child',
},
export default {
mixins: [pipelinesMixin],
props: {
endpoint: {
type: String,
required: true,
},
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
helpPagePath: {
type: String,
required: true,
},
computed: {
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
autoDevopsHelpPath: {
type: String,
required: true,
},
created() {
this.service = new PipelinesService(this.endpoint);
errorStateSvgPath: {
type: String,
required: true,
},
methods: {
successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: {
pipelines: resp.data,
},
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
viewType: {
type: String,
required: false,
default: 'child',
},
};
},
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
},
computed: {
shouldRenderTable() {
return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
},
methods: {
successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: {
pipelines: resp.data,
},
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
},
};
</script>
<template>
<div class="content-list pipelines">

View file

@ -50,7 +50,7 @@ export function createContent(mergeRequests) {
if (mergeRequests.length === 0) {
$content.text(s__('Commits|No related merge requests found'));
} else {
mergeRequests.forEach((mergeRequest) => {
mergeRequests.forEach(mergeRequest => {
const $header = createHeader($content.children().length, mergeRequests.length);
const $item = createItem(mergeRequest);
$content.append($header);
@ -64,8 +64,9 @@ export function createContent(mergeRequests) {
export function fetchCommitMergeRequests() {
const $container = $('.merge-requests');
axios.get($container.data('projectCommitPath'))
.then((response) => {
axios
.get($container.data('projectCommitPath'))
.then(response => {
const $content = createContent(response.data);
$container.html($content);

View file

@ -32,22 +32,31 @@ export default class CommitsList {
if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
[obj.name]: obj.value,
}), {});
const params = form.serializeArray().reduce(
(acc, obj) =>
Object.assign(acc, {
[obj.name]: obj.value,
}),
{},
);
return axios.get(form.attr('action'), {
params,
})
return axios
.get(form.attr('action'), {
params,
})
.then(({ data }) => {
this.lastSearch = search;
this.content.html(data.html);
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
window.history.replaceState({
page: commitsUrl,
}, document.title, commitsUrl);
window.history.replaceState(
{
page: commitsUrl,
},
document.title,
commitsUrl,
);
})
.catch(() => {
this.content.fadeTo('fast', 1.0);
@ -75,8 +84,15 @@ export default class CommitsList {
processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
commitsCount += Number(
$(processedData)
.nextUntil('li.js-commit-header')
.first()
.find('li.commit').length,
);
$commitsHeadersLast
.find('span.commits-count')
.text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
localTimeAgo($processedData.find('.js-timeago'));

View file

@ -5,6 +5,14 @@ import 'bootstrap';
// custom jQuery functions
$.fn.extend({
disable() { return $(this).prop('disabled', true).addClass('disabled'); },
enable() { return $(this).prop('disabled', false).removeClass('disabled'); },
disable() {
return $(this)
.prop('disabled', true)
.addClass('disabled');
},
enable() {
return $(this)
.prop('disabled', false)
.removeClass('disabled');
},
});

View file

@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) {
$submit.disable();
$input.focus();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
const confirmText = rstrip($(this).val());
if (confirmText === confirmTextMatch) {
$submit.enable();
} else {
$submit.disable();
}
});
$('.js-confirm-danger-submit').off('click').on('click', () => $form.submit());
$('.js-confirm-danger-input')
.off('input')
.on('input', function handleInput() {
const confirmText = rstrip($(this).val());
if (confirmText === confirmTextMatch) {
$submit.enable();
} else {
$submit.disable();
}
});
$('.js-confirm-danger-submit')
.off('click')
.on('click', () => $form.submit());
}
export default function initConfirmDangerModal() {
$(document).on('click', '.js-confirm-danger', (e) => {
$(document).on('click', '.js-confirm-danger', e => {
e.preventDefault();
const $btn = $(e.target);
const $form = $btn.closest('form');

View file

@ -20,8 +20,11 @@ export default class ContextualSidebar {
}
bindEvents() {
document.addEventListener('click', (e) => {
if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
document.addEventListener('click', e => {
if (
!e.target.closest('.nav-sidebar') &&
(bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
) {
this.toggleCollapsedSidebar(true);
}
});

View file

@ -36,7 +36,7 @@ export default class CreateItemDropdown {
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
return selected && 'id' in selected ? _.escape(selected.title) : this.defaultToggleLabel;
},
fieldName: this.fieldName,
text(item) {
@ -46,7 +46,7 @@ export default class CreateItemDropdown {
return _.escape(item.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
clicked: options => {
options.e.preventDefault();
this.onSelect();
},
@ -77,9 +77,8 @@ export default class CreateItemDropdown {
getData(term, callback) {
this.getDataOption(term, (data = []) => {
// Ensure the selected item isn't already in the data to avoid duplicates
const alreadyHasSelectedItem = this.selectedItem && data.some(item =>
item.id === this.selectedItem.id,
);
const alreadyHasSelectedItem =
this.selectedItem && data.some(item => item.id === this.selectedItem.id);
let uniqueData = data;
if (!alreadyHasSelectedItem) {
@ -106,9 +105,7 @@ export default class CreateItemDropdown {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer
.find('.js-dropdown-create-new-item code')
.text(newValue);
this.$dropdownContainer.find('.js-dropdown-create-new-item code').text(newValue);
}
this.toggleFooter(!newValue);

View file

@ -37,7 +37,7 @@ export default class CreateLabelDropdown {
addBinding() {
const self = this;
this.$colorSuggestions.on('click', function (e) {
this.$colorSuggestions.on('click', function(e) {
const $this = $(this);
self.addColorValue(e, $this);
});
@ -47,7 +47,7 @@ export default class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this));
this.$cancelButton.on('click', function (e) {
this.$cancelButton.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
@ -79,13 +79,9 @@ export default class CreateLabelDropdown {
}
resetForm() {
this.$newLabelField
.val('')
.trigger('change');
this.$newLabelField.val('').trigger('change');
this.$newColorField
.val('')
.trigger('change');
this.$newColorField.val('').trigger('change');
this.$colorPreview
.css('background-color', '')
@ -97,31 +93,34 @@ export default class CreateLabelDropdown {
e.preventDefault();
e.stopPropagation();
Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(),
color: this.$newColorField.val(),
}, (label) => {
this.$newLabelCreateButton.enable();
Api.newLabel(
this.namespacePath,
this.projectPath,
{
title: this.$newLabelField.val(),
color: this.$newColorField.val(),
},
label => {
this.$newLabelCreateButton.enable();
if (label.message) {
let errors;
if (label.message) {
let errors;
if (typeof label.message === 'string') {
errors = label.message;
if (typeof label.message === 'string') {
errors = label.message;
} else {
errors = Object.keys(label.message)
.map(key => `${humanize(key)} ${label.message[key].join(', ')}`)
.join('<br/>');
}
this.$newLabelError.html(errors).show();
} else {
errors = Object.keys(label.message).map(key =>
`${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
this.$dropdownBack.trigger('click');
$(document).trigger('created.label', label);
}
this.$newLabelError
.html(errors)
.show();
} else {
this.$dropdownBack.trigger('click');
$(document).trigger('created.label', label);
}
});
},
);
}
}

View file

@ -95,8 +95,10 @@ export default {
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
if (
// eslint-disable-next-line no-alert
window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))
) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)

View file

@ -8,17 +8,14 @@ export default class DeployKeysService {
}
getKeys() {
return this.axios.get()
.then(response => response.data);
return this.axios.get().then(response => response.data);
}
enableKey(id) {
return this.axios.put(`${id}/enable`)
.then(response => response.data);
return this.axios.put(`${id}/enable`).then(response => response.data);
}
disableKey(id) {
return this.axios.put(`${id}/disable`)
.then(response => response.data);
return this.axios.put(`${id}/disable`).then(response => response.data);
}
}

View file

@ -21,9 +21,12 @@ export default class Diff {
});
const tab = document.getElementById('diffs');
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== ''))
FilesCommentButton.init($diffFile);
const firstFile = $('.files').first().get(0);
const firstFile = $('.files')
.first()
.get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
@ -73,9 +76,10 @@ export default class Diff {
const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view };
axios.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
axios
.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
}
openAnchoredDiff(cb) {

View file

@ -41,6 +41,11 @@ export default {
required: true,
},
},
data() {
return {
assignedDiscussions: false,
};
},
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
@ -58,9 +63,9 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapState('diffs', ['showTreeList']),
...mapState('diffs', ['showTreeList', 'isLoading']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
@ -147,11 +152,12 @@ export default {
}
},
setDiscussions() {
if (this.isNotesFetched) {
if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
},
() =>
this.assignDiscussionsToDiff().then(() => {
this.assignedDiscussions = true;
}),
{ timeout: 1000 },
);
}

View file

@ -29,7 +29,7 @@ export default {
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
...mapGetters(['isNotesFetched']),
isCollapsed() {
return this.file.collapsed || false;
},
@ -79,7 +79,7 @@ export default {
.then(() => {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
this.assignDiscussionsToDiff();
},
{ timeout: 1000 },
);

View file

@ -1,17 +1,30 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
const treeListStorageKey = 'mr_diff_tree_list';
export default {
directives: {
Tooltip,
},
components: {
Icon,
FileRow,
},
data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
const renderTreeList = treeListStored !== null ?
convertPermissionToBoolean(treeListStored) : true;
return {
search: '',
renderTreeList,
focusSearch: false,
};
},
computed: {
@ -20,15 +33,35 @@ export default {
filteredTreeList() {
const search = this.search.toLowerCase().trim();
if (search === '') return this.tree;
if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
},
rowDisplayTextKey() {
if (this.renderTreeList && this.search.trim() === '') {
return 'name';
}
return 'path';
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
this.toggleFocusSearch(false);
},
toggleRenderTreeList(toggle) {
this.renderTreeList = toggle;
localStorage.setItem(treeListStorageKey, this.renderTreeList);
},
toggleFocusSearch(toggle) {
this.focusSearch = toggle;
},
blurSearch() {
if (this.search.trim() === '') {
this.toggleFocusSearch(false);
}
},
},
FileRowStats,
@ -37,28 +70,67 @@ export default {
<template>
<div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search">
<icon
name="search"
class="position-absolute tree-list-icon"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon
name="close"
name="search"
class="position-absolute tree-list-icon"
/>
</button>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
@focus="toggleFocusSearch(true)"
@blur="blurSearch"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<icon
name="close"
/>
</button>
</div>
<div
v-show="!focusSearch"
class="btn-group prepend-left-8 tree-list-view-toggle"
>
<button
v-tooltip.hover
:aria-label="__('List view')"
:title="__('List view')"
:class="{
active: !renderTreeList
}"
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
type="button"
@click="toggleRenderTreeList(false)"
>
<icon
name="hamburger"
/>
</button>
<button
v-tooltip.hover
:aria-label="__('Tree view')"
:title="__('Tree view')"
:class="{
active: renderTreeList
}"
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
type="button"
@click="toggleRenderTreeList(true)"
>
<icon
name="file-tree"
/>
</button>
</div>
</div>
<div
class="tree-list-scroll"
@ -72,6 +144,8 @@ export default {
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:show-changed-icon="true"
:display-text-key="rowDisplayTextKey"
:should-truncate-start="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>

View file

@ -5,7 +5,6 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types';
import {
@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => {
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
export const assignDiscussionsToDiff = (
{ commit, state, rootState },
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
Object.values(allLineDiscussions).forEach(discussions => {
if (discussions.length > 0) {
const { fileHash } = discussions[0];
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
fileHash,
discussions,
diffPositionByLineCode,
});
}
discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => {
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion,
diffPositionByLineCode,
});
});
};
@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion =>
dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
)
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};

View file

@ -90,53 +90,67 @@ export default {
}));
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
const firstDiscussion = discussions[0];
const isDiffDiscussion = firstDiscussion.diff_discussion;
const hasLineCode = firstDiscussion.line_code;
const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
const { latestDiff } = state;
if (
selectedFile &&
isDiffDiscussion &&
hasLineCode &&
diffPosition &&
const discussionLineCode = discussion.line_code;
const fileHash = discussion.diff_file.file_hash;
const lineCheck = ({ lineCode }) =>
lineCode === discussionLineCode &&
isDiscussionApplicableToLine({
discussion: firstDiscussion,
diffPosition,
latestDiff: state.latestDiff,
})
) {
const targetLine = selectedFile.parallelDiffLines.find(
line =>
(line.left && line.left.lineCode === firstDiscussion.line_code) ||
(line.right && line.right.lineCode === firstDiscussion.line_code),
);
if (targetLine) {
if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
Object.assign(targetLine.left, {
discussions,
});
} else {
Object.assign(targetLine.right, {
discussions,
discussion,
diffPosition: diffPositionByLineCode[lineCode],
latestDiff,
});
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.fileHash === fileHash) {
const file = { ...diffFile };
if (file.highlightedDiffLines) {
file.highlightedDiffLines = file.highlightedDiffLines.map(line => {
if (lineCheck(line)) {
return {
...line,
discussions: line.discussions.concat(discussion),
};
}
return line;
});
}
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === firstDiscussion.line_code,
);
if (file.parallelDiffLines) {
file.parallelDiffLines = file.parallelDiffLines.map(line => {
const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions,
if (left || right) {
return {
left: {
...line.left,
discussions: left ? line.left.discussions.concat(discussion) : [],
},
right: {
...line.right,
discussions: right ? line.right.discussions.concat(discussion) : [],
},
};
}
return line;
});
}
if (!file.parallelDiffLines || !file.highlightedDiffLines) {
file.discussions = file.discussions.concat(discussion);
}
return file;
}
}
return diffFile;
});
},
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {

View file

@ -136,7 +136,7 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
$cancelButton.on('click', e => {
e.preventDefault();
e.stopPropagation();
Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
@ -146,8 +146,10 @@ export default function dropzoneInput(form) {
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
$retryLink.on('click', e => {
const dropzoneInstance = Dropzone.forElement(
e.target.closest('.js-main-target-form').querySelector('.div-dropzone'),
);
const failedFiles = dropzoneInstance.files;
e.preventDefault();
@ -156,7 +158,7 @@ export default function dropzoneInput(form) {
// uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
failedFiles.map((failedFile) => {
failedFiles.map(failedFile => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
@ -168,7 +170,7 @@ export default function dropzoneInput(form) {
});
});
// eslint-disable-next-line consistent-return
handlePaste = (event) => {
handlePaste = event => {
const pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
const image = isImage(pasteEvent);
@ -182,7 +184,7 @@ export default function dropzoneInput(form) {
}
};
isImage = (data) => {
isImage = data => {
let i = 0;
while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i];
@ -203,8 +205,12 @@ export default function dropzoneInput(form) {
const caretStart = textarea.selectionStart;
const caretEnd = textarea.selectionEnd;
const textEnd = $(child).val().length;
const beforeSelection = $(child).val().substring(0, caretStart);
const afterSelection = $(child).val().substring(caretEnd, textEnd);
const beforeSelection = $(child)
.val()
.substring(0, caretStart);
const afterSelection = $(child)
.val()
.substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
@ -212,11 +218,11 @@ export default function dropzoneInput(form) {
return formTextarea.trigger('input');
};
addFileToForm = (path) => {
addFileToForm = path => {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
};
getFilename = (e) => {
getFilename = e => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
@ -231,7 +237,7 @@ export default function dropzoneInput(form) {
const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
const showError = (message) => {
const showError = message => {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
@ -252,14 +258,15 @@ export default function dropzoneInput(form) {
showSpinner();
closeAlertMessage();
axios.post(uploadsPath, formData)
axios
.post(uploadsPath, formData)
.then(({ data }) => {
const md = data.link.markdown;
insertToTextArea(filename, md);
closeSpinner();
})
.catch((e) => {
.catch(e => {
showError(e.response.data.message);
closeSpinner();
});
@ -267,7 +274,8 @@ export default function dropzoneInput(form) {
updateAttachingMessage = (files, messageContainer) => {
let attachingMessage;
const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length;
const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued')
.length;
// Dinamycally change uploading files text depending on files number in
// dropzone files queue.
@ -282,7 +290,10 @@ export default function dropzoneInput(form) {
form.find('.markdown-selector').click(function onMarkdownClick(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
$(this)
.closest('.gfm-form')
.find('.div-dropzone')
.click();
formTextarea.focus();
});

View file

@ -3,8 +3,7 @@ import Pikaday from 'pikaday';
import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
class DueDateSelect {

View file

@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) {
const characters = Array.from(emojiUnicode);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 &&
return (
emojiUnicode.length === 4 &&
characters[0].codePointAt(0) === baseFlagCodePoint &&
characters[1].codePointAt(0) === rainbowCodePoint;
characters[1].codePointAt(0) === rainbowCodePoint
);
}
// Chrome <57 renders keycaps oddly
@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) {
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
const tone1 = 127995; // parseInt('1F3FB', 16)
const tone5 = 127999; // parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
return (
emojiUnicode.length > 2 &&
Array.from(emojiUnicode).some(char => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
})
);
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
const horseRacingCodePoint = 127943; // parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
return (
firstCharacter &&
firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode)
);
}
// Check for `family_*`, `kiss_*`, `couple_*`
@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
Array.from(emojiUnicode).forEach((character) => {
Array.from(emojiUnicode).forEach(character => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
return (unicodeSupportMap.skinToneModifier && isSkinToneResult) || !isSkinToneResult;
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || !isHorseRacingSkinToneResult
);
}
@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
return (unicodeSupportMap.personZwj && isPersonZwjResult) || !isPersonZwjResult;
}
// Takes in a support map and determines whether
@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
const isOlderThanChrome57 =
unicodeSupportMap.meta &&
unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
return (
unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode)
);
}
export {

View file

@ -2,7 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
export default () => {
$('.js-experiment-feature-toggle').on('change', (e) => {
$('.js-experiment-feature-toggle').on('change', e => {
const el = e.target;
Cookies.set(el.name, el.value, {

View file

@ -25,13 +25,15 @@ export default {
if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === '';
this.userCanCreateNote =
$diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === '';
}
this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
$diffFile
.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
.on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
}
},
@ -64,9 +66,11 @@ export default {
},
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
return (
!buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS)
);
},
};

View file

@ -65,12 +65,15 @@ export default class FilterableList {
this.isBusy = true;
return axios.get(this.getFilterEndpoint(), {
params,
}).then((res) => {
this.onFilterSuccess(res, params);
this.onFilterComplete();
}).catch(() => this.onFilterComplete());
return axios
.get(this.getFilterEndpoint(), {
params,
})
.then(res => {
this.onFilterSuccess(res, params);
this.onFilterComplete();
})
.catch(() => this.onFilterComplete());
}
onFilterSuccess(response, queryData) {
@ -81,9 +84,13 @@ export default class FilterableList {
// Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData);
return window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
return window.history.replaceState(
{
page: currentPath,
},
document.title,
currentPath,
);
}
onFilterComplete() {

View file

@ -67,6 +67,11 @@ export const conditions = [
tokenKey: 'milestone',
value: 'none',
},
{
url: 'milestone_title=Any+Milestone',
tokenKey: 'milestone',
value: 'any',
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',

View file

@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => {
});
}
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
window.dispatchEvent(new Event('resize'));
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, {
once: true,
passive: true,
});
flashEl.addEventListener(
'transitionend',
() => {
flashEl.remove();
window.dispatchEvent(new Event('resize'));
if (document.body.classList.contains('flash-shown'))
document.body.classList.remove('flash-shown');
},
{
once: true,
passive: true,
},
);
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
};
@ -30,12 +35,12 @@ const createAction = config => `
</a>
`;
const createFlashEl = (message, type, isInContentWrapper = false) => `
const createFlashEl = (message, type, isFixedLayout = false) => `
<div
class="flash-${type}"
>
<div
class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
>
${_.escape(message)}
</div>
@ -69,12 +74,13 @@ const createFlash = function createFlash(
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
const navigation = parent.querySelector('.content');
if (!flashContainer) return null;
const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true;
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
removeFlashClickListener(flashEl, fadeTransition);
@ -83,7 +89,9 @@ const createFlash = function createFlash(
flashEl.innerHTML += createAction(actionConfig);
if (actionConfig.clickHandler) {
flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
flashEl
.querySelector('.flash-action')
.addEventListener('click', e => actionConfig.clickHandler(e));
}
}
@ -94,11 +102,5 @@ const createFlash = function createFlash(
return flashContainer;
};
export {
createFlash as default,
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
};
export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener };
window.Flash = createFlash;

View file

@ -11,9 +11,13 @@ let sidebar;
export const mousePos = [];
export const setSidebar = (el) => { sidebar = el; };
export const setSidebar = el => {
sidebar = el;
};
export const getOpenMenu = () => currentOpenMenu;
export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
export const setOpenMenu = (menu = null) => {
currentOpenMenu = menu;
};
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
@ -21,9 +25,10 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const isSidebarCollapsed = () =>
sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const canShowActiveSubItems = (el) => {
export const canShowActiveSubItems = el => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
return false;
}
@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => {
return true;
};
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const canShowSubItems = () =>
bp.getBreakpointSize() === 'sm' ||
bp.getBreakpointSize() === 'md' ||
bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu || !mousePos.length) return 0;
@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => {
const currentMousePosY = currentMousePos.y;
const [menuTop, menuBottom] = menuCornerLocs;
if (currentMousePosY < menuTop.y ||
currentMousePosY > menuBottom.y) return 0;
if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) {
if (
slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
) {
return HIDE_INTERVAL_TIMEOUT;
}
@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height :
boundingRect.top;
return bottomOverflow < 0
? boundingRect.top - outerHeight + boundingRect.height
: boundingRect.top;
};
export const hideMenu = (el) => {
export const hideMenu = el => {
if (!el) return;
const parentEl = el.parentNode;
@ -101,7 +111,7 @@ export const moveSubItemsToPosition = (el, subItems) => {
}
};
export const showSubLevelItems = (el) => {
export const showSubLevelItems = el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
}, timeout);
};
export const mouseLeaveTopItem = (el) => {
export const mouseLeaveTopItem = el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!canShowSubItems() || !canShowActiveSubItems(el) ||
(subItems && subItems === currentOpenMenu)) return;
if (
!canShowSubItems() ||
!canShowActiveSubItems(el) ||
(subItems && subItems === currentOpenMenu)
)
return;
el.classList.remove(IS_OVER_CLASS);
};
export const documentMouseMove = (e) => {
export const documentMouseMove = e => {
mousePos.push({
x: e.clientX,
y: e.clientY,
@ -146,7 +160,7 @@ export const documentMouseMove = (e) => {
if (mousePos.length > 6) mousePos.shift();
};
export const subItemsMouseLeave = (relatedTarget) => {
export const subItemsMouseLeave = relatedTarget => {
clearTimeout(timeoutId);
if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
@ -174,7 +188,7 @@ export default () => {
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
items.forEach((el) => {
items.forEach(el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (subItems) {

View file

@ -116,7 +116,8 @@ export default class GlFieldError {
this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup
this.inputElement.off('keyup.fieldValidator')
this.inputElement
.off('keyup.fieldValidator')
.on('keyup.fieldValidator', this.updateValidity.bind(this));
}

View file

@ -16,9 +16,12 @@ export default class GlFieldErrors {
initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
.map(selector => `input${selector}`).join(',');
.map(selector => `input${selector}`)
.join(',');
this.state.inputs = this.form.find(validateSelectors).toArray()
this.state.inputs = this.form
.find(validateSelectors)
.toArray()
.filter(input => !input.classList.contains(customValidationFlag))
.map(input => new GlFieldError({ input, formErrors: this }));
@ -42,7 +45,7 @@ export default class GlFieldErrors {
/* Public method for triggering validity updates manually */
updateFormValidityState() {
this.state.inputs.forEach((field) => {
this.state.inputs.forEach(field => {
if (field.state.submitted) {
field.updateValidity();
}
@ -50,8 +53,9 @@ export default class GlFieldErrors {
}
focusOnFirstInvalid() {
const firstInvalid = this.state.inputs
.filter(input => !input.inputDomElement.validity.valid)[0];
const firstInvalid = this.state.inputs.filter(
input => !input.inputDomElement.validity.valid,
)[0];
firstInvalid.inputElement.focus();
}
}

View file

@ -39,7 +39,10 @@ export default class GLForm {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
gl.utils.disableButtonIfEmptyField(
this.form.find('.js-note-text'),
this.form.find('.js-comment-button, .js-note-new-discussion'),
);
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
@ -55,11 +58,9 @@ export default class GLForm {
}
setupAutosize() {
this.textarea.off('autosize:resized')
.on('autosize:resized', this.setHeightData.bind(this));
this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this));
this.textarea.off('mouseup.autosize')
.on('mouseup.autosize', this.destroyAutosize.bind(this));
this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this));
setTimeout(() => {
autosize(this.textarea);
@ -91,10 +92,14 @@ export default class GLForm {
addEventListeners() {
this.textarea.on('focus', function focusTextArea() {
$(this).closest('.md-area').addClass('is-focused');
$(this)
.closest('.md-area')
.addClass('is-focused');
});
this.textarea.on('blur', function blurTextArea() {
$(this).closest('.md-area').removeClass('is-focused');
$(this)
.closest('.md-area')
.removeClass('is-focused');
});
}
}

View file

@ -7,8 +7,9 @@ export default function groupAvatar() {
});
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form');
// eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
const filename = $(this)
.val()
.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
return form.find('.js-avatar-filename').text(filename);
});
}

View file

@ -23,7 +23,8 @@ export default class GroupLabelSubscription {
event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url');
axios.post(url)
axios
.post(url)
.then(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
@ -39,7 +40,8 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
axios.post(url)
axios
.post(url)
.then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.')));
@ -58,6 +60,8 @@ export default class GroupLabelSubscription {
const newTitle = tooltipTitles[type];
$('.js-unsubscribe-button', $button.closest('.label-actions-list'))
.tooltip('hide').attr('title', newTitle).tooltip('_fixTitle');
.tooltip('hide')
.attr('title', newTitle)
.tooltip('_fixTitle');
}
}

View file

@ -1,44 +1,44 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
},
props: {
item: {
type: Object,
required: true,
},
props: {
item: {
type: Object,
required: true,
},
},
computed: {
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.item.visibility];
},
computed: {
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.item.visibility];
},
visibilityTooltip() {
if (this.item.type === ITEM_TYPE.GROUP) {
return GROUP_VISIBILITY_TYPE[this.item.visibility];
}
return PROJECT_VISIBILITY_TYPE[this.item.visibility];
},
isProject() {
return this.item.type === ITEM_TYPE.PROJECT;
},
isGroup() {
return this.item.type === ITEM_TYPE.GROUP;
},
visibilityTooltip() {
if (this.item.type === ITEM_TYPE.GROUP) {
return GROUP_VISIBILITY_TYPE[this.item.visibility];
}
return PROJECT_VISIBILITY_TYPE[this.item.visibility];
},
};
isProject() {
return this.item.type === ITEM_TYPE.PROJECT;
},
isGroup() {
return this.item.type === ITEM_TYPE.GROUP;
},
},
};
</script>
<template>

View file

@ -1,52 +1,52 @@
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
title: {
type: String,
required: false,
default: '',
},
directives: {
tooltip,
cssClass: {
type: String,
required: false,
default: '',
},
props: {
title: {
type: String,
required: false,
default: '',
},
cssClass: {
type: String,
required: false,
default: '',
},
iconName: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'bottom',
},
/**
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
value: {
type: [Number, String],
required: false,
default: '',
},
iconName: {
type: String,
required: true,
},
computed: {
isValuePresent() {
return this.value !== '';
},
tooltipPlacement: {
type: String,
required: false,
default: 'bottom',
},
};
/**
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
value: {
type: [Number, String],
required: false,
default: '',
},
},
computed: {
isValuePresent() {
return this.value !== '';
},
},
};
</script>
<template>

View file

@ -37,20 +37,22 @@ export default class NewGroupChild {
getDroplabConfig() {
return {
InputSetter: [{
input: this.newGroupChildButton,
valueAttribute: 'data-value',
inputAttribute: 'data-action',
}, {
input: this.newGroupChildButton,
valueAttribute: 'data-text',
}],
InputSetter: [
{
input: this.newGroupChildButton,
valueAttribute: 'data-value',
inputAttribute: 'data-action',
},
{
input: this.newGroupChildButton,
valueAttribute: 'data-text',
},
],
};
}
bindEvents() {
this.newGroupChildButton
.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
}
onClickNewGroupChildButton(e) {

View file

@ -17,13 +17,14 @@ export default class GroupsStore {
}
setSearchedGroups(rawGroups) {
const formatGroups = groups => groups.map((group) => {
const formattedGroup = this.formatGroupItem(group);
if (formattedGroup.children && formattedGroup.children.length) {
formattedGroup.children = formatGroups(formattedGroup.children);
}
return formattedGroup;
});
const formatGroups = groups =>
groups.map(group => {
const formattedGroup = this.formatGroupItem(group);
if (formattedGroup.children && formattedGroup.children.length) {
formattedGroup.children = formatGroups(formattedGroup.children);
}
return formattedGroup;
});
if (rawGroups && rawGroups.length) {
this.state.groups = formatGroups(rawGroups);
@ -62,10 +63,10 @@ export default class GroupsStore {
formatGroupItem(rawGroupItem) {
const groupChildren = rawGroupItem.children || [];
const groupIsOpen = (groupChildren.length > 0) || false;
const childrenCount = this.hideProjects ?
rawGroupItem.subgroup_count :
rawGroupItem.children_count;
const groupIsOpen = groupChildren.length > 0 || false;
const childrenCount = this.hideProjects
? rawGroupItem.subgroup_count
: rawGroupItem.children_count;
return {
id: rawGroupItem.id,

View file

@ -22,7 +22,7 @@ export default class TransferDropdown {
search: { fields: ['text'] },
data: extraOptions.concat(this.data),
text: item => item.text,
clicked: (options) => {
clicked: options => {
const { e } = options;
e.preventDefault();
this.assignSelected(options.selectedObj);

View file

@ -23,7 +23,7 @@ export default function groupsSelect() {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then((res) => {
.then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
@ -36,7 +36,8 @@ export default function groupsSelect() {
more,
},
});
}).catch(params.error);
})
.catch(params.error);
},
data(search, page) {
return {
@ -68,7 +69,9 @@ export default function groupsSelect() {
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
return `<div class='group-result'> <div class='group-name'>${
object.full_name
}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;

View file

@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) {
const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name);
return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`;
return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(
title,
)}</div>`;
}
export function renderAvatar(entity, options = {}) {

View file

@ -60,8 +60,10 @@ export default class ImageDiff {
}
renderBadge(discussionEl, index) {
const imageBadge = imageDiffHelper
.generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
const imageBadge = imageDiffHelper.generateBadgeFromDiscussionDOM(
this.imageFrameEl,
discussionEl,
);
this.imageBadges.push(imageBadge);

View file

@ -8,5 +8,6 @@ export default () => {
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl =>
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge),
);
};

View file

@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff {
this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
viewTypeNames.forEach((viewType) => {
viewTypeNames.forEach(viewType => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
});
}
@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff {
// Re-render indicator in new view
if (indicator.removed) {
const normalizedIndicator = imageDiffHelper
.resizeCoordinatesToImageElement(this.imageEl, {
x: indicator.x,
y: indicator.y,
width: indicator.image.width,
height: indicator.image.height,
});
const normalizedIndicator = imageDiffHelper.resizeCoordinatesToImageElement(this.imageEl, {
x: indicator.x,
y: indicator.y,
width: indicator.image.width,
height: indicator.image.height,
});
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
}
}

View file

@ -60,66 +60,71 @@ class ImporterStatus {
attributes = Object.assign(repoData, attributes);
}
return axios.post(this.importUrl, attributes)
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
job.attr('id', `project_${data.id}`);
return axios
.post(this.importUrl, attributes)
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
job.attr('id', `project_${data.id}`);
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(sprintf(
_.escape(__('%{loadingIcon} Started')), {
loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`,
},
false,
));
})
.catch((error) => {
let details = error;
job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(
sprintf(
_.escape(__('%{loadingIcon} Started')),
{
loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(
connectingVerb,
)}"></i>`,
},
false,
),
);
})
.catch(error => {
let details = error;
const $statusField = $(`#repo_${this.id} .job-status`);
$statusField.text(__('Failed'));
const $statusField = $(`#repo_${this.id} .job-status`);
$statusField.text(__('Failed'));
if (error.response && error.response.data && error.response.data.errors) {
details = error.response.data.errors;
}
if (error.response && error.response.data && error.response.data.errors) {
details = error.response.data.errors;
}
flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
}
autoUpdate() {
return axios.get(this.jobsUrl)
.then(({ data = [] }) => {
data.forEach((job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
return axios.get(this.jobsUrl).then(({ data = [] }) => {
data.forEach(job => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
switch (job.import_status) {
case 'finished':
jobItem.removeClass('table-active').addClass('table-success');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
statusField.html(`${spinner} ${__('Scheduled')}`);
break;
case 'started':
statusField.html(`${spinner} ${__('Started')}`);
break;
case 'failed':
statusField.html(__('Failed'));
break;
default:
statusField.html(job.import_status);
break;
}
});
switch (job.import_status) {
case 'finished':
jobItem.removeClass('table-active').addClass('table-success');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
statusField.html(`${spinner} ${__('Scheduled')}`);
break;
case 'started':
statusField.html(`${spinner} ${__('Started')}`);
break;
case 'failed':
statusField.html(__('Failed'));
break;
default:
statusField.html(job.import_status);
break;
}
});
});
}
setAutoUpdate() {
@ -141,7 +146,4 @@ function initImporterStatus() {
}
}
export {
initImporterStatus as default,
ImporterStatus,
};
export { initImporterStatus as default, ImporterStatus };

View file

@ -1,7 +1,7 @@
import $ from 'jquery';
import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
export default stickyTop => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({

View file

@ -2,13 +2,7 @@ import Notes from './notes';
export default () => {
const dataEl = document.querySelector('.js-notes-data');
const {
notesUrl,
notesIds,
now,
diffView,
enableGFM,
} = JSON.parse(dataEl.innerHTML);
const { notesUrl, notesIds, now, diffView, enableGFM } = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance

View file

@ -97,7 +97,8 @@ export default class IntegrationSettingsForm {
testSettings(formData) {
this.toggleSubmitBtnState(true);
return axios.put(this.testEndPoint, formData)
return axios
.put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
let flashActions;
@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) {
flashActions = {
title: 'Save anyway',
clickHandler: (e) => {
clickHandler: e => {
e.preventDefault();
this.$form.submit();
},

View file

@ -27,7 +27,10 @@ class AutoWidthDropdownSelect {
// We have to look at the parent because
// `offsetParent` on a `display: none;` is `null`
const offsetParentWidth = $(this).parent().offsetParent().width();
const offsetParentWidth = $(this)
.parent()
.offsetParent()
.width();
// Reset any width to let it naturally flow
$dropdown.css('width', 'auto');
if ($dropdown.outerWidth(false) > offsetParentWidth) {

View file

@ -32,7 +32,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
return new Flash("Issue update failed");
return new Flash('Issue update failed');
},
getSelectedIssues() {
@ -63,7 +63,7 @@ export default {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
this.getLabelsFromSelection().forEach(id => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
@ -89,8 +89,8 @@ export default {
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
remove_label_ids: [],
},
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
@ -134,7 +134,7 @@ export default {
// Collect unique label IDs for all checked issues
this.getElement('.selected-issuable:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
issuableLabels.forEach(labelId => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);

View file

@ -1,6 +1,3 @@
/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
/* global GitLab */
import $ from 'jquery';
import Pikaday from 'pikaday';
import Autosave from './autosave';
@ -8,7 +5,7 @@ import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
export default class IssuableForm {
constructor(form) {
@ -19,9 +16,11 @@ export default class IssuableForm {
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.gfmAutoComplete = new GfmAutoComplete(
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
).setup();
this.usersSelect = new UsersSelect();
this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
@ -57,8 +56,16 @@ export default class IssuableForm {
}
initAutosave() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
this.autosave = new Autosave(this.titleField, [
document.location.pathname,
document.location.search,
'title',
]);
return new Autosave(this.descriptionField, [
document.location.pathname,
document.location.search,
'description',
]);
}
handleSubmit() {
@ -74,7 +81,7 @@ export default class IssuableForm {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
return undefined;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
@ -89,10 +96,9 @@ export default class IssuableForm {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
toggleWip(event) {
@ -110,7 +116,7 @@ export default class IssuableForm {
}
addWip() {
this.titleField.val(`WIP: ${(this.titleField.val())}`);
this.titleField.val(`WIP: ${this.titleField.val()}`);
}
initTargetBranchDropdown() {

View file

@ -77,11 +77,11 @@
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
'isJobStuck',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
'hasRunnersForProject',
]),
shouldRenderContent() {
@ -195,9 +195,9 @@
<!-- Body Section -->
<stuck-block
v-if="isJobStuck"
v-if="job.stuck"
class="js-job-stuck"
:has-no-runners-for-project="job.runners.available"
:has-no-runners-for-project="hasRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>

View file

@ -1,5 +1,4 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@ -9,11 +8,9 @@ export default {
CiIcon,
Icon,
},
directives: {
tooltip,
},
props: {
job: {
type: Object,
@ -24,10 +21,9 @@ export default {
required: true,
},
},
computed: {
tooltipText() {
return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`;
return `${this.job.name} - ${this.job.status.tooltip}`;
},
},
};
@ -36,7 +32,10 @@ export default {
<template>
<div
class="build-job"
:class="{ retried: job.retried, active: isActive }"
:class="{
retried: job.retried,
active: isActive
}"
>
<a
v-tooltip

View file

@ -23,14 +23,7 @@ export default {
<template>
<div class="bs-callout bs-callout-warning">
<p
v-if="hasNoRunnersForProject"
class="js-stuck-no-runners append-bottom-0"
>
{{ s__(`Job|This job is stuck, because the project
doesn't have any runners online assigned to it.`) }}
</p>
<p
v-else-if="tags.length"
v-if="tags.length"
class="js-stuck-with-tags append-bottom-0"
>
{{ s__(`This job is stuck, because you don't have
@ -43,6 +36,13 @@ export default {
{{ tag }}
</span>
</p>
<p
v-else-if="hasNoRunnersForProject"
class="js-stuck-no-runners append-bottom-0"
>
{{ s__(`Job|This job is stuck, because the project
doesn't have any runners online assigned to it.`) }}
</p>
<p
v-else
class="js-stuck-no-active-runner append-bottom-0"

View file

@ -41,17 +41,10 @@ export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
/**
* When the job is pending and there are no available runners
* we need to render the stuck block;
*
* @returns {Boolean}
*/
export const isJobStuck = state =>
(!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
(!_.isEmpty(state.job.runners) && state.job.runners.available === false);
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -1,28 +0,0 @@
export const pad = (val, len = 2) => `0${val}`.slice(-len);
/**
* Formats dates in Pickaday
* @param {String} dateString Date in yyyy-mm-dd format
* @return {Date} UTC format
*/
export const parsePikadayDate = dateString => {
const parts = dateString.split('-');
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1] - 1, 10);
const day = parseInt(parts[2], 10);
return new Date(year, month, day);
};
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
* @return {String} Date formated in yyyy-mm-dd
*/
export const pikadayToString = date => {
const day = pad(date.getDate());
const month = pad(date.getMonth() + 1);
const year = date.getFullYear();
return `${year}-${month}-${day}`;
};

View file

@ -1,4 +1,5 @@
import $ from 'jquery';
import _ from 'underscore';
import timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
@ -46,6 +47,8 @@ const getMonthNames = abbreviated => {
];
};
export const pad = (val, len = 2) => `0${val}`.slice(-len);
/**
* Given a date object returns the day of the week in English
* @param {date} date
@ -74,10 +77,10 @@ let timeagoInstance;
/**
* Sets a timeago Instance
*/
export function getTimeago() {
export const getTimeago = () => {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
const localeRemaining = (number, index) =>
[
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
@ -93,9 +96,9 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
};
const locale = function getLocale(number, index) {
return [
const locale = (number, index) =>
[
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
@ -111,7 +114,6 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
};
timeago.register(timeagoLanguageCode, locale);
timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
@ -119,7 +121,7 @@ export function getTimeago() {
}
return timeagoInstance;
}
};
/**
* For the given element, renders a timeago instance.
@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => {
* @param {Number} seconds
* @return {String}
*/
export function timeIntervalInWords(intervalInSeconds) {
export const timeIntervalInWords = intervalInSeconds => {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - minutes * 60;
@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) {
text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
};
export function dateInWords(date, abbreviated = false, hideYear = false) {
export const dateInWords = (date, abbreviated = false, hideYear = false) => {
if (!date) return date;
const month = date.getMonth();
@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
}
return `${monthName} ${date.getDate()}, ${year}`;
}
};
/**
* Returns month name based on provided date.
@ -391,3 +393,83 @@ export const formatTime = milliseconds => {
formattedTime += remainingSeconds;
return formattedTime;
};
/**
* Formats dates in Pickaday
* @param {String} dateString Date in yyyy-mm-dd format
* @return {Date} UTC format
*/
export const parsePikadayDate = dateString => {
const parts = dateString.split('-');
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1] - 1, 10);
const day = parseInt(parts[2], 10);
return new Date(year, month, day);
};
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
* @return {String} Date formated in yyyy-mm-dd
*/
export const pikadayToString = date => {
const day = pad(date.getDate());
const month = pad(date.getMonth() + 1);
const year = date.getFullYear();
return `${year}-${month}-${day}`;
};
/**
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length.
*/
export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => {
const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay;
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
const timePeriodConstraints = {
weeks: MINUTES_PER_WEEK,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= periodCount * minutesPerPeriod;
return periodCount;
});
};
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
export const stringifyTime = timeObject => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
).trim();
return reducedTime.length ? reducedTime : '0m';
};
/**
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
export const abbreviateTime = timeStr =>
timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];

View file

@ -1,63 +0,0 @@
import _ from 'underscore';
/*
* TODO: Make these methods more configurable (e.g. stringifyTime condensed or
* non-condensed, abbreviateTimelengths)
* */
/*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length.
*/
export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay;
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
const timePeriodConstraints = {
weeks: MINUTES_PER_WEEK,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= periodCount * minutesPerPeriod;
return periodCount;
});
}
/*
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
export function stringifyTime(timeObject) {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
).trim();
return reducedTime.length ? reducedTime : '0m';
}
/*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
export function abbreviateTime(timeStr) {
return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
}

View file

@ -1,6 +1,6 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling

View file

@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
import initDiscussionFilters from '../notes/discussion_filters';
import store from './stores';
import MergeRequest from '../merge_request';
@ -88,5 +89,6 @@ export default function initMrNotes() {
},
});
initDiscussionFilters(store);
initDiffsApp(store);
}

View file

@ -56,10 +56,11 @@ export default {
</script>
<template>
<div class="line-resolve-all-container prepend-top-10">
<div
v-if="discussionCount > 0"
class="line-resolve-all-container prepend-top-8">
<div>
<div
v-if="discussionCount > 0"
:class="{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span

View file

@ -0,0 +1,82 @@
<script>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import { mapGetters, mapActions } from 'vuex';
export default {
components: {
Icon,
},
props: {
filters: {
type: Array,
required: true,
},
defaultValue: {
type: Number,
default: null,
required: false,
},
},
data() {
return { currentValue: this.defaultValue };
},
computed: {
...mapGetters([
'getNotesDataByProp',
]),
currentFilter() {
if (!this.currentValue) return this.filters[0];
return this.filters.find(filter => filter.value === this.currentValue);
},
},
methods: {
...mapActions(['filterDiscussion']),
selectFilter(value) {
const filter = parseInt(value, 10);
// close dropdown
$(this.$refs.dropdownToggle).dropdown('toggle');
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
},
},
};
</script>
<template>
<div class="discussion-filter-container d-inline-block align-bottom">
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
class="btn btn-default"
data-toggle="dropdown"
aria-expanded="false"
>
{{ currentFilter.title }}
<icon name="chevron-down" />
</button>
<div
class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
aria-labelledby="discussion-filter-dropdown">
<div class="dropdown-content">
<ul>
<li
v-for="filter in filters"
:key="filter.value"
>
<button
:class="{ 'is-active': filter.value === currentValue }"
type="button"
@click="selectFilter(filter.value)"
>
{{ filter.title }}
</button>
</li>
</ul>
</div>
</div>
</div>
</template>

View file

@ -50,11 +50,11 @@ export default {
},
data() {
return {
isLoading: true,
currentFilter: null,
};
},
computed: {
...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
noteableType() {
return this.noteableData.noteableType;
},
@ -102,6 +102,7 @@ export default {
},
methods: {
...mapActions({
setLoadingState: 'setLoadingState',
fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
@ -133,19 +134,19 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
})
.then(() => {
this.isLoading = false;
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
this.setLoadingState(false);
this.setNotesFetchedState(true);
Flash('Something went wrong while fetching comments. Please try again.');
});

View file

@ -0,0 +1,33 @@
import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue';
export default (store) => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
const filters = Object.keys(filterValues).map(entry =>
({ title: entry, value: filterValues[entry] }));
return new Vue({
el: discussionFilterEl,
name: 'DiscussionFilter',
components: {
DiscussionFilter,
},
store,
render(createElement) {
return createElement('discussion-filter', {
props: {
filters,
defaultValue,
},
});
},
});
}
return null;
};

View file

@ -1,10 +1,13 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import createStore from './stores';
document.addEventListener('DOMContentLoaded', () => {
const store = createStore();
initDiscussionFilters(store);
return new Vue({
el: '#js-vue-notes',
components: {

View file

@ -5,8 +5,9 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
fetchDiscussions(endpoint) {
return Vue.http.get(endpoint);
fetchDiscussions(endpoint, filter) {
const config = filter !== undefined ? { params: { notes_filter: filter } } : null;
return Vue.http.get(endpoint, config);
},
deleteNote(endpoint) {
return Vue.http.delete(endpoint);

View file

@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale';
let eTagPoll;
@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchDiscussions = ({ commit }, path) =>
export const fetchDiscussions = ({ commit }, { path, filter }) =>
service
.fetchDiscussions(path)
.fetchDiscussions(path, filter)
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
dispatch('fetchDiscussions', state.notesData.discussionsPath);
dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else {
commit(types.ADD_NEW_NOTE, note);
}
@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => {
mrWidgetEventHub.$emit('mr.discussion.updated');
};
export const setLoadingState = ({ commit }, data) => {
commit(types.SET_NOTES_LOADING_STATE, data);
};
export const filterDiscussion = ({ dispatch }, { path, filter }) => {
dispatch('setLoadingState', true);
dispatch('fetchDiscussions', { path, filter })
.then(() => {
dispatch('setLoadingState', false);
dispatch('setNotesFetchedState', true);
})
.catch(() => {
dispatch('setLoadingState', false);
dispatch('setNotesFetchedState', true);
Flash(__('Something went wrong while fetching comments. Please try again.'));
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -1,6 +1,5 @@
import _ from 'underscore';
import * as constants from '../constants';
import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
@ -11,6 +10,8 @@ export const getNotesData = state => state.notesData;
export const isNotesFetched = state => state.isNotesFetched;
export const isLoading = state => state.isLoading;
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
@ -29,9 +30,6 @@ export const notesById = state =>
return acc;
}, {});
export const discussionsStructuredByLineCode = state =>
reduceDiscussionsToLineCodes(state.discussions);
export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;

View file

@ -11,6 +11,7 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
isNotesFetched: false,
isLoading: true,
// holds endpoints and permissions provided through haml
notesData: {

View file

@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';

View file

@ -216,6 +216,10 @@ export default {
Object.assign(state, { isNotesFetched: value });
},
[types.SET_NOTES_LOADING_STATE](state, value) {
state.isLoading = value;
},
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);

View file

@ -25,18 +25,6 @@ export const getQuickActionText = note => {
return text;
};
export const reduceDiscussionsToLineCodes = selectedDiscussions =>
selectedDiscussions.reduce((acc, note) => {
if (note.diff_discussion && note.line_code) {
// For context about line notes: there might be multiple notes with the same line code
const items = acc[note.line_code] || [];
items.push(note);
Object.assign(acc, { [note.line_code]: items });
}
return acc;
}, {});
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();

View file

@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
export default {
init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
init(
limit = 0,
preload = false,
disable = false,
prepareData = $.noop,
callback = $.noop,
container = '',
) {
this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
this.limit = limit;
this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
this.prepareData = prepareData;
this.callback = callback;
this.loading = $('.loading').first();
this.loading = $(`${container} .loading`).first();
if (preload) {
this.offset = 0;
this.getOld();

View file

@ -170,7 +170,7 @@ export default class UserTabs {
this.loadActivityCalendar('activity');
// eslint-disable-next-line no-new
new Activities();
new Activities('#activity');
this.loaded.activity = true;
}

View file

@ -1,111 +1,111 @@
<script>
import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/pretty_time';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
directives: {
tooltip,
},
props: {
showComparisonState: {
type: Boolean,
required: true,
},
directives: {
tooltip,
showSpentOnlyState: {
type: Boolean,
required: true,
},
props: {
showComparisonState: {
type: Boolean,
required: true,
},
showSpentOnlyState: {
type: Boolean,
required: true,
},
showEstimateOnlyState: {
type: Boolean,
required: true,
},
showNoTimeTrackingState: {
type: Boolean,
required: true,
},
timeSpentHumanReadable: {
type: String,
required: false,
default: '',
},
timeEstimateHumanReadable: {
type: String,
required: false,
default: '',
},
showEstimateOnlyState: {
type: Boolean,
required: true,
},
computed: {
timeSpent() {
return this.abbreviateTime(this.timeSpentHumanReadable);
},
timeEstimate() {
return this.abbreviateTime(this.timeEstimateHumanReadable);
},
divClass() {
if (this.showComparisonState) {
return 'compare';
} else if (this.showEstimateOnlyState) {
return 'estimate-only';
} else if (this.showSpentOnlyState) {
return 'spend-only';
} else if (this.showNoTimeTrackingState) {
return 'no-tracking';
}
showNoTimeTrackingState: {
type: Boolean,
required: true,
},
timeSpentHumanReadable: {
type: String,
required: false,
default: '',
},
timeEstimateHumanReadable: {
type: String,
required: false,
default: '',
},
},
computed: {
timeSpent() {
return this.abbreviateTime(this.timeSpentHumanReadable);
},
timeEstimate() {
return this.abbreviateTime(this.timeEstimateHumanReadable);
},
divClass() {
if (this.showComparisonState) {
return 'compare';
} else if (this.showEstimateOnlyState) {
return 'estimate-only';
} else if (this.showSpentOnlyState) {
return 'spend-only';
} else if (this.showNoTimeTrackingState) {
return 'no-tracking';
}
return '';
},
spanClass() {
if (this.showComparisonState) {
return '';
},
spanClass() {
if (this.showComparisonState) {
return '';
} else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
return 'bold';
} else if (this.showNoTimeTrackingState) {
return 'no-value';
}
} else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
return 'bold';
} else if (this.showNoTimeTrackingState) {
return 'no-value';
}
return '';
},
text() {
if (this.showComparisonState) {
return `${this.timeSpent} / ${this.timeEstimate}`;
} else if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimate}`;
} else if (this.showSpentOnlyState) {
return `${this.timeSpent} / --`;
} else if (this.showNoTimeTrackingState) {
return 'None';
}
return '';
},
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', ({ title, text: this.text }));
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
return '';
},
methods: {
abbreviateTime(timeStr) {
return abbreviateTime(timeStr);
},
text() {
if (this.showComparisonState) {
return `${this.timeSpent} / ${this.timeEstimate}`;
} else if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimate}`;
} else if (this.showSpentOnlyState) {
return `${this.timeSpent} / --`;
} else if (this.showNoTimeTrackingState) {
return 'None';
}
return '';
},
};
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', { title, text: this.text });
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
},
methods: {
abbreviateTime(timeStr) {
return abbreviateTime(timeStr);
},
},
};
</script>
<template>

View file

@ -1,5 +1,5 @@
<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {

View file

@ -34,10 +34,21 @@ export default {
required: false,
default: false,
},
displayTextKey: {
type: String,
required: false,
default: 'name',
},
shouldTruncateStart: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
mouseOver: false,
truncateStart: 0,
};
},
computed: {
@ -60,6 +71,15 @@ export default {
'is-open': this.file.opened,
};
},
outputText() {
const text = this.file[this.displayTextKey];
if (this.truncateStart === 0) {
return text;
}
return `...${text.substring(this.truncateStart, text.length)}`;
},
},
watch: {
'file.active': function fileActiveWatch(active) {
@ -72,6 +92,15 @@ export default {
if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true);
}
if (this.shouldTruncateStart) {
const { scrollWidth, offsetWidth } = this.$refs.textOutput;
const textOverflow = scrollWidth - offsetWidth;
if (textOverflow > 0) {
this.truncateStart = Math.ceil(textOverflow / 5) + 3;
}
}
},
methods: {
toggleTreeOpen(path) {
@ -139,6 +168,7 @@ export default {
class="file-row-name-container"
>
<span
ref="textOutput"
:style="levelIndentation"
class="file-row-name str-truncated"
>
@ -156,7 +186,7 @@ export default {
:size="16"
class="append-right-5"
/>
{{ file.name }}
{{ outputText }}
</span>
<component
:is="extraComponent"
@ -175,6 +205,8 @@ export default {
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
:display-text-key="displayTextKey"
:should-truncate-start="shouldTruncateStart"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>

View file

@ -1,6 +1,6 @@
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',

View file

@ -334,6 +334,14 @@ img.emoji {
}
}
.outline-0 {
outline: 0;
&:focus {
outline: 0;
}
}
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
@ -369,3 +377,5 @@ img.emoji {
.flex-align-self-center { align-self: center; }
.flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }

View file

@ -37,6 +37,7 @@
button {
padding-top: 0;
background-color: transparent;
}
&.active a,

View file

@ -1027,8 +1027,12 @@
overflow-x: auto;
}
.tree-list-search .form-control {
padding-left: 30px;
.tree-list-search {
flex: 0 0 34px;
.form-control {
padding-left: 30px;
}
}
.tree-list-icon {
@ -1063,3 +1067,9 @@
}
}
}
.tree-list-view-toggle {
svg {
top: 0;
}
}

View file

@ -185,7 +185,17 @@ ul.related-merge-requests > li {
}
.new-branch-col {
padding-top: 10px;
font-size: 0;
.discussion-filter-container {
&:not(:only-child) {
margin-right: $gl-padding-8;
}
@include media-breakpoint-down(md) {
margin-top: $gl-padding-8;
}
}
}
.create-mr-dropdown-wrap {
@ -205,6 +215,10 @@ ul.related-merge-requests > li {
.btn-group:not(.hidden) {
display: flex;
@include media-breakpoint-down(md) {
margin-top: $gl-padding-8;
}
}
.js-create-merge-request {
@ -251,7 +265,6 @@ ul.related-merge-requests > li {
.new-branch-col {
padding-top: 0;
text-align: right;
align-self: center;
}
@ -262,3 +275,9 @@ ul.related-merge-requests > li {
}
}
}
@include media-breakpoint-up(lg) {
.new-branch-col {
text-align: right;
}
}

View file

@ -818,9 +818,17 @@
display: flex;
justify-content: space-between;
@include media-breakpoint-down(xs) {
@include media-breakpoint-down(md) {
flex-direction: column-reverse;
}
.discussion-filter-container {
margin-top: $gl-padding-8;
&:not(:only-child) {
padding-right: $gl-padding-8;
}
}
}
.limit-container-width:not(.container-limited) {

View file

@ -618,7 +618,6 @@ ul.notes {
.line-resolve-all-container {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0;
padding-left: $gl-padding;
}
> div {
@ -756,3 +755,23 @@ ul.notes {
margin-top: 4px;
}
}
.discussion-filter-container {
.btn > svg {
width: $gl-col-padding;
height: $gl-col-padding;
}
.dropdown-menu {
margin-bottom: $gl-padding-4;
@include media-breakpoint-down(md) {
margin-left: $btn-side-margin + $contextual-sidebar-collapsed-width;
}
@include media-breakpoint-down(xs) {
margin-left: $btn-side-margin;
}
}
}

View file

@ -2,6 +2,7 @@
module IssuableActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
included do
before_action :labels, only: [:show, :new, :edit]
@ -95,10 +96,14 @@ module IssuableActions
def discussions
notes = issuable.discussion_notes
.inc_relations_for_view
.with_notes_filter(notes_filter)
.includes(:noteable)
.fresh
notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
end
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@ -110,6 +115,32 @@ module IssuableActions
private
def notes_filter
strong_memoize(:notes_filter) do
notes_filter_param = params[:notes_filter]&.to_i
# GitLab Geo does not expect database UPDATE or INSERT statements to happen
# on GET requests.
# This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
if Gitlab::Database.read_only?
notes_filter_param || current_user&.notes_filter_for(issuable)
else
notes_filter = current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
# We need to invalidate the cache for polling notes otherwise it will
# ignore the filter.
# The ideal would be to invalidate the cache for each user.
issuable.expire_note_etag_cache if notes_filter_updated?
notes_filter
end
end
end
def notes_filter_updated?
current_user&.user_preference&.previous_changes&.any?
end
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end

View file

@ -17,10 +17,17 @@ module NotesActions
notes_json = { notes: [], last_fetched_at: current_fetched_at }
notes = notes_finder.execute
.inc_relations_for_view
notes = notes_finder
.execute
.inc_relations_for_view
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
notes =
ResourceEvents::MergeIntoNotesService
.new(noteable, current_user, last_fetched_at: current_fetched_at)
.execute(notes)
end
notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@ -224,6 +231,10 @@ module NotesActions
request.headers['X-Last-Fetched-At']
end
def notes_filter
current_user&.notes_filter_for(params[:target_type])
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end

View file

@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
alias_method :awardable, :note
def finder_params
params.merge(last_fetched_at: last_fetched_at)
params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter)
end
def authorize_admin_note!

Some files were not shown because too many files have changed in this diff Show more