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 .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
...@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git ...@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git
- gitlab-org - gitlab-org
.default-cache: &default-cache .default-cache: &default-cache
key: "ruby-2.4.4-debian-stretch-with-yarn" key: "ruby-2.4.5-debian-stretch-with-yarn"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
...@@ -75,11 +75,6 @@ stages: ...@@ -75,11 +75,6 @@ stages:
- mysql:5.7 - mysql:5.7
- redis:alpine - redis:alpine
.rails5-variables: &rails5-variables
script:
- export RAILS5=${RAILS5}
- export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
.rails5: &rails5 .rails5: &rails5
allow_failure: true allow_failure: true
only: only:
...@@ -139,7 +134,7 @@ stages: ...@@ -139,7 +134,7 @@ stages:
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}" - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
- apk add --update openssl - apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- chmod 755 $SCRIPT_NAME - chmod 755 $(basename $SCRIPT_NAME)
.rake-exec: &rake-exec .rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
...@@ -150,7 +145,6 @@ stages: ...@@ -150,7 +145,6 @@ stages:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *rails5-variables
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -594,7 +588,7 @@ static-analysis: ...@@ -594,7 +588,7 @@ static-analysis:
script: script:
- scripts/static-analysis - scripts/static-analysis
cache: cache:
key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop" key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
...@@ -723,7 +717,7 @@ gitlab:assets:compile: ...@@ -723,7 +717,7 @@ gitlab:assets:compile:
- public/assets/ - public/assets/
karma: karma:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-pull-cache-job
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- compile-assets - compile-assets
...@@ -929,3 +923,93 @@ no_ee_check: ...@@ -929,3 +923,93 @@ no_ee_check:
- scripts/no-ee-check - scripts/no-ee-check
only: only:
- //@gitlab-org/gitlab-ce - //@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` ...@@ -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 the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required - [ ] 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**. - [ ] 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 #### Backports
...@@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue` ...@@ -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 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` - [ ] 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 - [ ] 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 [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
......
...@@ -417,8 +417,7 @@ end ...@@ -417,8 +417,7 @@ end
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly' gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.15.0' gem 'grpc', '~> 1.15.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed gem 'google-protobuf', '~> 3.6'
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 1.0.0', require: false gem 'toml-rb', '~> 1.0.0', require: false
......
...@@ -303,7 +303,7 @@ GEM ...@@ -303,7 +303,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1) google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2) googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.6) googleauth (0.6.6)
...@@ -1005,7 +1005,7 @@ DEPENDENCIES ...@@ -1005,7 +1005,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (~> 3.6)
gpgme gpgme
grape (~> 1.1) grape (~> 1.1)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
......
...@@ -306,7 +306,7 @@ GEM ...@@ -306,7 +306,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1) google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2) googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.6) googleauth (0.6.6)
...@@ -1014,7 +1014,7 @@ DEPENDENCIES ...@@ -1014,7 +1014,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (~> 3.6)
gpgme gpgme
grape (~> 1.1) grape (~> 1.1)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
......
...@@ -6,10 +6,12 @@ import Pager from './pager'; ...@@ -6,10 +6,12 @@ import Pager from './pager';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
export default class Activities { export default class Activities {
constructor() { constructor(container = '') {
Pager.init(20, true, false, data => data, this.updateTooltips); 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(); e.preventDefault();
this.toggleFilter(e.currentTarget); this.toggleFilter(e.currentTarget);
this.reloadActivities(); this.reloadActivities();
...@@ -22,7 +24,7 @@ export default class Activities { ...@@ -22,7 +24,7 @@ export default class Activities {
reloadActivities() { reloadActivities() {
$('.content_list').html(''); $('.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) { toggleFilter(sender) {
......
import $ from 'jquery'; import $ from 'jquery';
export const addTooltipToEl = (el) => { export const addTooltipToEl = el => {
const textEl = el.querySelector('.js-breadcrumb-item-text'); const textEl = el.querySelector('.js-breadcrumb-item-text');
if (textEl && textEl.scrollWidth > textEl.offsetWidth) { if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
...@@ -14,17 +14,18 @@ export default () => { ...@@ -14,17 +14,18 @@ export default () => {
const breadcrumbs = document.querySelector('.js-breadcrumbs-list'); const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
if (breadcrumbs) { 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')) .map(el => el.querySelector('a'))
.filter(el => el); .filter(el => el);
const $expander = $('.js-breadcrumbs-collapsed-expander'); const $expander = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach(el => addTooltipToEl(el)); topLevelLinks.forEach(el => addTooltipToEl(el));
$expander.closest('.dropdown') $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => {
.on('show.bs.dropdown hide.bs.dropdown', (e) => { $('.js-breadcrumbs-collapsed-expander', e.currentTarget)
$('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open') .toggleClass('open')
.tooltip('hide'); .tooltip('hide');
}); });
} }
}; };
...@@ -12,16 +12,16 @@ export default class BuildArtifacts { ...@@ -12,16 +12,16 @@ export default class BuildArtifacts {
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
disablePropagation() { disablePropagation() {
$('.top-block').on('click', '.download', function (e) { $('.top-block').on('click', '.download', function(e) {
return e.stopPropagation(); 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(); return e.stopImmediatePropagation();
}); });
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
setupEntryClick() { 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)); visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
}); });
} }
...@@ -37,11 +37,15 @@ export default class BuildArtifacts { ...@@ -37,11 +37,15 @@ export default class BuildArtifacts {
// We want the tooltip to show if you hover anywhere on the row // 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 // But be placed below and in the middle of the file name
$('.js-artifact-tree-row') $('.js-artifact-tree-row')
.on('mouseenter', (e) => { .on('mouseenter', e => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show'); $(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('show');
}) })
.on('mouseleave', (e) => { .on('mouseleave', e => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); $(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('hide');
}); });
} }
} }
...@@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status'; ...@@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list'; import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) { function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => ` const errorList = [].concat(errors).map(
errorString => `
<li> <li>
${_.escape(errorString)} ${_.escape(errorString)}
</li> </li>
`); `,
);
return ` return `
<p> <p>
...@@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) { ...@@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) {
// Used for the variable list on CI/CD projects/groups settings page // Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList { export default class AjaxVariableList {
constructor({ constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) {
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
this.container = container; this.container = container;
this.saveButton = saveButton; this.saveButton = saveButton;
this.errorBox = errorBox; this.errorBox = errorBox;
...@@ -58,18 +54,21 @@ export default class AjaxVariableList { ...@@ -58,18 +54,21 @@ export default class AjaxVariableList {
// to match it up in `updateRowsWithPersistedVariables` // to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false); this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, { return axios
variables_attributes: this.variableList.getAllData(), .patch(
}, { this.saveEndpoint,
// 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 variables_attributes: this.variableList.getAllData(),
validateStatus: status => ( },
status >= statusCodes.OK && {
status < statusCodes.MULTIPLE_CHOICES // 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
status === statusCodes.BAD_REQUEST, validateStatus: status =>
}) (status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) ||
.then((res) => { status === statusCodes.BAD_REQUEST,
},
)
.then(res => {
loadingIcon.classList.toggle('hide', true); loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true); this.variableList.toggleEnableRow(true);
...@@ -90,18 +89,21 @@ export default class AjaxVariableList { ...@@ -90,18 +89,21 @@ export default class AjaxVariableList {
} }
updateRowsWithPersistedVariables(persistedVariables = []) { updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({ const persistedVariableMap = [].concat(persistedVariables).reduce(
...variableMap, (variableMap, variable) => ({
[variable.key]: 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 // 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 // to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) { if (convertPermissionToBoolean(destroyInput.value)) {
row.remove(); 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 { } else {
const key = row.querySelector('.js-ci-variable-input-key').value; const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key]; const persistedVariable = persistedVariableMap[key];
......
...@@ -16,10 +16,7 @@ function createEnvironmentItem(value) { ...@@ -16,10 +16,7 @@ function createEnvironmentItem(value) {
} }
export default class VariableList { export default class VariableList {
constructor({ constructor({ container, formField }) {
container,
formField,
}) {
this.$container = $(container); this.$container = $(container);
this.formField = formField; this.formField = formField;
this.environmentDropdownMap = new WeakMap(); this.environmentDropdownMap = new WeakMap();
...@@ -71,7 +68,7 @@ export default class VariableList { ...@@ -71,7 +68,7 @@ export default class VariableList {
this.initRow(rowEl); this.initRow(rowEl);
}); });
this.$container.on('click', '.js-row-remove-button', (e) => { this.$container.on('click', '.js-row-remove-button', e => {
e.preventDefault(); e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row')); this.removeRow($(e.currentTarget).closest('.js-row'));
}); });
...@@ -81,7 +78,7 @@ export default class VariableList { ...@@ -81,7 +78,7 @@ export default class VariableList {
.join(','); .join(',');
// Remove any empty rows except the last row // 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'); const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
...@@ -136,7 +133,7 @@ export default class VariableList { ...@@ -136,7 +133,7 @@ export default class VariableList {
$rowClone.removeAttr('data-is-persisted'); $rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults // Reset the inputs to their defaults
Object.keys(this.inputMap).forEach((name) => { Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default); $rowClone.find(entry.selector).val(entry.default);
}); });
...@@ -171,7 +168,7 @@ export default class VariableList { ...@@ -171,7 +168,7 @@ export default class VariableList {
} }
checkIfRowTouched($row) { checkIfRowTouched($row) {
return Object.keys(this.inputMap).some((name) => { return Object.keys(this.inputMap).some(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
const $el = $row.find(entry.selector); const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default; return $el.length && $el.val() !== entry.default;
...@@ -190,11 +187,14 @@ export default class VariableList { ...@@ -190,11 +187,14 @@ export default class VariableList {
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // 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 = {}; const resultant = {};
Object.keys(this.inputMap).forEach((name) => { Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector); const $input = $(rowEl).find(entry.selector);
if ($input.length) { if ($input.length) {
...@@ -207,11 +207,16 @@ export default class VariableList { ...@@ -207,11 +207,16 @@ export default class VariableList {
} }
getEnvironmentValues() { getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray() const valueMap = this.$container
.reduce((prevValueMap, envInput) => ({ .find(this.inputMap.environment_scope.selector)
...prevValueMap, .toArray()
[envInput.value]: envInput.value, .reduce(
}), {}); (prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}),
{},
);
return Object.keys(valueMap).map(createEnvironmentItem); return Object.keys(valueMap).map(createEnvironmentItem);
} }
......
...@@ -2,10 +2,7 @@ import $ from 'jquery'; ...@@ -2,10 +2,7 @@ import $ from 'jquery';
import VariableList from './ci_variable_list'; import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page // Used for the variable list on scheduled pipeline edit page
export default function setupNativeFormVariableList({ export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
container,
formField = 'variables',
}) {
const $container = $(container); const $container = $(container);
const variableList = new VariableList({ const variableList = new VariableList({
......
...@@ -76,12 +76,8 @@ export default class ClusterStore { ...@@ -76,12 +76,8 @@ export default class ClusterStore {
this.state.status = serverState.status; this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason; this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => { serverState.applications.forEach(serverAppEntry => {
const { const { name: appId, status, status_reason: statusReason } = serverAppEntry;
name: appId,
status,
status_reason: statusReason,
} = serverAppEntry;
this.state.applications[appId] = { this.state.applications[appId] = {
...(this.state.applications[appId] || {}), ...(this.state.applications[appId] || {}),
......
...@@ -24,36 +24,44 @@ class CommentTypeToggle { ...@@ -24,36 +24,44 @@ class CommentTypeToggle {
setConfig() { setConfig() {
const config = { const config = {
InputSetter: [{ InputSetter: [
input: this.noteTypeInput, {
valueAttribute: 'data-value', input: this.noteTypeInput,
}, valueAttribute: 'data-value',
{ },
input: this.submitButton, {
valueAttribute: 'data-submit-text', input: this.submitButton,
}], valueAttribute: 'data-submit-text',
},
],
}; };
if (this.closeButton) { if (this.closeButton) {
config.InputSetter.push({ config.InputSetter.push(
input: this.closeButton, {
valueAttribute: 'data-close-text', input: this.closeButton,
}, { valueAttribute: 'data-close-text',
input: this.closeButton, },
valueAttribute: 'data-close-text', {
inputAttribute: 'data-alternative-text', input: this.closeButton,
}); valueAttribute: 'data-close-text',
inputAttribute: 'data-alternative-text',
},
);
} }
if (this.reopenButton) { if (this.reopenButton) {
config.InputSetter.push({ config.InputSetter.push(
input: this.reopenButton, {
valueAttribute: 'data-reopen-text', input: this.reopenButton,
}, { valueAttribute: 'data-reopen-text',
input: this.reopenButton, },
valueAttribute: 'data-reopen-text', {
inputAttribute: 'data-alternative-text', input: this.reopenButton,
}); valueAttribute: 'data-reopen-text',
inputAttribute: 'data-alternative-text',
},
);
} }
return config; return config;
......
This diff is collapsed.
...@@ -19,11 +19,13 @@ export default () => { ...@@ -19,11 +19,13 @@ export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) { if (pipelineTableViewEl) {
// Update MR and Commits tabs // Update MR and Commits tabs
pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { pipelineTableViewEl.addEventListener('update-pipelines-count', event => {
if (event.detail.pipelines && if (
event.detail.pipelines &&
event.detail.pipelines.count && event.detail.pipelines.count &&
event.detail.pipelines.count.all) { event.detail.pipelines.count.all
) {
const badge = document.querySelector('.js-pipelines-mr-count'); const badge = document.querySelector('.js-pipelines-mr-count');
badge.textContent = event.detail.pipelines.count.all; badge.textContent = event.detail.pipelines.count.all;
......
<script> <script>
import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store'; import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines'; import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default { export default {
mixins: [ mixins: [pipelinesMixin],
pipelinesMixin, props: {
], endpoint: {
props: { type: String,
endpoint: { required: true,
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',
},
}, },
helpPagePath: {
type: String,
required: true,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
viewType: {
type: String,
required: false,
default: 'child',
},
},
data() { data() {
const store = new PipelineStore(); const store = new PipelineStore();
return { return {
store, store,
state: store.state, state: store.state,
}; };
}, },
computed: { computed: {
shouldRenderTable() { shouldRenderTable() {
return !this.isLoading && return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError;
this.state.pipelines.length > 0 &&
!this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
created() { shouldRenderErrorState() {
this.service = new PipelinesService(this.endpoint); return this.hasError && !this.isLoading;
}, },
methods: { },
successCallback(resp) { created() {
// depending of the endpoint the response can either bring a `pipelines` key or not. this.service = new PipelinesService(this.endpoint);
const pipelines = resp.data.pipelines || resp.data; },
this.setCommonData(pipelines); 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', { const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: { detail: {
pipelines: resp.data, pipelines: resp.data,
}, },
}); });
// notifiy to update the count in tabs // notifiy to update the count in tabs
if (this.$el.parentElement) { if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent); this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="content-list pipelines"> <div class="content-list pipelines">
......
...@@ -50,7 +50,7 @@ export function createContent(mergeRequests) { ...@@ -50,7 +50,7 @@ export function createContent(mergeRequests) {
if (mergeRequests.length === 0) { if (mergeRequests.length === 0) {
$content.text(s__('Commits|No related merge requests found')); $content.text(s__('Commits|No related merge requests found'));
} else { } else {
mergeRequests.forEach((mergeRequest) => { mergeRequests.forEach(mergeRequest => {
const $header = createHeader($content.children().length, mergeRequests.length); const $header = createHeader($content.children().length, mergeRequests.length);
const $item = createItem(mergeRequest); const $item = createItem(mergeRequest);
$content.append($header); $content.append($header);
...@@ -64,8 +64,9 @@ export function createContent(mergeRequests) { ...@@ -64,8 +64,9 @@ export function createContent(mergeRequests) {
export function fetchCommitMergeRequests() { export function fetchCommitMergeRequests() {
const $container = $('.merge-requests'); const $container = $('.merge-requests');
axios.get($container.data('projectCommitPath')) axios
.then((response) => { .get($container.data('projectCommitPath'))
.then(response => {
const $content = createContent(response.data); const $content = createContent(response.data);
$container.html($content); $container.html($content);
......
...@@ -32,22 +32,31 @@ export default class CommitsList { ...@@ -32,22 +32,31 @@ export default class CommitsList {
if (search === this.lastSearch) return Promise.resolve(); if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = `${form.attr('action')}?${form.serialize()}`; const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
this.content.fadeTo('fast', 0.5); this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { const params = form.serializeArray().reduce(
[obj.name]: obj.value, (acc, obj) =>
}), {}); Object.assign(acc, {
[obj.name]: obj.value,
}),
{},
);
return axios.get(form.attr('action'), { return axios
params, .get(form.attr('action'), {
}) params,
})
.then(({ data }) => { .then(({ data }) => {
this.lastSearch = search; this.lastSearch = search;
this.content.html(data.html); this.content.html(data.html);
this.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
window.history.replaceState({ window.history.replaceState(
page: commitsUrl, {
}, document.title, commitsUrl); page: commitsUrl,
},
document.title,
commitsUrl,
);
}) })
.catch(() => { .catch(() => {
this.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
...@@ -75,8 +84,15 @@ export default class CommitsList { ...@@ -75,8 +84,15 @@ export default class CommitsList {
processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`); processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header. // Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); commitsCount += Number(
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); $(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')); localTimeAgo($processedData.find('.js-timeago'));
......
...@@ -5,6 +5,14 @@ import 'bootstrap'; ...@@ -5,6 +5,14 @@ import 'bootstrap';
// custom jQuery functions // custom jQuery functions
$.fn.extend({ $.fn.extend({
disable() { return $(this).prop('disabled', true).addClass('disabled'); }, disable() {
enable() { return $(this).prop('disabled', false).removeClass('disabled'); }, return $(this)
.prop('disabled', true)
.addClass('disabled');
},
enable() {
return $(this)
.prop('disabled', false)
.removeClass('disabled');
},
}); });
...@@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) { ...@@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) {
$submit.disable(); $submit.disable();
$input.focus(); $input.focus();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() { $('.js-confirm-danger-input')
const confirmText = rstrip($(this).val()); .off('input')
if (confirmText === confirmTextMatch) { .on('input', function handleInput() {
$submit.enable(); const confirmText = rstrip($(this).val());
} else { if (confirmText === confirmTextMatch) {
$submit.disable(); $submit.enable();
} } else {
}); $submit.disable();
$('.js-confirm-danger-submit').off('click').on('click', () => $form.submit()); }
});
$('.js-confirm-danger-submit')
.off('click')
.on('click', () => $form.submit());
} }
export default function initConfirmDangerModal() { export default function initConfirmDangerModal() {
$(document).on('click', '.js-confirm-danger', (e) => { $(document).on('click', '.js-confirm-danger', e => {
e.preventDefault(); e.preventDefault();
const $btn = $(e.target); const $btn = $(e.target);
const $form = $btn.closest('form'); const $form = $btn.closest('form');
......
...@@ -20,8 +20,11 @@ export default class ContextualSidebar { ...@@ -20,8 +20,11 @@ export default class ContextualSidebar {
} }
bindEvents() { bindEvents() {
document.addEventListener('click', (e) => { document.addEventListener('click', e => {
if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) { if (
!e.target.closest('.nav-sidebar') &&
(bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
) {
this.toggleCollapsedSidebar(true); this.toggleCollapsedSidebar(true);
} }
}); });
......
...@@ -36,7 +36,7 @@ export default class CreateItemDropdown { ...@@ -36,7 +36,7 @@ export default class CreateItemDropdown {
}, },
selectable: true, selectable: true,
toggleLabel(selected) { 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, fieldName: this.fieldName,
text(item) { text(item) {
...@@ -46,7 +46,7 @@ export default class CreateItemDropdown { ...@@ -46,7 +46,7 @@ export default class CreateItemDropdown {
return _.escape(item.id); return _.escape(item.id);
}, },
onFilter: this.toggleCreateNewButton.bind(this), onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => { clicked: options => {
options.e.preventDefault(); options.e.preventDefault();
this.onSelect(); this.onSelect();
}, },
...@@ -77,9 +77,8 @@ export default class CreateItemDropdown { ...@@ -77,9 +77,8 @@ export default class CreateItemDropdown {
getData(term, callback) { getData(term, callback) {
this.getDataOption(term, (data = []) => { this.getDataOption(term, (data = []) => {
// Ensure the selected item isn't already in the data to avoid duplicates // Ensure the selected item isn't already in the data to avoid duplicates
const alreadyHasSelectedItem = this.selectedItem && data.some(item => const alreadyHasSelectedItem =
item.id === this.selectedItem.id, this.selectedItem && data.some(item => item.id === this.selectedItem.id);
);
let uniqueData = data; let uniqueData = data;
if (!alreadyHasSelectedItem) { if (!alreadyHasSelectedItem) {
...@@ -106,9 +105,7 @@ export default class CreateItemDropdown { ...@@ -106,9 +105,7 @@ export default class CreateItemDropdown {
if (newValue) { if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue); this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer this.$dropdownContainer.find('.js-dropdown-create-new-item code').text(newValue);
.find('.js-dropdown-create-new-item code')
.text(newValue);
} }
this.toggleFooter(!newValue); this.toggleFooter(!newValue);
......
...@@ -37,7 +37,7 @@ export default class CreateLabelDropdown { ...@@ -37,7 +37,7 @@ export default class CreateLabelDropdown {
addBinding() { addBinding() {
const self = this; const self = this;
this.$colorSuggestions.on('click', function (e) { this.$colorSuggestions.on('click', function(e) {
const $this = $(this); const $this = $(this);
self.addColorValue(e, $this); self.addColorValue(e, $this);
}); });
...@@ -47,7 +47,7 @@ export default class CreateLabelDropdown { ...@@ -47,7 +47,7 @@ export default class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this)); this.$dropdownBack.on('click', this.resetForm.bind(this));
this.$cancelButton.on('click', function (e) { this.$cancelButton.on('click', function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
...@@ -79,13 +79,9 @@ export default class CreateLabelDropdown { ...@@ -79,13 +79,9 @@ export default class CreateLabelDropdown {
} }
resetForm() { resetForm() {
this.$newLabelField this.$newLabelField.val('').trigger('change');
.val('')
.trigger('change');
this.$newColorField this.$newColorField.val('').trigger('change');
.val('')
.trigger('change');
this.$colorPreview this.$colorPreview
.css('background-color', '') .css('background-color', '')
...@@ -97,31 +93,34 @@ export default class CreateLabelDropdown { ...@@ -97,31 +93,34 @@ export default class CreateLabelDropdown {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Api.newLabel(this.namespacePath, this.projectPath, { Api.newLabel(
title: this.$newLabelField.val(), this.namespacePath,
color: this.$newColorField.val(), this.projectPath,
}, (label) => { {
this.$newLabelCreateButton.enable(); title: this.$newLabelField.val(),
color: this.$newColorField.val(),
if (label.message) { },
let errors; label => {
this.$newLabelCreateButton.enable();
if (typeof label.message === 'string') {
errors = label.message; 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 { } else {
errors = Object.keys(label.message).map(key => this.$dropdownBack.trigger('click');
`${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
this.$newLabelError $(document).trigger('created.label', label);
.html(errors) }
.show(); },
} else { );
this.$dropdownBack.trigger('click');
$(document).trigger('created.label', label);
}
});
} }
} }
...@@ -95,8 +95,10 @@ export default { ...@@ -95,8 +95,10 @@ export default {
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key'))); .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
}, },
disableKey(deployKey, callback) { disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert if (
if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { // eslint-disable-next-line no-alert
window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))
) {
this.service this.service
.disableKey(deployKey.id) .disableKey(deployKey.id)
.then(this.fetchKeys) .then(this.fetchKeys)
......
...@@ -8,17 +8,14 @@ export default class DeployKeysService { ...@@ -8,17 +8,14 @@ export default class DeployKeysService {
} }
getKeys() { getKeys() {
return this.axios.get() return this.axios.get().then(response => response.data);
.then(response => response.data);
} }
enableKey(id) { enableKey(id) {
return this.axios.put(`${id}/enable`) return this.axios.put(`${id}/enable`).then(response => response.data);
.then(response => response.data);
} }
disableKey(id) { disableKey(id) {
return this.axios.put(`${id}/disable`) return this.axios.put(`${id}/disable`).then(response => response.data);
.then(response => response.data);
} }
} }
...@@ -21,9 +21,12 @@ export default class Diff { ...@@ -21,9 +21,12 @@ export default class Diff {
}); });
const tab = document.getElementById('diffs'); 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'); const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote)); $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
...@@ -73,9 +76,10 @@ export default class Diff { ...@@ -73,9 +76,10 @@ export default class Diff {
const view = file.data('view'); const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view }; const params = { since, to, bottom, offset, unfold, view };
axios.get(link, { params }) axios
.then(({ data }) => $target.parent().replaceWith(data)) .get(link, { params })
.catch(() => flash(__('An error occurred while loading diff'))); .then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
} }
openAnchoredDiff(cb) { openAnchoredDiff(cb) {
......
...@@ -41,6 +41,11 @@ export default { ...@@ -41,6 +41,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
assignedDiscussions: false,
};
},
computed: { computed: {
...mapState({ ...mapState({
isLoading: state => state.diffs.isLoading, isLoading: state => state.diffs.isLoading,
...@@ -58,9 +63,9 @@ export default { ...@@ -58,9 +63,9 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath, plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapState('diffs', ['showTreeList']), ...mapState('diffs', ['showTreeList', 'isLoading']),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() { targetBranch() {
return { return {
branchName: this.targetBranchName, branchName: this.targetBranchName,
...@@ -147,11 +152,12 @@ export default { ...@@ -147,11 +152,12 @@ export default {
} }
}, },
setDiscussions() { setDiscussions() {
if (this.isNotesFetched) { if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
requestIdleCallback( requestIdleCallback(
() => { () =>
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); this.assignDiscussionsToDiff().then(() => {
}, this.assignedDiscussions = true;
}),
{ timeout: 1000 }, { timeout: 1000 },
); );
} }
......
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
}, },
computed: { computed: {
...mapState('diffs', ['currentDiffFileId']), ...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), ...mapGetters(['isNotesFetched']),
isCollapsed() { isCollapsed() {
return this.file.collapsed || false; return this.file.collapsed || false;
}, },
...@@ -79,7 +79,7 @@ export default { ...@@ -79,7 +79,7 @@ export default {
.then(() => { .then(() => {
requestIdleCallback( requestIdleCallback(
() => { () => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); this.assignDiscussionsToDiff();
}, },
{ timeout: 1000 }, { timeout: 1000 },
); );
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; 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 Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue'; import FileRowStats from './file_row_stats.vue';
const treeListStorageKey = 'mr_diff_tree_list';
export default { export default {
directives: {
Tooltip,
},
components: { components: {
Icon, Icon,
FileRow, FileRow,
}, },
data() { data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
const renderTreeList = treeListStored !== null ?
convertPermissionToBoolean(treeListStored) : true;
return { return {
search: '', search: '',
renderTreeList,
focusSearch: false,
}; };
}, },
computed: { computed: {
...@@ -20,15 +33,35 @@ export default { ...@@ -20,15 +33,35 @@ export default {
filteredTreeList() { filteredTreeList() {
const search = this.search.toLowerCase().trim(); 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); return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
}, },
rowDisplayTextKey() {
if (this.renderTreeList && this.search.trim() === '') {
return 'name';
}
return 'path';
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() { clearSearch() {
this.search = ''; 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, FileRowStats,
...@@ -37,28 +70,67 @@ export default { ...@@ -37,28 +70,67 @@ export default {
<template> <template>
<div class="tree-list-holder d-flex flex-column"> <div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search"> <div class="append-bottom-8 position-relative tree-list-search d-flex">
<icon <div class="flex-fill d-flex">
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"
>
<icon <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>
<div <div
class="tree-list-scroll" class="tree-list-scroll"
...@@ -72,6 +144,8 @@ export default { ...@@ -72,6 +144,8 @@ export default {
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="$options.FileRowStats" :extra-component="$options.FileRowStats"
:show-changed-icon="true" :show-changed-icon="true"
:display-text-key="rowDisplayTextKey"
:should-truncate-start="true"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
/> />
......
...@@ -5,7 +5,6 @@ import createFlash from '~/flash'; ...@@ -5,7 +5,6 @@ import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
...@@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => { ...@@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => {
// This is adding line discussions to the actual lines in the diff tree // This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode // 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); const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
Object.values(allLineDiscussions).forEach(discussions => { discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => {
if (discussions.length > 0) { commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
const { fileHash } = discussions[0]; discussion,
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { diffPositionByLineCode,
fileHash, });
discussions,
diffPositionByLineCode,
});
}
}); });
}; };
...@@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { ...@@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true }) return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true })) .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
)
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
}; };
......
...@@ -90,53 +90,67 @@ export default { ...@@ -90,53 +90,67 @@ export default {
})); }));
}, },
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) { [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); const { latestDiff } = state;
const firstDiscussion = discussions[0];
const isDiffDiscussion = firstDiscussion.diff_discussion; const discussionLineCode = discussion.line_code;
const hasLineCode = firstDiscussion.line_code; const fileHash = discussion.diff_file.file_hash;
const diffPosition = diffPositionByLineCode[firstDiscussion.line_code]; const lineCheck = ({ lineCode }) =>
lineCode === discussionLineCode &&
if (
selectedFile &&
isDiffDiscussion &&
hasLineCode &&
diffPosition &&
isDiscussionApplicableToLine({ isDiscussionApplicableToLine({
discussion: firstDiscussion, discussion,
diffPosition, diffPosition: diffPositionByLineCode[lineCode],
latestDiff: state.latestDiff, latestDiff,
}) });
) {
const targetLine = selectedFile.parallelDiffLines.find( state.diffFiles = state.diffFiles.map(diffFile => {
line => if (diffFile.fileHash === fileHash) {
(line.left && line.left.lineCode === firstDiscussion.line_code) || const file = { ...diffFile };
(line.right && line.right.lineCode === firstDiscussion.line_code),
); if (file.highlightedDiffLines) {
if (targetLine) { file.highlightedDiffLines = file.highlightedDiffLines.map(line => {
if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) { if (lineCheck(line)) {
Object.assign(targetLine.left, { return {
discussions, ...line,
}); discussions: line.discussions.concat(discussion),
} else { };
Object.assign(targetLine.right, { }
discussions,
return line;
}); });
} }
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === firstDiscussion.line_code,
);
if (targetInlineLine) { if (file.parallelDiffLines) {
Object.assign(targetInlineLine, { file.parallelDiffLines = file.parallelDiffLines.map(line => {
discussions, 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 }) { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
......
...@@ -136,7 +136,7 @@ export default function dropzoneInput(form) { ...@@ -136,7 +136,7 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any) // removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue. // and remove them from dropzone files queue.
$cancelButton.on('click', (e) => { $cancelButton.on('click', e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
...@@ -146,8 +146,10 @@ export default function dropzoneInput(form) { ...@@ -146,8 +146,10 @@ export default function dropzoneInput(form) {
// clear dropzone files queue, change status of failed files to undefined, // clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again. // and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it. // addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => { $retryLink.on('click', e => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); const dropzoneInstance = Dropzone.forElement(
e.target.closest('.js-main-target-form').querySelector('.div-dropzone'),
);
const failedFiles = dropzoneInstance.files; const failedFiles = dropzoneInstance.files;
e.preventDefault(); e.preventDefault();
...@@ -156,7 +158,7 @@ export default function dropzoneInput(form) { ...@@ -156,7 +158,7 @@ export default function dropzoneInput(form) {
// uploading of files that are being uploaded at the moment. // uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true); dropzoneInstance.removeAllFiles(true);
failedFiles.map((failedFile) => { failedFiles.map(failedFile => {
const file = failedFile; const file = failedFile;
if (file.status === Dropzone.ERROR) { if (file.status === Dropzone.ERROR) {
...@@ -168,7 +170,7 @@ export default function dropzoneInput(form) { ...@@ -168,7 +170,7 @@ export default function dropzoneInput(form) {
}); });
}); });
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
handlePaste = (event) => { handlePaste = event => {
const pasteEvent = event.originalEvent; const pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
const image = isImage(pasteEvent); const image = isImage(pasteEvent);
...@@ -182,7 +184,7 @@ export default function dropzoneInput(form) { ...@@ -182,7 +184,7 @@ export default function dropzoneInput(form) {
} }
}; };
isImage = (data) => { isImage = data => {
let i = 0; let i = 0;
while (i < data.clipboardData.items.length) { while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i]; const item = data.clipboardData.items[i];
...@@ -203,8 +205,12 @@ export default function dropzoneInput(form) { ...@@ -203,8 +205,12 @@ export default function dropzoneInput(form) {
const caretStart = textarea.selectionStart; const caretStart = textarea.selectionStart;
const caretEnd = textarea.selectionEnd; const caretEnd = textarea.selectionEnd;
const textEnd = $(child).val().length; const textEnd = $(child).val().length;
const beforeSelection = $(child).val().substring(0, caretStart); const beforeSelection = $(child)
const afterSelection = $(child).val().substring(caretEnd, textEnd); .val()
.substring(0, caretStart);
const afterSelection = $(child)
.val()
.substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection); $(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`; textarea.style.height = `${textarea.scrollHeight}px`;
...@@ -212,11 +218,11 @@ export default function dropzoneInput(form) { ...@@ -212,11 +218,11 @@ export default function dropzoneInput(form) {
return formTextarea.trigger('input'); return formTextarea.trigger('input');
}; };
addFileToForm = (path) => { addFileToForm = path => {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
}; };
getFilename = (e) => { getFilename = e => {
let value; let value;
if (window.clipboardData && window.clipboardData.getData) { if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text'); value = window.clipboardData.getData('Text');
...@@ -231,7 +237,7 @@ export default function dropzoneInput(form) { ...@@ -231,7 +237,7 @@ export default function dropzoneInput(form) {
const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
const showError = (message) => { const showError = message => {
$uploadingErrorContainer.removeClass('hide'); $uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message); $uploadingErrorMessage.html(message);
}; };
...@@ -252,14 +258,15 @@ export default function dropzoneInput(form) { ...@@ -252,14 +258,15 @@ export default function dropzoneInput(form) {
showSpinner(); showSpinner();
closeAlertMessage(); closeAlertMessage();
axios.post(uploadsPath, formData) axios
.post(uploadsPath, formData)
.then(({ data }) => { .then(({ data }) => {
const md = data.link.markdown; const md = data.link.markdown;
insertToTextArea(filename, md); insertToTextArea(filename, md);
closeSpinner(); closeSpinner();
}) })
.catch((e) => { .catch(e => {
showError(e.response.data.message); showError(e.response.data.message);
closeSpinner(); closeSpinner();
}); });
...@@ -267,7 +274,8 @@ export default function dropzoneInput(form) { ...@@ -267,7 +274,8 @@ export default function dropzoneInput(form) {
updateAttachingMessage = (files, messageContainer) => { updateAttachingMessage = (files, messageContainer) => {
let attachingMessage; 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 // Dinamycally change uploading files text depending on files number in
// dropzone files queue. // dropzone files queue.
...@@ -282,7 +290,10 @@ export default function dropzoneInput(form) { ...@@ -282,7 +290,10 @@ export default function dropzoneInput(form) {
form.find('.markdown-selector').click(function onMarkdownClick(e) { form.find('.markdown-selector').click(function onMarkdownClick(e) {
e.preventDefault(); e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click(); $(this)
.closest('.gfm-form')
.find('.div-dropzone')
.click();
formTextarea.focus(); formTextarea.focus();
}); });
......
...@@ -3,8 +3,7 @@ import Pikaday from 'pikaday'; ...@@ -3,8 +3,7 @@ import Pikaday from 'pikaday';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
import boardsStore from './boards/stores/boards_store'; import boardsStore from './boards/stores/boards_store';
class DueDateSelect { class DueDateSelect {
......
...@@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16) ...@@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) { function isRainbowFlagEmoji(emojiUnicode) {
const characters = Array.from(emojiUnicode); const characters = Array.from(emojiUnicode);
// Length 4 because flags are made of 2 characters which are surrogate pairs // 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[0].codePointAt(0) === baseFlagCodePoint &&
characters[1].codePointAt(0) === rainbowCodePoint; characters[1].codePointAt(0) === rainbowCodePoint
);
} }
// Chrome <57 renders keycaps oddly // Chrome <57 renders keycaps oddly
...@@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) { ...@@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) {
} }
// Check for a skin tone variation emoji which aren't always supported // Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16) const tone1 = 127995; // parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16) const tone5 = 127999; // parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) { function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => { return (
const cp = char.codePointAt(0); emojiUnicode.length > 2 &&
return cp >= tone1 && cp <= tone5; Array.from(emojiUnicode).some(char => {
}); const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
})
);
} }
// macOS supports most skin tone emoji's but // macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing // 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) { function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
const firstCharacter = Array.from(emojiUnicode)[0]; const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint && return (
isSkinToneComboEmoji(emojiUnicode); firstCharacter &&
firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode)
);
} }
// Check for `family_*`, `kiss_*`, `couple_*` // Check for `family_*`, `kiss_*`, `couple_*`
...@@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) ...@@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) { function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false; let hasPersonEmoji = false;
let hasZwj = false; let hasZwj = false;
Array.from(emojiUnicode).forEach((character) => { Array.from(emojiUnicode).forEach(character => {
const cp = character.codePointAt(0); const cp = character.codePointAt(0);
if (cp === zwj) { if (cp === zwj) {
hasZwj = true; hasZwj = true;
...@@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { ...@@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
// in `isEmojiUnicodeSupported` logic // in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return ( return (unicodeSupportMap.skinToneModifier && isSkinToneResult) || !isSkinToneResult;
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
} }
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice // Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
...@@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { ...@@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return ( return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || !isHorseRacingSkinToneResult
!isHorseRacingSkinToneResult
); );
} }
...@@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico ...@@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico
// in `isEmojiUnicodeSupported` logic // in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return ( return (unicodeSupportMap.personZwj && isPersonZwjResult) || !isPersonZwjResult;
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
} }
// Takes in a support map and determines whether // Takes in a support map and determines whether
...@@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { ...@@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
// //
// Combines all the edge case tests into a one-stop shop method // Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && const isOlderThanChrome57 =
unicodeSupportMap.meta &&
unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57; unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function // For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] && return (
unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode)
);
} }
export { export {
......
...@@ -2,7 +2,7 @@ import $ from 'jquery'; ...@@ -2,7 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
export default () => { export default () => {
$('.js-experiment-feature-toggle').on('change', (e) => { $('.js-experiment-feature-toggle').on('change', e => {
const el = e.target; const el = e.target;
Cookies.set(el.name, el.value, { Cookies.set(el.name, el.value, {
......
...@@ -25,13 +25,15 @@ export default { ...@@ -25,13 +25,15 @@ export default {
if (!this.userCanCreateNote) { if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined // 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'; this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) { 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)); .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
} }
}, },
...@@ -64,9 +66,11 @@ export default { ...@@ -64,9 +66,11 @@ export default {
}, },
validateButtonParent(buttonParentElement) { 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(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_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 { ...@@ -65,12 +65,15 @@ export default class FilterableList {
this.isBusy = true; this.isBusy = true;
return axios.get(this.getFilterEndpoint(), { return axios
params, .get(this.getFilterEndpoint(), {
}).then((res) => { params,
this.onFilterSuccess(res, params); })
this.onFilterComplete(); .then(res => {
}).catch(() => this.onFilterComplete()); this.onFilterSuccess(res, params);
this.onFilterComplete();
})
.catch(() => this.onFilterComplete());
} }
onFilterSuccess(response, queryData) { onFilterSuccess(response, queryData) {
...@@ -81,9 +84,13 @@ export default class FilterableList { ...@@ -81,9 +84,13 @@ export default class FilterableList {
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData); const currentPath = this.getPagePath(queryData);
return window.history.replaceState({ return window.history.replaceState(
page: currentPath, {
}, document.title, currentPath); page: currentPath,
},
document.title,
currentPath,
);
} }
onFilterComplete() { onFilterComplete() {
......
...@@ -67,6 +67,11 @@ export const conditions = [ ...@@ -67,6 +67,11 @@ export const conditions = [
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, },
{
url: 'milestone_title=Any+Milestone',
tokenKey: 'milestone',
value: 'any',
},
{ {
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
......
...@@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => { ...@@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => {
}); });
} }
flashEl.addEventListener('transitionend', () => { flashEl.addEventListener(
flashEl.remove(); 'transitionend',
window.dispatchEvent(new Event('resize')); () => {
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); flashEl.remove();
}, { window.dispatchEvent(new Event('resize'));
once: true, if (document.body.classList.contains('flash-shown'))
passive: true, document.body.classList.remove('flash-shown');
}); },
{
once: true,
passive: true,
},
);
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
}; };
...@@ -30,12 +35,12 @@ const createAction = config => ` ...@@ -30,12 +35,12 @@ const createAction = config => `
</a> </a>
`; `;
const createFlashEl = (message, type, isInContentWrapper = false) => ` const createFlashEl = (message, type, isFixedLayout = false) => `
<div <div
class="flash-${type}" class="flash-${type}"
> >
<div <div
class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
> >
${_.escape(message)} ${_.escape(message)}
</div> </div>
...@@ -69,12 +74,13 @@ const createFlash = function createFlash( ...@@ -69,12 +74,13 @@ const createFlash = function createFlash(
addBodyClass = false, addBodyClass = false,
) { ) {
const flashContainer = parent.querySelector('.flash-container'); const flashContainer = parent.querySelector('.flash-container');
const navigation = parent.querySelector('.content');
if (!flashContainer) return null; 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}`); const flashEl = flashContainer.querySelector(`.flash-${type}`);
removeFlashClickListener(flashEl, fadeTransition); removeFlashClickListener(flashEl, fadeTransition);
...@@ -83,7 +89,9 @@ const createFlash = function createFlash( ...@@ -83,7 +89,9 @@ const createFlash = function createFlash(
flashEl.innerHTML += createAction(actionConfig); flashEl.innerHTML += createAction(actionConfig);
if (actionConfig.clickHandler) { 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( ...@@ -94,11 +102,5 @@ const createFlash = function createFlash(
return flashContainer; return flashContainer;
}; };
export { export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener };
createFlash as default,
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
};
window.Flash = createFlash; window.Flash = createFlash;
...@@ -11,9 +11,13 @@ let sidebar; ...@@ -11,9 +11,13 @@ let sidebar;
export const mousePos = []; export const mousePos = [];
export const setSidebar = (el) => { sidebar = el; }; export const setSidebar = el => {
sidebar = el;
};
export const getOpenMenu = () => currentOpenMenu; 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); export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
...@@ -21,9 +25,10 @@ let headerHeight = 50; ...@@ -21,9 +25,10 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight; 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()) { if (el.classList.contains('active') && !isSidebarCollapsed()) {
return false; return false;
} }
...@@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => { ...@@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => {
return true; 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 = () => { export const getHideSubItemsInterval = () => {
if (!currentOpenMenu || !mousePos.length) return 0; if (!currentOpenMenu || !mousePos.length) return 0;
...@@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => { ...@@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => {
const currentMousePosY = currentMousePos.y; const currentMousePosY = currentMousePos.y;
const [menuTop, menuBottom] = menuCornerLocs; const [menuTop, menuBottom] = menuCornerLocs;
if (currentMousePosY < menuTop.y || if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
currentMousePosY > menuBottom.y) return 0;
if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && if (
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
) {
return HIDE_INTERVAL_TIMEOUT; return HIDE_INTERVAL_TIMEOUT;
} }
...@@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => { ...@@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height : return bottomOverflow < 0
boundingRect.top; ? boundingRect.top - outerHeight + boundingRect.height
: boundingRect.top;
}; };
export const hideMenu = (el) => { export const hideMenu = el => {
if (!el) return; if (!el) return;
const parentEl = el.parentNode; const parentEl = el.parentNode;
...@@ -101,7 +111,7 @@ export const moveSubItemsToPosition = (el, subItems) => { ...@@ -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 subItems = el.querySelector('.sidebar-sub-level-items');
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only'); const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
...@@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { ...@@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
}, timeout); }, timeout);
}; };
export const mouseLeaveTopItem = (el) => { export const mouseLeaveTopItem = el => {
const subItems = el.querySelector('.sidebar-sub-level-items'); const subItems = el.querySelector('.sidebar-sub-level-items');
if (!canShowSubItems() || !canShowActiveSubItems(el) || if (
(subItems && subItems === currentOpenMenu)) return; !canShowSubItems() ||
!canShowActiveSubItems(el) ||
(subItems && subItems === currentOpenMenu)
)
return;
el.classList.remove(IS_OVER_CLASS); el.classList.remove(IS_OVER_CLASS);
}; };
export const documentMouseMove = (e) => { export const documentMouseMove = e => {
mousePos.push({ mousePos.push({
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
...@@ -146,7 +160,7 @@ export const documentMouseMove = (e) => { ...@@ -146,7 +160,7 @@ export const documentMouseMove = (e) => {
if (mousePos.length > 6) mousePos.shift(); if (mousePos.length > 6) mousePos.shift();
}; };
export const subItemsMouseLeave = (relatedTarget) => { export const subItemsMouseLeave = relatedTarget => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
...@@ -174,7 +188,7 @@ export default () => { ...@@ -174,7 +188,7 @@ export default () => {
headerHeight = document.querySelector('.nav-sidebar').offsetTop; headerHeight = document.querySelector('.nav-sidebar').offsetTop;
items.forEach((el) => { items.forEach(el => {
const subItems = el.querySelector('.sidebar-sub-level-items'); const subItems = el.querySelector('.sidebar-sub-level-items');
if (subItems) { if (subItems) {
......
...@@ -116,7 +116,8 @@ export default class GlFieldError { ...@@ -116,7 +116,8 @@ export default class GlFieldError {
this.form.focusOnFirstInvalid.apply(this.form); this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup // 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)); .on('keyup.fieldValidator', this.updateValidity.bind(this));
} }
......
...@@ -16,9 +16,12 @@ export default class GlFieldErrors { ...@@ -16,9 +16,12 @@ export default class GlFieldErrors {
initValidators() { initValidators() {
// register selectors here as needed // register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]'] 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)) .filter(input => !input.classList.contains(customValidationFlag))
.map(input => new GlFieldError({ input, formErrors: this })); .map(input => new GlFieldError({ input, formErrors: this }));
...@@ -42,7 +45,7 @@ export default class GlFieldErrors { ...@@ -42,7 +45,7 @@ export default class GlFieldErrors {
/* Public method for triggering validity updates manually */ /* Public method for triggering validity updates manually */
updateFormValidityState() { updateFormValidityState() {
this.state.inputs.forEach((field) => { this.state.inputs.forEach(field => {
if (field.state.submitted) { if (field.state.submitted) {
field.updateValidity(); field.updateValidity();
} }
...@@ -50,8 +53,9 @@ export default class GlFieldErrors { ...@@ -50,8 +53,9 @@ export default class GlFieldErrors {
} }
focusOnFirstInvalid() { focusOnFirstInvalid() {
const firstInvalid = this.state.inputs const firstInvalid = this.state.inputs.filter(
.filter(input => !input.inputDomElement.validity.valid)[0]; input => !input.inputDomElement.validity.valid,
)[0];
firstInvalid.inputElement.focus(); firstInvalid.inputElement.focus();
} }
} }
...@@ -39,7 +39,10 @@ export default class GLForm { ...@@ -39,7 +39,10 @@ export default class GLForm {
this.form.find('.div-dropzone').remove(); this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // 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 = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM); this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form); dropzoneInput(this.form);
...@@ -55,11 +58,9 @@ export default class GLForm { ...@@ -55,11 +58,9 @@ export default class GLForm {
} }
setupAutosize() { setupAutosize() {
this.textarea.off('autosize:resized') this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this));
.on('autosize:resized', this.setHeightData.bind(this));
this.textarea.off('mouseup.autosize') this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this));
.on('mouseup.autosize', this.destroyAutosize.bind(this));
setTimeout(() => { setTimeout(() => {
autosize(this.textarea); autosize(this.textarea);
...@@ -91,10 +92,14 @@ export default class GLForm { ...@@ -91,10 +92,14 @@ export default class GLForm {
addEventListeners() { addEventListeners() {
this.textarea.on('focus', function focusTextArea() { 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.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() { ...@@ -7,8 +7,9 @@ export default function groupAvatar() {
}); });
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() { $('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form'); const form = $(this).closest('form');
// eslint-disable-next-line no-useless-escape const filename = $(this)
const filename = $(this).val().replace(/^.*[\\\/]/, ''); .val()
.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
return form.find('.js-avatar-filename').text(filename); return form.find('.js-avatar-filename').text(filename);
}); });
} }
...@@ -23,7 +23,8 @@ export default class GroupLabelSubscription { ...@@ -23,7 +23,8 @@ export default class GroupLabelSubscription {
event.preventDefault(); event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url'); const url = this.$unsubscribeButtons.attr('data-url');
axios.post(url) axios
.post(url)
.then(() => { .then(() => {
this.toggleSubscriptionButtons(); this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url'); this.$unsubscribeButtons.removeAttr('data-url');
...@@ -39,7 +40,8 @@ export default class GroupLabelSubscription { ...@@ -39,7 +40,8 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url); this.$unsubscribeButtons.attr('data-url', url);
axios.post(url) axios
.post(url)
.then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons()) .then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.'))); .catch(() => flash(__('There was an error when subscribing to this label.')));
...@@ -58,6 +60,8 @@ export default class GroupLabelSubscription { ...@@ -58,6 +60,8 @@ export default class GroupLabelSubscription {
const newTitle = tooltipTitles[type]; const newTitle = tooltipTitles[type];
$('.js-unsubscribe-button', $button.closest('.label-actions-list')) $('.js-unsubscribe-button', $button.closest('.label-actions-list'))
.tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); .tooltip('hide')
.attr('title', newTitle)
.tooltip('_fixTitle');
} }
} }
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { import {
ITEM_TYPE, ITEM_TYPE,
VISIBILITY_TYPE_ICON, VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE, GROUP_VISIBILITY_TYPE,
PROJECT_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE,
} from '../constants'; } from '../constants';
import itemStatsValue from './item_stats_value.vue'; import itemStatsValue from './item_stats_value.vue';
export default { export default {
components: { components: {
icon, icon,
timeAgoTooltip, timeAgoTooltip,
itemStatsValue, itemStatsValue,
},
props: {
item: {
type: Object,
required: true,
}, },
props: { },
item: { computed: {
type: Object, visibilityIcon() {
required: true, return VISIBILITY_TYPE_ICON[this.item.visibility];
},
}, },
computed: { visibilityTooltip() {
visibilityIcon() { if (this.item.type === ITEM_TYPE.GROUP) {
return VISIBILITY_TYPE_ICON[this.item.visibility]; return GROUP_VISIBILITY_TYPE[this.item.visibility];
}, }
visibilityTooltip() { return PROJECT_VISIBILITY_TYPE[this.item.visibility];
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;
},
}, },
}; isProject() {
return this.item.type === ITEM_TYPE.PROJECT;
},
isGroup() {
return this.item.type === ITEM_TYPE.GROUP;
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
props: {
title: {
type: String,
required: false,
default: '',
}, },
directives: { cssClass: {
tooltip, type: String,
required: false,
default: '',
}, },
props: { iconName: {
title: { type: String,
type: String, required: true,
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: '',
},
}, },
computed: { tooltipPlacement: {
isValuePresent() { type: String,
return this.value !== ''; 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> </script>
<template> <template>
......
...@@ -37,20 +37,22 @@ export default class NewGroupChild { ...@@ -37,20 +37,22 @@ export default class NewGroupChild {
getDroplabConfig() { getDroplabConfig() {
return { return {
InputSetter: [{ InputSetter: [
input: this.newGroupChildButton, {
valueAttribute: 'data-value', input: this.newGroupChildButton,
inputAttribute: 'data-action', valueAttribute: 'data-value',
}, { inputAttribute: 'data-action',
input: this.newGroupChildButton, },
valueAttribute: 'data-text', {
}], input: this.newGroupChildButton,
valueAttribute: 'data-text',
},
],
}; };
} }
bindEvents() { bindEvents() {
this.newGroupChildButton this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
} }
onClickNewGroupChildButton(e) { onClickNewGroupChildButton(e) {
......
...@@ -17,13 +17,14 @@ export default class GroupsStore { ...@@ -17,13 +17,14 @@ export default class GroupsStore {
} }
setSearchedGroups(rawGroups) { setSearchedGroups(rawGroups) {
const formatGroups = groups => groups.map((group) => { const formatGroups = groups =>
const formattedGroup = this.formatGroupItem(group); groups.map(group => {
if (formattedGroup.children && formattedGroup.children.length) { const formattedGroup = this.formatGroupItem(group);
formattedGroup.children = formatGroups(formattedGroup.children); if (formattedGroup.children && formattedGroup.children.length) {
} formattedGroup.children = formatGroups(formattedGroup.children);
return formattedGroup; }
}); return formattedGroup;
});
if (rawGroups && rawGroups.length) { if (rawGroups && rawGroups.length) {
this.state.groups = formatGroups(rawGroups); this.state.groups = formatGroups(rawGroups);
...@@ -62,10 +63,10 @@ export default class GroupsStore { ...@@ -62,10 +63,10 @@ export default class GroupsStore {
formatGroupItem(rawGroupItem) { formatGroupItem(rawGroupItem) {
const groupChildren = rawGroupItem.children || []; const groupChildren = rawGroupItem.children || [];
const groupIsOpen = (groupChildren.length > 0) || false; const groupIsOpen = groupChildren.length > 0 || false;
const childrenCount = this.hideProjects ? const childrenCount = this.hideProjects
rawGroupItem.subgroup_count : ? rawGroupItem.subgroup_count
rawGroupItem.children_count; : rawGroupItem.children_count;
return { return {
id: rawGroupItem.id, id: rawGroupItem.id,
......
...@@ -22,7 +22,7 @@ export default class TransferDropdown { ...@@ -22,7 +22,7 @@ export default class TransferDropdown {
search: { fields: ['text'] }, search: { fields: ['text'] },
data: extraOptions.concat(this.data), data: extraOptions.concat(this.data),
text: item => item.text, text: item => item.text,
clicked: (options) => { clicked: options => {
const { e } = options; const { e } = options;
e.preventDefault(); e.preventDefault();
this.assignSelected(options.selectedObj); this.assignSelected(options.selectedObj);
......
...@@ -23,7 +23,7 @@ export default function groupsSelect() { ...@@ -23,7 +23,7 @@ export default function groupsSelect() {
axios[params.type.toLowerCase()](params.url, { axios[params.type.toLowerCase()](params.url, {
params: params.data, params: params.data,
}) })
.then((res) => { .then(res => {
const results = res.data || []; const results = res.data || [];
const headers = normalizeHeaders(res.headers); const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
...@@ -36,7 +36,8 @@ export default function groupsSelect() { ...@@ -36,7 +36,8 @@ export default function groupsSelect() {
more, more,
}, },
}); });
}).catch(params.error); })
.catch(params.error);
}, },
data(search, page) { data(search, page) {
return { return {
...@@ -68,7 +69,9 @@ export default function groupsSelect() { ...@@ -68,7 +69,9 @@ export default function groupsSelect() {
} }
}, },
formatResult(object) { 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) { formatSelection(object) {
return object.full_name; return object.full_name;
......
...@@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) { ...@@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) {
const bgClass = getIdenticonBackgroundClass(entity.id); const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name); 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 = {}) { export function renderAvatar(entity, options = {}) {
......
...@@ -60,8 +60,10 @@ export default class ImageDiff { ...@@ -60,8 +60,10 @@ export default class ImageDiff {
} }
renderBadge(discussionEl, index) { renderBadge(discussionEl, index) {
const imageBadge = imageDiffHelper const imageBadge = imageDiffHelper.generateBadgeFromDiscussionDOM(
.generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl); this.imageFrameEl,
discussionEl,
);
this.imageBadges.push(imageBadge); this.imageBadges.push(imageBadge);
......
...@@ -8,5 +8,6 @@ export default () => { ...@@ -8,5 +8,6 @@ export default () => {
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file'); const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl => [...diffFileEls].forEach(diffFileEl =>
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge)); imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge),
);
}; };
...@@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff { ...@@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff {
this.imageEls = {}; this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes); const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
viewTypeNames.forEach((viewType) => { viewTypeNames.forEach(viewType => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img'); this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
}); });
} }
...@@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff { ...@@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff {
// Re-render indicator in new view // Re-render indicator in new view
if (indicator.removed) { if (indicator.removed) {
const normalizedIndicator = imageDiffHelper const normalizedIndicator = imageDiffHelper.resizeCoordinatesToImageElement(this.imageEl, {
.resizeCoordinatesToImageElement(this.imageEl, { x: indicator.x,
x: indicator.x, y: indicator.y,
y: indicator.y, width: indicator.image.width,
width: indicator.image.width, height: indicator.image.height,
height: indicator.image.height, });
});
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator); imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
} }
} }
......
...@@ -60,66 +60,71 @@ class ImporterStatus { ...@@ -60,66 +60,71 @@ class ImporterStatus {
attributes = Object.assign(repoData, attributes); attributes = Object.assign(repoData, attributes);
} }
return axios.post(this.importUrl, attributes) return axios
.then(({ data }) => { .post(this.importUrl, attributes)
const job = $(`tr#repo_${id}`); .then(({ data }) => {
job.attr('id', `project_${data.id}`); const job = $(`tr#repo_${id}`);
job.attr('id', `project_${data.id}`);
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job); job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); job.addClass('table-active');
job.find('.import-actions').html(sprintf( const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
_.escape(__('%{loadingIcon} Started')), { job.find('.import-actions').html(
loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`, sprintf(
}, _.escape(__('%{loadingIcon} Started')),
false, {
)); loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(
}) connectingVerb,
.catch((error) => { )}"></i>`,
let details = error; },
false,
const $statusField = $(`#repo_${this.id} .job-status`); ),
$statusField.text(__('Failed')); );
})
if (error.response && error.response.data && error.response.data.errors) { .catch(error => {
details = error.response.data.errors; let details = error;
}
const $statusField = $(`#repo_${this.id} .job-status`);
flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); $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() { autoUpdate() {
return axios.get(this.jobsUrl) return axios.get(this.jobsUrl).then(({ data = [] }) => {
.then(({ data = [] }) => { data.forEach(job => {
data.forEach((job) => { const jobItem = $(`#project_${job.id}`);
const jobItem = $(`#project_${job.id}`); const statusField = jobItem.find('.job-status');
const statusField = jobItem.find('.job-status');
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
switch (job.import_status) {
switch (job.import_status) { case 'finished':
case 'finished': jobItem.removeClass('table-active').addClass('table-success');
jobItem.removeClass('table-active').addClass('table-success'); statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); break;
break; case 'scheduled':
case 'scheduled': statusField.html(`${spinner} ${__('Scheduled')}`);
statusField.html(`${spinner} ${__('Scheduled')}`); break;
break; case 'started':
case 'started': statusField.html(`${spinner} ${__('Started')}`);
statusField.html(`${spinner} ${__('Started')}`); break;
break; case 'failed':
case 'failed': statusField.html(__('Failed'));
statusField.html(__('Failed')); break;
break; default:
default: statusField.html(job.import_status);
statusField.html(job.import_status); break;
break; }
}
});
}); });
});
} }
setAutoUpdate() { setAutoUpdate() {
...@@ -141,7 +146,4 @@ function initImporterStatus() { ...@@ -141,7 +146,4 @@ function initImporterStatus() {
} }
} }
export { export { initImporterStatus as default, ImporterStatus };
initImporterStatus as default,
ImporterStatus,
};
import $ from 'jquery'; import $ from 'jquery';
import { stickyMonitor } from './lib/utils/sticky'; import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => { export default stickyTop => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({ $('.js-diff-stats-dropdown').glDropdown({
......
...@@ -2,13 +2,7 @@ import Notes from './notes'; ...@@ -2,13 +2,7 @@ import Notes from './notes';
export default () => { export default () => {
const dataEl = document.querySelector('.js-notes-data'); const dataEl = document.querySelector('.js-notes-data');
const { const { notesUrl, notesIds, now, diffView, enableGFM } = JSON.parse(dataEl.innerHTML);
notesUrl,
notesIds,
now,
diffView,
enableGFM,
} = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign // 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 // into the window object, we can just access the current isntance with Notes.instance
......
...@@ -97,7 +97,8 @@ export default class IntegrationSettingsForm { ...@@ -97,7 +97,8 @@ export default class IntegrationSettingsForm {
testSettings(formData) { testSettings(formData) {
this.toggleSubmitBtnState(true); this.toggleSubmitBtnState(true);
return axios.put(this.testEndPoint, formData) return axios
.put(this.testEndPoint, formData)
.then(({ data }) => { .then(({ data }) => {
if (data.error) { if (data.error) {
let flashActions; let flashActions;
...@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm { ...@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) { if (data.test_failed) {
flashActions = { flashActions = {
title: 'Save anyway', title: 'Save anyway',
clickHandler: (e) => { clickHandler: e => {
e.preventDefault(); e.preventDefault();
this.$form.submit(); this.$form.submit();
}, },
......
...@@ -27,7 +27,10 @@ class AutoWidthDropdownSelect { ...@@ -27,7 +27,10 @@ class AutoWidthDropdownSelect {
// We have to look at the parent because // We have to look at the parent because
// `offsetParent` on a `display: none;` is `null` // `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 // Reset any width to let it naturally flow
$dropdown.css('width', 'auto'); $dropdown.css('width', 'auto');
if ($dropdown.outerWidth(false) > offsetParentWidth) { if ($dropdown.outerWidth(false) > offsetParentWidth) {
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
onFormSubmitFailure() { onFormSubmitFailure() {
this.form.find('[type="submit"]').enable(); this.form.find('[type="submit"]').enable();
return new Flash("Issue update failed"); return new Flash('Issue update failed');
}, },
getSelectedIssues() { getSelectedIssues() {
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
const result = []; const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate'); const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => { this.getLabelsFromSelection().forEach(id => {
if (labelsToKeep.indexOf(id) === -1) { if (labelsToKeep.indexOf(id) === -1) {
result.push(id); result.push(id);
} }
...@@ -89,8 +89,8 @@ export default { ...@@ -89,8 +89,8 @@ export default {
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [], add_label_ids: [],
remove_label_ids: [] remove_label_ids: [],
} },
}; };
if (this.willUpdateLabels) { if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked'); formData.update.add_label_ids = this.$labelDropdown.data('marked');
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
// Collect unique label IDs for all checked issues // Collect unique label IDs for all checked issues
this.getElement('.selected-issuable:checked').each((i, el) => { this.getElement('.selected-issuable:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => { issuableLabels.forEach(labelId => {
// Store unique IDs // Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) { if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId); uniqueIds.push(labelId);
......
/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
/* global GitLab */
import $ from 'jquery'; import $ from 'jquery';
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import Autosave from './autosave'; import Autosave from './autosave';
...@@ -8,7 +5,7 @@ import UsersSelect from './users_select'; ...@@ -8,7 +5,7 @@ import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; 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 { export default class IssuableForm {
constructor(form) { constructor(form) {
...@@ -19,9 +16,11 @@ export default class IssuableForm { ...@@ -19,9 +16,11 @@ export default class IssuableForm {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); this.gfmAutoComplete = new GfmAutoComplete(
new UsersSelect(); gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
new ZenMode(); ).setup();
this.usersSelect = new UsersSelect();
this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]'); this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]'); this.descriptionField = this.form.find('textarea[name*="[description]"]');
...@@ -57,8 +56,16 @@ export default class IssuableForm { ...@@ -57,8 +56,16 @@ export default class IssuableForm {
} }
initAutosave() { initAutosave() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']); this.autosave = new Autosave(this.titleField, [
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']); document.location.pathname,
document.location.search,
'title',
]);
return new Autosave(this.descriptionField, [
document.location.pathname,
document.location.search,
'description',
]);
} }
handleSubmit() { handleSubmit() {
...@@ -74,7 +81,7 @@ export default class IssuableForm { ...@@ -74,7 +81,7 @@ export default class IssuableForm {
this.$wipExplanation = this.form.find('.js-wip-explanation'); this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation'); this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return; return undefined;
} }
this.form.on('click', '.js-toggle-wip', this.toggleWip); this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation); this.titleField.on('keyup blur', this.renderWipExplanation);
...@@ -89,10 +96,9 @@ export default class IssuableForm { ...@@ -89,10 +96,9 @@ export default class IssuableForm {
if (this.workInProgress()) { if (this.workInProgress()) {
this.$wipExplanation.show(); this.$wipExplanation.show();
return this.$noWipExplanation.hide(); return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
} }
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
} }
toggleWip(event) { toggleWip(event) {
...@@ -110,7 +116,7 @@ export default class IssuableForm { ...@@ -110,7 +116,7 @@ export default class IssuableForm {
} }
addWip() { addWip() {
this.titleField.val(`WIP: ${(this.titleField.val())}`); this.titleField.val(`WIP: ${this.titleField.val()}`);
} }
initTargetBranchDropdown() { initTargetBranchDropdown() {
......
...@@ -77,11 +77,11 @@ ...@@ -77,11 +77,11 @@
'shouldRenderCalloutMessage', 'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel', 'shouldRenderTriggeredLabel',
'hasEnvironment', 'hasEnvironment',
'isJobStuck',
'hasTrace', 'hasTrace',
'emptyStateIllustration', 'emptyStateIllustration',
'isScrollingDown', 'isScrollingDown',
'emptyStateAction', 'emptyStateAction',
'hasRunnersForProject',
]), ]),
shouldRenderContent() { shouldRenderContent() {
...@@ -195,9 +195,9 @@ ...@@ -195,9 +195,9 @@
<!-- Body Section --> <!-- Body Section -->
<stuck-block <stuck-block
v-if="isJobStuck" v-if="job.stuck"
class="js-job-stuck" class="js-job-stuck"
:has-no-runners-for-project="job.runners.available" :has-no-runners-for-project="hasRunnersForProject"
:tags="job.tags" :tags="job.tags"
:runners-path="runnerSettingsUrl" :runners-path="runnerSettingsUrl"
/> />
......
<script> <script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -9,11 +8,9 @@ export default { ...@@ -9,11 +8,9 @@ export default {
CiIcon, CiIcon,
Icon, Icon,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
job: { job: {
type: Object, type: Object,
...@@ -24,10 +21,9 @@ export default { ...@@ -24,10 +21,9 @@ export default {
required: true, required: true,
}, },
}, },
computed: { computed: {
tooltipText() { tooltipText() {
return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`; return `${this.job.name} - ${this.job.status.tooltip}`;
}, },
}, },
}; };
...@@ -36,7 +32,10 @@ export default { ...@@ -36,7 +32,10 @@ export default {
<template> <template>
<div <div
class="build-job" class="build-job"
:class="{ retried: job.retried, active: isActive }" :class="{
retried: job.retried,
active: isActive
}"
> >
<a <a
v-tooltip v-tooltip
......
...@@ -23,14 +23,7 @@ export default { ...@@ -23,14 +23,7 @@ export default {
<template> <template>
<div class="bs-callout bs-callout-warning"> <div class="bs-callout bs-callout-warning">
<p <p
v-if="hasNoRunnersForProject" v-if="tags.length"
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"
class="js-stuck-with-tags append-bottom-0" class="js-stuck-with-tags append-bottom-0"
> >
{{ s__(`This job is stuck, because you don't have {{ s__(`This job is stuck, because you don't have
...@@ -43,6 +36,13 @@ export default { ...@@ -43,6 +36,13 @@ export default {
{{ tag }} {{ tag }}
</span> </span>
</p> </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 <p
v-else v-else
class="js-stuck-no-active-runner append-bottom-0" class="js-stuck-no-active-runner append-bottom-0"
......
...@@ -41,17 +41,10 @@ export const emptyStateIllustration = state => ...@@ -41,17 +41,10 @@ export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {}; (state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; 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 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; 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 'jquery';
import _ from 'underscore';
import timeago from 'timeago.js'; import timeago from 'timeago.js';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { pluralize } from './text_utility'; import { pluralize } from './text_utility';
...@@ -46,6 +47,8 @@ const getMonthNames = abbreviated => { ...@@ -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 * Given a date object returns the day of the week in English
* @param {date} date * @param {date} date
...@@ -74,10 +77,10 @@ let timeagoInstance; ...@@ -74,10 +77,10 @@ let timeagoInstance;
/** /**
* Sets a timeago Instance * Sets a timeago Instance
*/ */
export function getTimeago() { export const getTimeago = () => {
if (!timeagoInstance) { if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) { const localeRemaining = (number, index) =>
return [ [
[s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
...@@ -93,9 +96,9 @@ export function getTimeago() { ...@@ -93,9 +96,9 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index]; ][index];
};
const locale = function getLocale(number, index) { const locale = (number, index) =>
return [ [
[s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
...@@ -111,7 +114,6 @@ export function getTimeago() { ...@@ -111,7 +114,6 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index]; ][index];
};
timeago.register(timeagoLanguageCode, locale); timeago.register(timeagoLanguageCode, locale);
timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining); timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
...@@ -119,7 +121,7 @@ export function getTimeago() { ...@@ -119,7 +121,7 @@ export function getTimeago() {
} }
return timeagoInstance; return timeagoInstance;
} };
/** /**
* For the given element, renders a timeago instance. * For the given element, renders a timeago instance.
...@@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => { ...@@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => {
* @param {Number} seconds * @param {Number} seconds
* @return {String} * @return {String}
*/ */
export function timeIntervalInWords(intervalInSeconds) { export const timeIntervalInWords = intervalInSeconds => {
const secondsInteger = parseInt(intervalInSeconds, 10); const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60); const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - minutes * 60; const seconds = secondsInteger - minutes * 60;
...@@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) {
text = `${seconds} ${pluralize('second', seconds)}`; text = `${seconds} ${pluralize('second', seconds)}`;
} }
return text; return text;
} };
export function dateInWords(date, abbreviated = false, hideYear = false) { export const dateInWords = (date, abbreviated = false, hideYear = false) => {
if (!date) return date; if (!date) return date;
const month = date.getMonth(); const month = date.getMonth();
...@@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { ...@@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
} }
return `${monthName} ${date.getDate()}, ${year}`; return `${monthName} ${date.getDate()}, ${year}`;
} };
/** /**
* Returns month name based on provided date. * Returns month name based on provided date.
...@@ -391,3 +393,83 @@ export const formatTime = milliseconds => { ...@@ -391,3 +393,83 @@ export const formatTime = milliseconds => {
formattedTime += remainingSeconds; formattedTime += remainingSeconds;
return formattedTime; 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 $ from 'jquery';
import Pikaday from 'pikaday'; 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 // 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 // children of an element with the `clearable-input` class, and have a sibling
......
...@@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import initDiffsApp from '../diffs'; import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue'; import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue'; import discussionCounter from '../notes/components/discussion_counter.vue';
import initDiscussionFilters from '../notes/discussion_filters';
import store from './stores'; import store from './stores';
import MergeRequest from '../merge_request'; import MergeRequest from '../merge_request';
...@@ -88,5 +89,6 @@ export default function initMrNotes() { ...@@ -88,5 +89,6 @@ export default function initMrNotes() {
}, },
}); });
initDiscussionFilters(store);
initDiffsApp(store); initDiffsApp(store);
} }
...@@ -56,10 +56,11 @@ export default { ...@@ -56,10 +56,11 @@ export default {
</script> </script>
<template> <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>
<div <div
v-if="discussionCount > 0"
:class="{ 'has-next-btn': hasNextButton }" :class="{ 'has-next-btn': hasNextButton }"
class="line-resolve-all"> class="line-resolve-all">
<span <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 { ...@@ -50,11 +50,11 @@ export default {
}, },
data() { data() {
return { return {
isLoading: true, currentFilter: null,
}; };
}, },
computed: { computed: {
...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']), ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
noteableType() { noteableType() {
return this.noteableData.noteableType; return this.noteableData.noteableType;
}, },
...@@ -102,6 +102,7 @@ export default { ...@@ -102,6 +102,7 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
setLoadingState: 'setLoadingState',
fetchDiscussions: 'fetchDiscussions', fetchDiscussions: 'fetchDiscussions',
poll: 'poll', poll: 'poll',
actionToggleAward: 'toggleAward', actionToggleAward: 'toggleAward',
...@@ -133,19 +134,19 @@ export default { ...@@ -133,19 +134,19 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion }; return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
}, },
fetchNotes() { fetchNotes() {
return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath')) return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => { .then(() => {
this.initPolling(); this.initPolling();
}) })
.then(() => { .then(() => {
this.isLoading = false; this.setLoadingState(false);
this.setNotesFetchedState(true); this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData'); eventHub.$emit('fetchedNotesData');
}) })
.then(() => this.$nextTick()) .then(() => this.$nextTick())
.then(() => this.checkLocationHash()) .then(() => this.checkLocationHash())
.catch(() => { .catch(() => {
this.isLoading = false; this.setLoadingState(false);
this.setNotesFetchedState(true); this.setNotesFetchedState(true);
Flash('Something went wrong while fetching comments. Please try again.'); 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 Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import createStore from './stores'; import createStore from './stores';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const store = createStore(); const store = createStore();
initDiscussionFilters(store);
return new Vue({ return new Vue({
el: '#js-vue-notes', el: '#js-vue-notes',
components: { components: {
......
...@@ -5,8 +5,9 @@ import * as constants from '../constants'; ...@@ -5,8 +5,9 @@ import * as constants from '../constants';
Vue.use(VueResource); Vue.use(VueResource);
export default { export default {
fetchDiscussions(endpoint) { fetchDiscussions(endpoint, filter) {
return Vue.http.get(endpoint); const config = filter !== undefined ? { params: { notes_filter: filter } } : null;
return Vue.http.get(endpoint, config);
}, },
deleteNote(endpoint) { deleteNote(endpoint) {
return Vue.http.delete(endpoint); return Vue.http.delete(endpoint);
......
...@@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler'; ...@@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale';
let eTagPoll; let eTagPoll;
...@@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) => ...@@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchDiscussions = ({ commit }, path) => export const fetchDiscussions = ({ commit }, { path, filter }) =>
service service
.fetchDiscussions(path) .fetchDiscussions(path, filter)
.then(res => res.json()) .then(res => res.json())
.then(discussions => { .then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.SET_INITIAL_DISCUSSIONS, discussions);
...@@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { ...@@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (discussion) { if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) { } else if (note.type === constants.DIFF_NOTE) {
dispatch('fetchDiscussions', state.notesData.discussionsPath); dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else { } else {
commit(types.ADD_NEW_NOTE, note); commit(types.ADD_NEW_NOTE, note);
} }
...@@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => { ...@@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => {
mrWidgetEventHub.$emit('mr.discussion.updated'); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import _ from 'underscore'; import _ from 'underscore';
import * as constants from '../constants'; import * as constants from '../constants';
import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils'; import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions); export const discussions = state => collapseSystemNotes(state.discussions);
...@@ -11,6 +10,8 @@ export const getNotesData = state => state.notesData; ...@@ -11,6 +10,8 @@ export const getNotesData = state => state.notesData;
export const isNotesFetched = state => state.isNotesFetched; export const isNotesFetched = state => state.isNotesFetched;
export const isLoading = state => state.isLoading;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData; export const getNoteableData = state => state.noteableData;
...@@ -29,9 +30,6 @@ export const notesById = state => ...@@ -29,9 +30,6 @@ export const notesById = state =>
return acc; return acc;
}, {}); }, {});
export const discussionsStructuredByLineCode = state =>
reduceDiscussionsToLineCodes(state.discussions);
export const noteableType = state => { export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
......
...@@ -11,6 +11,7 @@ export default () => ({ ...@@ -11,6 +11,7 @@ export default () => ({
// View layer // View layer
isToggleStateButtonLoading: false, isToggleStateButtonLoading: false,
isNotesFetched: false, isNotesFetched: false,
isLoading: true,
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: { notesData: {
......
...@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; ...@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
// DISCUSSION // DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
......
...@@ -216,6 +216,10 @@ export default { ...@@ -216,6 +216,10 @@ export default {
Object.assign(state, { isNotesFetched: value }); Object.assign(state, { isNotesFetched: value });
}, },
[types.SET_NOTES_LOADING_STATE](state, value) {
state.isLoading = value;
},
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
......
...@@ -25,18 +25,6 @@ export const getQuickActionText = note => { ...@@ -25,18 +25,6 @@ export const getQuickActionText = note => {
return text; 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 hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
...@@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400; ...@@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
export default { 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.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
this.limit = limit; this.limit = limit;
this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable; this.disable = disable;
this.prepareData = prepareData; this.prepareData = prepareData;
this.callback = callback; this.callback = callback;
this.loading = $('.loading').first(); this.loading = $(`${container} .loading`).first();
if (preload) { if (preload) {
this.offset = 0; this.offset = 0;
this.getOld(); this.getOld();
......
...@@ -170,7 +170,7 @@ export default class UserTabs { ...@@ -170,7 +170,7 @@ export default class UserTabs {
this.loadActivityCalendar('activity'); this.loadActivityCalendar('activity');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Activities(); new Activities('#activity');
this.loaded.activity = true; this.loaded.activity = true;
} }
......
<script> <script>
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/pretty_time'; import { abbreviateTime } from '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'TimeTrackingCollapsedState', name: 'TimeTrackingCollapsedState',
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
props: {
showComparisonState: {
type: Boolean,
required: true,
}, },
directives: { showSpentOnlyState: {
tooltip, type: Boolean,
required: true,
}, },
props: { showEstimateOnlyState: {
showComparisonState: { type: Boolean,
type: Boolean, required: true,
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: '',
},
}, },
computed: { showNoTimeTrackingState: {
timeSpent() { type: Boolean,
return this.abbreviateTime(this.timeSpentHumanReadable); required: true,
}, },
timeEstimate() { timeSpentHumanReadable: {
return this.abbreviateTime(this.timeEstimateHumanReadable); type: String,
}, required: false,
divClass() { default: '',
if (this.showComparisonState) { },
return 'compare'; timeEstimateHumanReadable: {
} else if (this.showEstimateOnlyState) { type: String,
return 'estimate-only'; required: false,
} else if (this.showSpentOnlyState) { default: '',
return 'spend-only'; },
} else if (this.showNoTimeTrackingState) { },
return 'no-tracking'; 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 ''; return '';
}, } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
spanClass() { return 'bold';
if (this.showComparisonState) { } else if (this.showNoTimeTrackingState) {
return ''; return 'no-value';
} else if (this.showEstimateOnlyState || this.showSpentOnlyState) { }
return 'bold';
} else if (this.showNoTimeTrackingState) {
return 'no-value';
}
return ''; return '';
}, },
text() { text() {
if (this.showComparisonState) { if (this.showComparisonState) {
return `${this.timeSpent} / ${this.timeEstimate}`; return `${this.timeSpent} / ${this.timeEstimate}`;
} else if (this.showEstimateOnlyState) { } else if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimate}`; return `-- / ${this.timeEstimate}`;
} else if (this.showSpentOnlyState) { } else if (this.showSpentOnlyState) {
return `${this.timeSpent} / --`; return `${this.timeSpent} / --`;
} else if (this.showNoTimeTrackingState) { } else if (this.showNoTimeTrackingState) {
return 'None'; return 'None';
} }
return ''; return '';
}, },
timeTrackedTooltipText() { timeTrackedTooltipText() {
let title; let title;
if (this.showComparisonState) { if (this.showComparisonState) {
title = __('Time remaining'); title = __('Time remaining');
} else if (this.showEstimateOnlyState) { } else if (this.showEstimateOnlyState) {
title = __('Estimated'); title = __('Estimated');
} else if (this.showSpentOnlyState) { } else if (this.showSpentOnlyState) {
title = __('Time spent'); title = __('Time spent');
} }
return sprintf('%{title}: %{text}', ({ title, text: this.text })); return sprintf('%{title}: %{text}', { title, text: this.text });
}, },
tooltipText() { tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
}, },
methods: { },
abbreviateTime(timeStr) { methods: {
return abbreviateTime(timeStr); abbreviateTime(timeStr) {
}, return abbreviateTime(timeStr);
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
......
...@@ -34,10 +34,21 @@ export default { ...@@ -34,10 +34,21 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
displayTextKey: {
type: String,
required: false,
default: 'name',
},
shouldTruncateStart: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
mouseOver: false, mouseOver: false,
truncateStart: 0,
}; };
}, },
computed: { computed: {
...@@ -60,6 +71,15 @@ export default { ...@@ -60,6 +71,15 @@ export default {
'is-open': this.file.opened, '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: { watch: {
'file.active': function fileActiveWatch(active) { 'file.active': function fileActiveWatch(active) {
...@@ -72,6 +92,15 @@ export default { ...@@ -72,6 +92,15 @@ export default {
if (this.hasPathAtCurrentRoute()) { if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true); 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: { methods: {
toggleTreeOpen(path) { toggleTreeOpen(path) {
...@@ -139,6 +168,7 @@ export default { ...@@ -139,6 +168,7 @@ export default {
class="file-row-name-container" class="file-row-name-container"
> >
<span <span
ref="textOutput"
:style="levelIndentation" :style="levelIndentation"
class="file-row-name str-truncated" class="file-row-name str-truncated"
> >
...@@ -156,7 +186,7 @@ export default { ...@@ -156,7 +186,7 @@ export default {
:size="16" :size="16"
class="append-right-5" class="append-right-5"
/> />
{{ file.name }} {{ outputText }}
</span> </span>
<component <component
:is="extraComponent" :is="extraComponent"
...@@ -175,6 +205,8 @@ export default { ...@@ -175,6 +205,8 @@ export default {
:hide-extra-on-tree="hideExtraOnTree" :hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent" :extra-component="extraComponent"
:show-changed-icon="showChangedIcon" :show-changed-icon="showChangedIcon"
:display-text-key="displayTextKey"
:should-truncate-start="shouldTruncateStart"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile" @clickFile="clickedFile"
/> />
......
<script> <script>
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
export default { export default {
name: 'DatePicker', name: 'DatePicker',
......
...@@ -334,6 +334,14 @@ img.emoji { ...@@ -334,6 +334,14 @@ img.emoji {
} }
} }
.outline-0 {
outline: 0;
&:focus {
outline: 0;
}
}
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; } .prepend-top-2 { margin-top: 2px; }
...@@ -369,3 +377,5 @@ img.emoji { ...@@ -369,3 +377,5 @@ img.emoji {
.flex-align-self-center { align-self: center; } .flex-align-self-center { align-self: center; }
.flex-grow { flex-grow: 1; } .flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; } .flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
button { button {
padding-top: 0; padding-top: 0;
background-color: transparent;
} }
&.active a, &.active a,
......
...@@ -1027,8 +1027,12 @@ ...@@ -1027,8 +1027,12 @@
overflow-x: auto; overflow-x: auto;
} }
.tree-list-search .form-control { .tree-list-search {
padding-left: 30px; flex: 0 0 34px;
.form-control {
padding-left: 30px;
}
} }
.tree-list-icon { .tree-list-icon {
...@@ -1063,3 +1067,9 @@ ...@@ -1063,3 +1067,9 @@
} }
} }
} }
.tree-list-view-toggle {
svg {
top: 0;
}
}
...@@ -185,7 +185,17 @@ ul.related-merge-requests > li { ...@@ -185,7 +185,17 @@ ul.related-merge-requests > li {
} }
.new-branch-col { .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 { .create-mr-dropdown-wrap {
...@@ -205,6 +215,10 @@ ul.related-merge-requests > li { ...@@ -205,6 +215,10 @@ ul.related-merge-requests > li {
.btn-group:not(.hidden) { .btn-group:not(.hidden) {
display: flex; display: flex;
@include media-breakpoint-down(md) {
margin-top: $gl-padding-8;
}
} }
.js-create-merge-request { .js-create-merge-request {
...@@ -251,7 +265,6 @@ ul.related-merge-requests > li { ...@@ -251,7 +265,6 @@ ul.related-merge-requests > li {
.new-branch-col { .new-branch-col {
padding-top: 0; padding-top: 0;
text-align: right;
align-self: center; align-self: center;
} }
...@@ -262,3 +275,9 @@ ul.related-merge-requests > li { ...@@ -262,3 +275,9 @@ ul.related-merge-requests > li {
} }
} }
} }
@include media-breakpoint-up(lg) {
.new-branch-col {
text-align: right;
}
}
...@@ -818,9 +818,17 @@ ...@@ -818,9 +818,17 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@include media-breakpoint-down(xs) { @include media-breakpoint-down(md) {
flex-direction: column-reverse; 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) { .limit-container-width:not(.container-limited) {
......
...@@ -618,7 +618,6 @@ ul.notes { ...@@ -618,7 +618,6 @@ ul.notes {
.line-resolve-all-container { .line-resolve-all-container {
@include notes-media('min', map-get($grid-breakpoints, sm)) { @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0; margin-right: 0;
padding-left: $gl-padding;
} }
> div { > div {
...@@ -756,3 +755,23 @@ ul.notes { ...@@ -756,3 +755,23 @@ ul.notes {
margin-top: 4px; 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 @@ ...@@ -2,6 +2,7 @@
module IssuableActions module IssuableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
included do included do
before_action :labels, only: [:show, :new, :edit] before_action :labels, only: [:show, :new, :edit]
...@@ -95,10 +96,14 @@ module IssuableActions ...@@ -95,10 +96,14 @@ module IssuableActions
def discussions def discussions
notes = issuable.discussion_notes notes = issuable.discussion_notes
.inc_relations_for_view .inc_relations_for_view
.with_notes_filter(notes_filter)
.includes(:noteable) .includes(:noteable)
.fresh .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 = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
...@@ -110,6 +115,32 @@ module IssuableActions ...@@ -110,6 +115,32 @@ module IssuableActions
private 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 def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity) DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end end
......
...@@ -17,10 +17,17 @@ module NotesActions ...@@ -17,10 +17,17 @@ module NotesActions
notes_json = { notes: [], last_fetched_at: current_fetched_at } notes_json = { notes: [], last_fetched_at: current_fetched_at }
notes = notes_finder.execute notes = notes_finder
.inc_relations_for_view .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 = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
...@@ -224,6 +231,10 @@ module NotesActions ...@@ -224,6 +231,10 @@ module NotesActions
request.headers['X-Last-Fetched-At'] request.headers['X-Last-Fetched-At']
end end
def notes_filter
current_user&.notes_filter_for(params[:target_type])
end
def notes_finder def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params) @notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end end
......
...@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
alias_method :awardable, :note alias_method :awardable, :note
def finder_params def finder_params
params.merge(last_fetched_at: last_fetched_at) params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter)
end end
def authorize_admin_note! 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