Commit fee6989f authored by DJ Mountney's avatar DJ Mountney

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

parents 34d84fd2 4de90041
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$)/
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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) {
......
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');
});
}
};
......@@ -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');
});
}
}
......@@ -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];
......
......@@ -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);
}
......
......@@ -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({
......
......@@ -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] || {}),
......
......@@ -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;
......
This diff is collapsed.
......@@ -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;
......
<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,
},
helpPagePath: {
type: String,
required: true,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
viewType: {
type: String,
required: false,
default: 'child',
},
},
data() {
const store = new PipelineStore();
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
},
return {
store,
state: store.state,
};
},
computed: {
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
computed: {
shouldRenderTable() {
return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError;
},
created() {
this.service = new PipelinesService(this.endpoint);
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
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);
},
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,
},
});
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);
}
},
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
};
},
};
</script>
<template>
<div class="content-list pipelines">
......
......@@ -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);
......
......@@ -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'));
......
......@@ -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');
},
});
......@@ -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');
......
......@@ -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);
}
});
......
......@@ -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);
......
......@@ -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();
if (label.message) {
let errors;
if (typeof label.message === 'string') {
errors = label.message;
Api.newLabel(
this.namespacePath,
this.projectPath,
{
title: this.$newLabelField.val(),
color: this.$newColorField.val(),
},
label => {
this.$newLabelCreateButton.enable();
if (label.message) {
let errors;
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');
this.$newLabelError
.html(errors)
.show();
} else {
this.$dropdownBack.trigger('click');
$(document).trigger('created.label', label);
}
});
$(document).trigger('created.label', label);
}
},
);
}
}
......@@ -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)
......
......@@ -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);
}
}
......@@ -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) {
......
......@@ -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 },
);
}
......
......@@ -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 },
);
......
<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"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
@focus="toggleFocusSearch(true)"
@blur="blurSearch"
/>
</button>
<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"
/>
......
......@@ -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')));
};
......
......@@ -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];
if (
selectedFile &&
isDiffDiscussion &&
hasLineCode &&
diffPosition &&
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
const { latestDiff } = state;
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 (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions,
if (file.parallelDiffLines) {
file.parallelDiffLines = file.parallelDiffLines.map(line => {
const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right);
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 }) {
......
......@@ -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();
});
......
......@@ -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 {
......
......@@ -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 {
......
......@@ -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, {
......
......@@ -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)
);
},
};
......@@ -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() {
......
......@@ -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',
......
......@@ -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;
......@@ -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) {
......
......@@ -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));
}
......
......@@ -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();
}
}
......@@ -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');
});
}
}
......@@ -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);
});
}
......@@ -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');
}
}
<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>
......
<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>
......
......@@ -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) {
......
......@@ -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,
......
......@@ -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);
......
......@@ -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;
......
......@@ -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 = {}) {
......
......@@ -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);
......
......@@ -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),
);
};
......@@ -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);
}
}
......
......@@ -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}`);
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;
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;
}
flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
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.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'));
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 }));
});
}
autoUpdate() {
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>';
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;
}
});
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>';
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 };
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({
......
......@@ -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
......
......@@ -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();
},
......
......@@ -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) {
......
......@@ -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);
......
/* 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() {
......
......@@ -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"
/>
......
<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
......
......@@ -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"
......
......@@ -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 () => {};
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}`;
};
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];
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];
}
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
......
......@@ -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);
}
......@@ -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
......
<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>
......@@ -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.');
});
......
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;
};
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: {
......
......@@ -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);
......
......@@ -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 () => {};
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;
......
......@@ -11,6 +11,7 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
isNotesFetched: false,
isLoading: true,
// holds endpoints and permissions provided through haml
notesData: {
......
......@@ -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';
......
......@@ -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);
......
......@@ -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();
......@@ -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();
......
......@@ -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;
}
......
<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 '';
},
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 '';
},
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 sprintf('%{title}: %{text}', { title, text: this.text });
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
methods: {
abbreviateTime(timeStr) {
return abbreviateTime(timeStr);
},
},
methods: {
abbreviateTime(timeStr) {
return abbreviateTime(timeStr);
},
};
},
};
</script>
<template>
......
<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 {
......
......@@ -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"
/>
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',
......
......@@ -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; }
......@@ -37,6 +37,7 @@
button {
padding-top: 0;
background-color: transparent;
}
&.active a,
......
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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) {
......
......@@ -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;
}
}
}
......@@ -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
......
......@@ -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
......
......@@ -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!
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment