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