Commit 1082361a authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-16

parents 9ad5803b d87e88a6
......@@ -22,9 +22,7 @@ export default {
directives: {
tooltip,
},
mixins: [
timeagoMixin,
],
mixins: [timeagoMixin],
props: {
deployment: {
type: Object,
......@@ -50,7 +48,7 @@ export default {
return !!(this.deployment.url && this.deployment.name);
},
hasMetrics() {
return !!(this.deployment.metrics_url);
return !!this.deployment.metrics_url;
},
},
methods: {
......@@ -63,7 +61,7 @@ export default {
MRWidgetService.stopEnvironment(this.deployment.stop_url)
.then(res => res.data)
.then((data) => {
.then(data => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
......
......@@ -60,11 +60,29 @@ export default {
let memoryUsageMsg = '';
if (memoryTo > memoryFrom) {
memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
memoryUsageMsg = sprintf(
s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
),
messageProps,
false,
);
} else if (memoryTo < memoryFrom) {
memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
memoryUsageMsg = sprintf(
s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
),
messageProps,
false,
);
} else {
memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB'), messageProps, false);
memoryUsageMsg = sprintf(
s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB',
),
messageProps,
false,
);
}
return memoryUsageMsg;
......@@ -77,7 +95,7 @@ export default {
methods: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
return (bytesToMiB(valueInBytes)).toFixed(2);
return bytesToMiB(valueInBytes).toFixed(2);
},
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
......@@ -103,7 +121,7 @@ export default {
loadMetrics() {
backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
.then(res => {
if (res.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
......@@ -114,14 +132,14 @@ export default {
})
.catch(stop);
})
.then((res) => {
.then(res => {
if (res.status === statusCodes.NO_CONTENT) {
return res;
}
return res.data;
})
.then((data) => {
.then(data => {
this.computeGraphData(data.metrics, data.deployment_time);
return data;
})
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'MrWidgetAuthor',
directives: {
tooltip,
export default {
name: 'MrWidgetAuthor',
directives: {
tooltip,
},
props: {
author: {
type: Object,
required: true,
},
props: {
author: {
type: Object,
required: true,
},
showAuthorName: {
type: Boolean,
required: false,
default: true,
},
showAuthorTooltip: {
type: Boolean,
required: false,
default: false,
},
showAuthorName: {
type: Boolean,
required: false,
default: true,
},
computed: {
authorUrl() {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
return this.author.avatarUrl || this.author.avatar_url;
},
showAuthorTooltip: {
type: Boolean,
required: false,
default: false,
},
};
},
computed: {
authorUrl() {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
return this.author.avatarUrl || this.author.avatar_url;
},
},
};
</script>
<template>
<a
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import MrWidgetAuthor from './mr_widget_author.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import MrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MrWidgetAuthorTime',
components: {
MrWidgetAuthor,
export default {
name: 'MrWidgetAuthorTime',
components: {
MrWidgetAuthor,
},
directives: {
tooltip,
},
props: {
actionText: {
type: String,
required: true,
},
directives: {
tooltip,
author: {
type: Object,
required: true,
},
props: {
actionText: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
dateTitle: {
type: String,
required: true,
},
dateReadable: {
type: String,
required: true,
},
dateTitle: {
type: String,
required: true,
},
};
dateReadable: {
type: String,
required: true,
},
},
};
</script>
<template>
<h4 class="js-mr-widget-author">
......
<script>
import { sprintf, s__ } from '~/locale';
import { sprintf, s__ } from '~/locale';
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: {
type: String,
required: false,
default: '',
},
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: {
type: String,
required: false,
default: '',
},
computed: {
missingBranchInfo() {
return sprintf(
s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'),
{ branch: this.missingBranch },
);
},
},
computed: {
missingBranchInfo() {
return sprintf(
s__(
'mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the',
),
{ branch: this.missingBranch },
);
},
};
},
};
</script>
<template>
<section class="mr-widget-help">
......
<script>
import { s__ } from '~/locale';
import { s__ } from '~/locale';
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: {
type: Object,
required: true,
default: () => ({}),
},
state: {
type: String,
required: false,
default: '',
},
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: {
type: Object,
required: true,
default: () => ({}),
},
computed: {
closesText() {
if (this.state === 'merged') {
return s__('mrWidget|Closed');
}
if (this.state === 'closed') {
return s__('mrWidget|Did not close');
}
return s__('mrWidget|Closes');
},
state: {
type: String,
required: false,
default: '',
},
};
},
computed: {
closesText() {
if (this.state === 'merged') {
return s__('mrWidget|Closed');
}
if (this.state === 'closed') {
return s__('mrWidget|Did not close');
}
return s__('mrWidget|Closes');
},
},
};
</script>
<template>
<section class="mr-info-list mr-links">
......
<script>
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
export default {
components: {
ciIcon,
},
props: {
status: {
type: String,
required: true,
},
props: {
status: {
type: String,
required: true,
},
showDisabledButton: {
type: Boolean,
required: false,
default: false,
},
showDisabledButton: {
type: Boolean,
required: false,
default: false,
},
computed: {
isLoading() {
return this.status === 'loading';
},
statusObj() {
return {
group: this.status,
icon: `status_${this.status}`,
};
},
},
computed: {
isLoading() {
return this.status === 'loading';
},
};
statusObj() {
return {
group: this.status,
icon: `status_${this.status}`,
};
},
},
};
</script>
<template>
<div class="space-children d-flex append-right-10 widget-status-icon">
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import { __ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
import { __ } from '../../locale';
export default {
directives: {
tooltip,
},
created() {
this.removesBranchText = __('<strong>Removes</strong> source branch');
this.tooltipTitle = __('A user with write access to the source branch selected this option');
},
};
export default {
directives: {
tooltip,
},
created() {
this.removesBranchText = __('<strong>Removes</strong> source branch');
this.tooltipTitle = __('A user with write access to the source branch selected this option');
},
};
</script>
<template>
......
<script>
import statusIcon from '../mr_widget_status_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
};
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
export default {
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return {
isRefreshing: false,
};
},
methods: {
refreshWidget() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isRefreshing = false;
});
},
data() {
return {
isRefreshing: false,
};
},
methods: {
refreshWidget() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isRefreshing = false;
});
},
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import statusIcon from '../mr_widget_status_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetChecking',
components: {
statusIcon,
},
};
export default {
name: 'MRWidgetChecking',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetClosed',
components: {
MrWidgetAuthorTime,
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
export default {
name: 'MRWidgetClosed',
components: {
MrWidgetAuthorTime,
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
only needs metrics and targetBranch */
mr: {
type: Object,
required: true,
default: () => ({}),
},
mr: {
type: Object,
required: true,
default: () => ({}),
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import statusIcon from '../mr_widget_status_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetConflicts',
components: {
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
export default {
name: 'MRWidgetConflicts',
components: {
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
only needs a few props */
mr: {
type: Object,
required: true,
default: () => ({}),
},
mr: {
type: Object,
required: true,
default: () => ({}),
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
MrWidgetAuthor,
statusIcon,
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
MrWidgetAuthor,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
return !shouldRemoveSourceBranch &&
canRemoveSourceBranch &&
mergeUserId === currentUserId;
},
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
Flash('Something went wrong. Please try again.');
});
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service
.cancelAutomaticMerge()
.then(res => res.data)
.then(data => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
Flash('Something went wrong. Please try again.');
});
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
this.isRemovingSourceBranch = true;
this.service.merge(options)
.then(res => res.data)
.then((data) => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
.catch(() => {
this.isRemovingSourceBranch = false;
Flash('Something went wrong. Please try again.');
});
},
this.isRemovingSourceBranch = true;
this.service
.merge(options)
.then(res => res.data)
.then(data => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
.catch(() => {
this.isRemovingSourceBranch = false;
Flash('Something went wrong. Please try again.');
});
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
directives: {
tooltip,
export default {
name: 'MRWidgetMerged',
directives: {
tooltip,
},
components: {
MrWidgetAuthorTime,
statusIcon,
ClipboardButton,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
components: {
MrWidgetAuthorTime,
statusIcon,
ClipboardButton,
service: {
type: Object,
required: true,
default: () => ({}),
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
},
data() {
return {
isMakingRequest: false,
};
},
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
return (
!sourceBranchRemoved &&
canRemoveSourceBranch &&
!this.isMakingRequest &&
!isRemovingSourceBranch
);
},
data() {
return {
isMakingRequest: false,
};
shouldShowSourceBranchRemoving() {
const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr;
return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest);
},
computed: {
shouldShowRemoveSourceBranch() {
const {
sourceBranchRemoved,
isRemovingSourceBranch,
canRemoveSourceBranch,
} = this.mr;
return !sourceBranchRemoved &&
canRemoveSourceBranch &&
!this.isMakingRequest &&
!isRemovingSourceBranch;
},
shouldShowSourceBranchRemoving() {
const {
sourceBranchRemoved,
isRemovingSourceBranch,
} = this.mr;
return !sourceBranchRemoved &&
(isRemovingSourceBranch || this.isMakingRequest);
},
shouldShowMergedButtons() {
const {
canRevertInCurrentMR,
canCherryPickInCurrentMR,
revertInForkPath,
cherryPickInForkPath,
} = this.mr;
shouldShowMergedButtons() {
const {
canRevertInCurrentMR,
canCherryPickInCurrentMR,
revertInForkPath,
cherryPickInForkPath,
} = this.mr;
return canRevertInCurrentMR ||
canCherryPickInCurrentMR ||
revertInForkPath ||
cherryPickInForkPath;
},
revertTitle() {
return s__('mrWidget|Revert this merge request in a new merge request');
},
cherryPickTitle() {
return s__('mrWidget|Cherry-pick this merge request in a new merge request');
},
revertLabel() {
return s__('mrWidget|Revert');
},
cherryPickLabel() {
return s__('mrWidget|Cherry-pick');
},
return (
canRevertInCurrentMR || canCherryPickInCurrentMR || revertInForkPath || cherryPickInForkPath
);
},
revertTitle() {
return s__('mrWidget|Revert this merge request in a new merge request');
},
cherryPickTitle() {
return s__('mrWidget|Cherry-pick this merge request in a new merge request');
},
revertLabel() {
return s__('mrWidget|Revert');
},
cherryPickLabel() {
return s__('mrWidget|Cherry-pick');
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
Flash(__('Something went wrong. Please try again.'));
});
},
this.service
.removeSourceBranch()
.then(res => res.data)
.then(data => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
Flash(__('Something went wrong. Please try again.'));
});
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import statusIcon from '../mr_widget_status_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMerging',
components: {
statusIcon,
export default {
name: 'MRWidgetMerging',
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
},
};
},
};
</script>
<template>
<div class="mr-widget-body mr-state-locked media">
......
<script>
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
directives: {
tooltip,
export default {
name: 'MRWidgetMissingBranch',
directives: {
tooltip,
},
components: {
mrWidgetMergeHelp,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
components: {
mrWidgetMergeHelp,
statusIcon,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
missingBranchNameMessage() {
return sprintf(s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), {
missingBranchNameMessage() {
return sprintf(
s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'),
{
missingBranchName: this.missingBranchName,
});
},
message() {
return sprintf(s__('mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line'), {
},
);
},
message() {
return sprintf(
s__(
'mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line',
),
{
missingBranchName: this.missingBranchName,
});
},
},
);
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
StatusIcon,
},
};
export default {
name: 'MRWidgetNotAllowed',
components: {
StatusIcon,
},
};
</script>
<template>
......
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
StatusIcon,
},
};
export default {
name: 'MRWidgetPipelineBlocked',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
......
<script>
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import Flash from '../../../flash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import Flash from '../../../flash';
export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
data() {
return {
isMakingRequest: false,
rebasingError: null,
};
},
data() {
return {
isMakingRequest: false,
rebasingError: null,
};
},
computed: {
status() {
if (this.mr.rebaseInProgress || this.isMakingRequest) {
return 'loading';
}
if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) {
return 'warning';
}
return 'success';
},
computed: {
status() {
if (this.mr.rebaseInProgress || this.isMakingRequest) {
return 'loading';
}
if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) {
return 'warning';
}
return 'success';
},
showDisabledButton() {
return ['failed', 'loading'].includes(this.status);
},
showDisabledButton() {
return ['failed', 'loading'].includes(this.status);
},
methods: {
rebase() {
this.isMakingRequest = true;
this.rebasingError = null;
},
methods: {
rebase() {
this.isMakingRequest = true;
this.rebasingError = null;
this.service.rebase()
.then(() => {
simplePoll(this.checkRebaseStatus);
})
.catch((error) => {
this.rebasingError = error.merge_error;
this.service
.rebase()
.then(() => {
simplePoll(this.checkRebaseStatus);
})
.catch(error => {
this.rebasingError = error.merge_error;
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
});
},
checkRebaseStatus(continuePolling, stopPolling) {
this.service
.poll()
.then(res => res.data)
.then(res => {
if (res.rebase_in_progress) {
continuePolling();
} else {
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
});
},
checkRebaseStatus(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.data)
.then((res) => {
if (res.rebase_in_progress) {
continuePolling();
} else {
this.isMakingRequest = false;
if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error;
Flash('Something went wrong. Please try again.');
}
eventHub.$emit('MRWidgetUpdateRequested');
stopPolling();
if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error;
Flash('Something went wrong. Please try again.');
}
})
.catch(() => {
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
eventHub.$emit('MRWidgetUpdateRequested');
stopPolling();
});
},
}
})
.catch(() => {
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
stopPolling();
});
},
};
},
};
</script>
<template>
<div class="mr-widget-body media">
......
......@@ -24,9 +24,10 @@ export default {
methods: {
removeWIP() {
this.isMakingRequest = true;
this.service.removeWIP()
this.service
.removeWIP()
.then(res => res.data)
.then((data) => {
.then(data => {
eventHub.$emit('UpdateWidgetData', data);
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
......
<script>
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
......@@ -100,8 +99,11 @@ export default {
return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
},
shouldRenderSourceBranchRemovalStatus() {
return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
return (
!this.mr.canRemoveSourceBranch &&
this.mr.shouldRemoveSourceBranch &&
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
},
created() {
......@@ -134,9 +136,10 @@ export default {
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
return this.service.checkStatus()
return this.service
.checkStatus()
.then(res => res.data)
.then((data) => {
.then(data => {
this.handleNotification(data);
this.mr.setData(data);
this.setFaviconHelper();
......@@ -173,20 +176,24 @@ export default {
return Promise.resolve();
},
fetchDeployments() {
return this.service.fetchDeployments()
return this.service
.fetchDeployments()
.then(res => res.data)
.then((data) => {
.then(data => {
if (data.length) {
this.mr.deployments = data;
}
})
.catch(() => {
createFlash('Something went wrong while fetching the environments for this merge request. Please try again.');
createFlash(
'Something went wrong while fetching the environments for this merge request. Please try again.',
);
});
},
fetchActionsContent() {
this.service.fetchMergeActionsContent()
.then((res) => {
this.service
.fetchMergeActionsContent()
.then(res => {
if (res.data) {
const el = document.createElement('div');
el.innerHTML = res.data;
......@@ -213,22 +220,22 @@ export default {
this.pollingInterval.stopTimer();
},
bindEventHubListeners() {
eventHub.$on('MRWidgetUpdateRequested', (cb) => {
eventHub.$on('MRWidgetUpdateRequested', cb => {
this.checkStatus(cb);
});
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', (params) => {
eventHub.$on('SetBranchRemoveFlag', params => {
[this.mr.isRemovingSourceBranch] = params;
});
eventHub.$on('FailedToMerge', (mergeError) => {
eventHub.$on('FailedToMerge', mergeError => {
this.mr.state = 'failedToMerge';
this.mr.mergeError = mergeError;
});
eventHub.$on('UpdateWidgetData', (data) => {
eventHub.$on('UpdateWidgetData', data => {
this.mr.setData(data);
});
......
......@@ -118,7 +118,9 @@ export default {
this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
const yCoord = this.vbHeight / 2 + rectWidth - 5;
return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`;
return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${
this.yAxisTextRotation
})`;
},
},
mounted() {
......@@ -207,8 +209,7 @@ export default {
renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) {
if (i > 0) {
d3
.select(this)
d3.select(this)
.select('line')
.attr('x2', width)
.attr('class', 'axis-tick');
......@@ -217,8 +218,7 @@ export default {
// Add the panning capabilities
if (this.isPanAvailable) {
d3
.select(this.$refs.baseSvg)
d3.select(this.$refs.baseSvg)
.call(this.zoom)
.on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel
}
......
<script>
/* eslint-disable vue/require-default-prop */
export default {
name: 'DeprecatedModal', // use GlModal instead
/* eslint-disable vue/require-default-prop */
export default {
name: 'DeprecatedModal', // use GlModal instead
props: {
id: {
type: String,
required: false,
},
title: {
type: String,
required: false,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: {
type: String,
required: false,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
required: false,
default: '',
},
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
props: {
id: {
type: String,
required: false,
},
title: {
type: String,
required: false,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: {
type: String,
required: false,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
required: false,
default: '',
},
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
btnKindClass() {
return {
[`btn-${this.kind}`]: true,
};
},
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
computed: {
btnKindClass() {
return {
[`btn-${this.kind}`]: true,
};
},
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
};
},
};
</script>
<template>
......
......@@ -583,7 +583,5 @@ const fileNameIcons = {
};
export default function getIconForFile(name) {
return fileNameIcons[name] ||
fileExtensionIcons[name ? name.split('.').pop() : ''] ||
'';
return fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop() : ''] || '';
}
<script>
// only allow classes in images.scss e.g. s12
const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
export default {
components: {
icon,
},
props: {
isLocked: {
type: Boolean,
default: false,
required: false,
},
props: {
isLocked: {
type: Boolean,
default: false,
required: false,
},
isConfidential: {
type: Boolean,
default: false,
required: false,
},
isConfidential: {
type: Boolean,
default: false,
required: false,
},
computed: {
warningIcon() {
if (this.isConfidential) return 'eye-slash';
if (this.isLocked) return 'lock';
},
computed: {
warningIcon() {
if (this.isConfidential) return 'eye-slash';
if (this.isLocked) return 'lock';
return '';
},
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
return '';
},
};
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
},
};
</script>
<template>
<div class="issuable-note-warning">
......
<script>
/* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button
/* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option.
This can also be used for initial page load when you don't
......@@ -17,34 +17,34 @@
*/
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
},
containerClass: {
type: [String, Array, Object],
required: false,
default: 'btn btn-align-content',
},
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
methods: {
onClick(e) {
this.$emit('click', e);
},
disabled: {
type: Boolean,
required: false,
default: false,
},
};
label: {
type: String,
required: false,
},
containerClass: {
type: [String, Array, Object],
required: false,
default: 'btn btn-align-content',
},
},
methods: {
onClick(e) {
this.$emit('click', e);
},
},
};
</script>
<template>
......
<script>
import $ from 'jquery';
import { s__ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
import $ from 'jquery';
import { s__ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
export default {
components: {
markdownHeader,
markdownToolbar,
icon,
export default {
components: {
markdownHeader,
markdownToolbar,
icon,
},
props: {
markdownPreviewPath: {
type: String,
required: false,
default: '',
},
props: {
markdownPreviewPath: {
type: String,
required: false,
default: '',
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
addSpacingClasses: {
type: Boolean,
required: false,
default: true,
},
quickActionsDocsPath: {
type: String,
required: false,
default: '',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
markdownDocsPath: {
type: String,
required: true,
},
data() {
return {
markdownPreview: '',
referencedCommands: '',
referencedUsers: '',
markdownPreviewLoading: false,
previewMarkdown: false,
};
markdownVersion: {
type: Number,
required: false,
default: 0,
},
computed: {
shouldShowReferencedUsers() {
const referencedUsersThreshold = 10;
return this.referencedUsers.length >= referencedUsersThreshold;
},
addSpacingClasses: {
type: Boolean,
required: false,
default: true,
},
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
snippets: this.enableAutocomplete,
});
quickActionsDocsPath: {
type: String,
required: false,
default: '',
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
if (glForm) {
glForm.destroy();
}
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
methods: {
showPreviewTab() {
if (this.previewMarkdown) return;
},
data() {
return {
markdownPreview: '',
referencedCommands: '',
referencedUsers: '',
markdownPreviewLoading: false,
previewMarkdown: false,
};
},
computed: {
shouldShowReferencedUsers() {
const referencedUsersThreshold = 10;
return this.referencedUsers.length >= referencedUsersThreshold;
},
},
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
snippets: this.enableAutocomplete,
});
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
if (glForm) {
glForm.destroy();
}
},
methods: {
showPreviewTab() {
if (this.previewMarkdown) return;
this.previewMarkdown = true;
this.previewMarkdown = true;
/*
/*
Can't use `$refs` as the component is technically in the parent component
so we access the VNode & then get the element
*/
const text = this.$slots.textarea[0].elm.value;
const text = this.$slots.textarea[0].elm.value;
if (text) {
this.markdownPreviewLoading = true;
this.$http
.post(this.versionedPreviewPath(), { text })
.then(resp => resp.json())
.then(data => this.renderMarkdown(data))
.catch(() => new Flash(s__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
},
if (text) {
this.markdownPreviewLoading = true;
this.$http
.post(this.versionedPreviewPath(), { text })
.then(resp => resp.json())
.then(data => this.renderMarkdown(data))
.catch(() => new Flash(s__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
},
showWriteTab() {
this.markdownPreview = '';
this.previewMarkdown = false;
},
showWriteTab() {
this.markdownPreview = '';
this.previewMarkdown = false;
},
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
if (data.references) {
this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users;
}
if (data.references) {
this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users;
}
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
},
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
},
versionedPreviewPath() {
const { markdownPreviewPath, markdownVersion } = this;
return `${markdownPreviewPath}${
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
}markdown_version=${markdownVersion}`;
},
versionedPreviewPath() {
const { markdownPreviewPath, markdownVersion } = this;
return `${markdownPreviewPath}${
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
}markdown_version=${markdownVersion}`;
},
};
},
};
</script>
<template>
......
<script>
import $ from 'jquery';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
import icon from '../icon.vue';
import $ from 'jquery';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
import icon from '../icon.vue';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
toolbarButton,
icon,
},
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
components: {
toolbarButton,
icon,
},
computed: {
mdTable() {
return [
'| header | header |',
'| ------ | ------ |',
'| cell | cell |',
'| cell | cell |',
].join('\n');
},
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
isValid(form) {
return (
!form ||
(form.find('.js-vue-markdown-field').length && $(this.$el).closest('form')[0] === form[0])
);
},
computed: {
mdTable() {
return [
'| header | header |',
'| ------ | ------ |',
'| cell | cell |',
'| cell | cell |',
].join('\n');
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
isValid(form) {
return !form ||
form.find('.js-vue-markdown-field').length &&
$(this.$el).closest('form')[0] === form[0];
},
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (!this.isValid(form)) return;
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (!this.isValid(form)) return;
this.$emit('preview-markdown');
},
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (!this.isValid(form)) return;
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (!this.isValid(form)) return;
this.$emit('write-markdown');
},
this.$emit('write-markdown');
},
};
},
};
</script>
<template>
......
<script>
import { Link } from '@gitlab-org/gitlab-ui';
import { Link } from '@gitlab-org/gitlab-ui';
export default {
components: {
'gl-link': Link,
export default {
components: {
'gl-link': Link,
},
props: {
markdownDocsPath: {
type: String,
required: true,
},
props: {
markdownDocsPath: {
type: String,
required: true,
},
quickActionsDocsPath: {
type: String,
required: false,
default: '',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
quickActionsDocsPath: {
type: String,
required: false,
default: '',
},
computed: {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
};
},
computed: {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
},
};
</script>
<template>
......
<script>
import tooltip from '../../directives/tooltip';
import icon from '../icon.vue';
import tooltip from '../../directives/tooltip';
import icon from '../icon.vue';
export default {
components: {
icon,
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
buttonTitle: {
type: String,
required: true,
},
directives: {
tooltip,
icon: {
type: String,
required: true,
},
props: {
buttonTitle: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
tag: {
type: String,
required: true,
},
tagBlock: {
type: String,
required: false,
default: '',
},
tagSelect: {
type: String,
required: false,
default: '',
},
prepend: {
type: Boolean,
required: false,
default: false,
},
tag: {
type: String,
required: true,
},
};
tagBlock: {
type: String,
required: false,
default: '',
},
tagSelect: {
type: String,
required: false,
default: '',
},
prepend: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
......
......@@ -41,7 +41,8 @@ export default {
// Find metric timestamp which is closest to deploymentTime
timestampDiff = Math.abs(metricTimestamps[0] - median);
metricTimestamps.forEach((timestamp, index) => {
if (index === 0) { // Skip first element
if (index === 0) {
// Skip first element
return;
}
......
<script>
/**
* Common component to render a placeholder note and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `getUserData` getter that contains
* {
* path: String,
* avatar_url: String,
* name: String,
* username: String,
* }
*
* @example
* <placeholder-note
* :note="{body: 'This is a note'}"
* />
*/
import { mapGetters } from 'vuex';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
/**
* Common component to render a placeholder note and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `getUserData` getter that contains
* {
* path: String,
* avatar_url: String,
* name: String,
* username: String,
* }
*
* @example
* <placeholder-note
* :note="{body: 'This is a note'}"
* />
*/
import { mapGetters } from 'vuex';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
components: {
userAvatarLink,
export default {
name: 'PlaceholderNote',
components: {
userAvatarLink,
},
props: {
note: {
type: Object,
required: true,
},
props: {
note: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'getUserData',
]),
},
};
},
computed: {
...mapGetters(['getUserData']),
},
};
</script>
<template>
......
<script>
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default {
name: 'PlaceholderSystemNote',
props: {
note: {
type: Object,
required: true,
},
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default {
name: 'PlaceholderSystemNote',
props: {
note: {
type: Object,
required: true,
},
};
},
};
</script>
<template>
......
<script>
export default {
props: {
startSize: {
type: Number,
required: true,
},
side: {
type: String,
required: true,
},
minSize: {
type: Number,
required: false,
default: 0,
},
maxSize: {
type: Number,
required: false,
default: Number.MAX_VALUE,
},
enabled: {
type: Boolean,
required: false,
default: true,
},
export default {
props: {
startSize: {
type: Number,
required: true,
},
data() {
return {
size: this.startSize,
};
side: {
type: String,
required: true,
},
computed: {
className() {
return `drag-${this.side}`;
},
cursorStyle() {
if (this.enabled) {
return { cursor: 'ew-resize' };
}
return {};
},
minSize: {
type: Number,
required: false,
default: 0,
},
methods: {
resetSize(e) {
e.preventDefault();
this.$emit('resize-start', this.size);
maxSize: {
type: Number,
required: false,
default: Number.MAX_VALUE,
},
enabled: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
size: this.startSize,
};
},
computed: {
className() {
return `drag-${this.side}`;
},
cursorStyle() {
if (this.enabled) {
return { cursor: 'ew-resize' };
}
return {};
},
},
methods: {
resetSize(e) {
e.preventDefault();
this.$emit('resize-start', this.size);
this.size = this.startSize;
this.$emit('update:size', this.size);
this.size = this.startSize;
this.$emit('update:size', this.size);
// End resizing on next tick so that listeners can react to DOM changes
this.$nextTick(() => {
this.$emit('resize-end', this.size);
});
},
startDrag(e) {
if (this.enabled) {
e.preventDefault();
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.endDrag, { once: true });
this.$emit('resize-start', this.size);
}
},
drag(e) {
// End resizing on next tick so that listeners can react to DOM changes
this.$nextTick(() => {
this.$emit('resize-end', this.size);
});
},
startDrag(e) {
if (this.enabled) {
e.preventDefault();
let moved = e.clientX - this.startPos;
if (this.side === 'left') moved = -moved;
let newSize = this.currentStartSize + moved;
if (newSize < this.minSize) {
newSize = this.minSize;
} else if (newSize > this.maxSize) {
newSize = this.maxSize;
}
this.size = newSize;
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.endDrag, { once: true });
this.$emit('resize-start', this.size);
}
},
drag(e) {
e.preventDefault();
let moved = e.clientX - this.startPos;
if (this.side === 'left') moved = -moved;
let newSize = this.currentStartSize + moved;
if (newSize < this.minSize) {
newSize = this.minSize;
} else if (newSize > this.maxSize) {
newSize = this.maxSize;
}
this.size = newSize;
this.$emit('update:size', newSize);
},
endDrag(e) {
e.preventDefault();
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
this.$emit('update:size', newSize);
},
endDrag(e) {
e.preventDefault();
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
};
},
};
</script>
<template>
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
export default {
name: 'DatePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
export default {
name: 'DatePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
beforeDestroy() {
this.calendar.destroy();
maxDate: {
type: Date,
required: false,
default: null,
},
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
},
beforeDestroy() {
this.calendar.destroy();
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
toggled() {
this.$emit('hidePicker');
},
toggled() {
this.$emit('hidePicker');
},
};
},
};
</script>
<template>
......
<script>
/* This is a re-usable vue component for rendering a project avatar that
/* This is a re-usable vue component for rendering a project avatar that
does not need to link to the project's profile. The image and an optional
tooltip can be configured by props passed to this component.
......@@ -16,70 +15,70 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
export default {
name: 'ProjectAvatarImage',
directives: {
tooltip,
export default {
name: 'ProjectAvatarImage',
directives: {
tooltip,
},
props: {
lazy: {
type: Boolean,
required: false,
default: false,
},
imgSrc: {
type: String,
required: false,
default: defaultAvatarUrl,
},
cssClasses: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: 'project avatar',
},
size: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
computed: {
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside project avatar link.
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
props: {
lazy: {
type: Boolean,
required: false,
default: false,
},
imgSrc: {
type: String,
required: false,
default: defaultAvatarUrl,
},
cssClasses: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: 'project avatar',
},
size: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
computed: {
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside project avatar link.
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
avatarSizeClass() {
return `s${this.size}`;
},
};
},
};
</script>
<template>
......
<script>
import DeprecatedModal from './deprecated_modal.vue';
import DeprecatedModal from './deprecated_modal.vue';
export default {
name: 'RecaptchaModal',
export default {
name: 'RecaptchaModal',
components: {
DeprecatedModal,
},
components: {
DeprecatedModal,
},
props: {
html: {
type: String,
required: false,
default: '',
},
props: {
html: {
type: String,
required: false,
default: '',
},
},
data() {
return {
script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js',
};
},
data() {
return {
script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js',
};
},
watch: {
html() {
this.appendRecaptchaScript();
},
watch: {
html() {
this.appendRecaptchaScript();
},
},
mounted() {
window.recaptchaDialogCallback = this.submit.bind(this);
},
mounted() {
window.recaptchaDialogCallback = this.submit.bind(this);
},
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
this.script = script;
this.script = script;
document.body.appendChild(script);
},
document.body.appendChild(script);
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
submit() {
this.$el.querySelector('form').submit();
},
submit() {
this.$el.querySelector('form').submit();
},
};
},
};
</script>
<template>
......
<script>
import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
export default {
name: 'SidebarDatePicker',
components: {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
},
data() {
return {
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
export default {
name: 'SidebarDatePicker',
components: {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
},
data() {
return {
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
......
<script>
import { s__ } from '../../locale';
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = s__('Pagination|Prev');
const NEXT = s__('Pagination|Next');
const FIRST = s__('Pagination|« First');
const LAST = s__('Pagination|Last »');
export default {
props: {
/**
import { s__ } from '../../locale';
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = s__('Pagination|Prev');
const NEXT = s__('Pagination|Next');
const FIRST = s__('Pagination|« First');
const LAST = s__('Pagination|Last »');
export default {
props: {
/**
This function will take the information given by the pagination component
Here is an example `change` method:
......@@ -20,12 +20,12 @@
gl.utils.visitUrl(`?page=${pagenum}`);
},
*/
change: {
type: Function,
required: true,
},
change: {
type: Function,
required: true,
},
/**
/**
pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component
......@@ -41,94 +41,94 @@
previousPage: +headers['X-Prev-Page'],
});
*/
pageInfo: {
type: Object,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
},
computed: {
prev() {
return this.pageInfo.previousPage;
},
next() {
return this.pageInfo.nextPage;
},
getItems() {
const total = this.pageInfo.totalPages;
const { page } = this.pageInfo;
const items = [];
if (page > 1) {
items.push({ title: FIRST, first: true });
}
if (page > 1) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
}
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i += 1) {
const isActive = i === page;
items.push({ title: i, active: isActive, page: true });
}
if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true });
}
if (page === total) {
items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) {
items.push({ title: NEXT, next: true });
}
if (total - page >= 1) {
items.push({ title: LAST, last: true });
}
return items;
},
showPagination() {
return this.pageInfo.totalPages > 1;
},
computed: {
prev() {
return this.pageInfo.previousPage;
},
next() {
return this.pageInfo.nextPage;
},
getItems() {
const total = this.pageInfo.totalPages;
const { page } = this.pageInfo;
const items = [];
if (page > 1) {
items.push({ title: FIRST, first: true });
}
if (page > 1) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
}
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i += 1) {
const isActive = i === page;
items.push({ title: i, active: isActive, page: true });
}
if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true });
}
if (page === total) {
items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) {
items.push({ title: NEXT, next: true });
}
if (total - page >= 1) {
items.push({ title: LAST, last: true });
}
return items;
},
showPagination() {
return this.pageInfo.totalPages > 1;
},
},
methods: {
changePage(text, isDisabled) {
if (isDisabled) return;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD:
break;
case LAST:
this.change(totalPages);
break;
case NEXT:
this.change(nextPage);
break;
case PREV:
this.change(previousPage);
break;
case FIRST:
this.change(1);
break;
default:
this.change(+text);
break;
}
},
methods: {
changePage(text, isDisabled) {
if (isDisabled) return;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD:
break;
case LAST:
this.change(totalPages);
break;
case NEXT:
this.change(nextPage);
break;
case PREV:
this.change(previousPage);
break;
case FIRST:
this.change(1);
break;
default:
this.change(+text);
break;
}
},
hideOnSmallScreen(item) {
return !item.first && !item.last && !item.next && !item.prev && !item.active;
},
hideOnSmallScreen(item) {
return !item.first && !item.last && !item.next && !item.prev && !item.active;
},
};
},
};
</script>
<template>
<div
......
......@@ -11,9 +11,7 @@ export default {
directives: {
tooltip,
},
mixins: [
timeagoMixin,
],
mixins: [timeagoMixin],
props: {
time: {
type: String,
......
<script>
import { s__ } from '../../locale';
import icon from './icon.vue';
import { s__ } from '../../locale';
import icon from './icon.vue';
const ICON_ON = 'status_success_borderless';
const ICON_OFF = 'status_failed_borderless';
const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
const ICON_ON = 'status_success_borderless';
const ICON_OFF = 'status_failed_borderless';
const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
icon,
},
export default {
components: {
icon,
},
model: {
prop: 'value',
event: 'change',
},
model: {
prop: 'value',
event: 'change',
},
props: {
name: {
type: String,
required: false,
default: null,
},
value: {
type: Boolean,
required: false,
default: null,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
props: {
name: {
type: String,
required: false,
default: null,
},
value: {
type: Boolean,
required: false,
default: null,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
toggleIcon() {
return this.value ? ICON_ON : ICON_OFF;
},
ariaLabel() {
return this.value ? LABEL_ON : LABEL_OFF;
},
computed: {
toggleIcon() {
return this.value ? ICON_ON : ICON_OFF;
},
ariaLabel() {
return this.value ? LABEL_ON : LABEL_OFF;
},
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
};
},
};
</script>
<template>
......
<script>
/* This is a re-usable vue component for rendering a user avatar wrapped in
a clickable link (likely to the user's profile). The link, image, and
tooltip can be configured by props passed to this component.
......
<script>
/* This is a re-usable vue component for rendering a user avatar svg (typically
for a blank state). It will receive styles comparable to the user avatar,
but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported.
......@@ -42,4 +41,3 @@ export default {
v-html="svg"
/>
</template>
......@@ -4,10 +4,7 @@
*
* Components need to have `scope`, `page` and `requestData`
*/
import {
historyPushState,
buildUrlWithCurrentLocation,
} from '../../lib/utils/common_utils';
import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
export default {
methods: {
......@@ -24,12 +21,14 @@ export default {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
const queryString = Object.keys(parameters)
.map(parameter => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
})
.join('&');
// update polling parameters
this.requestData = parameters;
......
......@@ -6,7 +6,7 @@ export default class ListLabel {
this.color = obj.color;
this.textColor = obj.text_color;
this.description = obj.description;
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
this.priority = obj.priority !== null ? obj.priority : Infinity;
}
}
......
import {
__,
n__,
s__,
sprintf,
} from '../locale';
import { __, n__, s__, sprintf } from '../locale';
export default (Vue) => {
export default Vue => {
Vue.mixin({
methods: {
/**
......
......@@ -21,7 +21,7 @@ Vue.http.interceptors.push((request, next) => {
Vue.http.interceptors.push((request, next) => {
request.headers.set(csrf.headerKey, csrf.token);
next((response) => {
next(response => {
// Headers object has a `forEach` property that iterates through all values.
const headers = {};
......
......@@ -8,8 +8,8 @@
= render partial: 'flash_messages', locals: { project: @project }
- if @project.repository_exists? && !@project.empty_repo?
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @project.default_branch)
- if !@project.empty_repo? && can?(current_user, :download_code, @project)
- signatures_path = project_signatures_path(@project, @project.default_branch)
.js-signature-container{ data: { 'signatures-path': signatures_path } }
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
......
---
title: Fix commit signature error when project is disabled
merge_request: 22344
author:
type: fixed
---
title: Fix auto-corrected upload URLs in webhooks
merge_request: 22361
author:
type: fixed
......@@ -109,7 +109,7 @@ It is responsibility of the author of a merge request that the merge request is
### List of merge requests ready for review
Developers who have capacity can regularly check the list of [merge requests to review](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&label_name%5B%5D=ready%20for%20review&assignee_id=0) and assign any merge request they want to review.
Developers who have capacity can regularly check the list of [merge requests to review](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&label_name%5B%5D=ready%20for%20review) and assign any merge request they want to review.
### Reviewing code
......
......@@ -7,7 +7,7 @@
> - the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
>
> **Note:**
> Starting from GitLab 11.1, the logs of web hooks are automatically removed after
> Starting from GitLab 11.1, the logs of webhooks are automatically removed after
> one month.
>
> **Note:**
......@@ -98,8 +98,8 @@ Below are described the supported events.
Triggered when you push to the repository except when pushing tags.
> **Note:** When more than 20 commits are pushed at once, the `commits` web hook
attribute will only contain the first 20 for performance reasons. Loading
> **Note:** When more than 20 commits are pushed at once, the `commits` webhook
attribute will only contain the first 20 for performance reasons. Loading
detailed commit data is expensive. Note that despite only 20 commits being
present in the `commits` attribute, the `total_commits_count` attribute will
contain the actual total.
......@@ -1182,10 +1182,11 @@ its description:
```
It will appear in the webhook body as the below (assuming that GitLab is
installed at gitlab.example.com):
installed at gitlab.example.com, and the project is at
example-group/example-project):
```markdown
![image](https://gitlab.example.com/uploads/$sha/image.png)
![image](https://gitlab.example.com/example-group/example-project/uploads/$sha/image.png)
```
This will not rewrite URLs that already are pointing to HTTP, HTTPS, or
......@@ -1215,7 +1216,7 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
> **Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address.
### Receiving duplicate or multiple web hook requests triggered by one event
### Receiving duplicate or multiple webhook requests triggered by one event
When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook.
If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
......
......@@ -25,6 +25,7 @@ module Gitlab
markdown_text.gsub(MARKDOWN_SIMPLE_IMAGE) do
if $~[:image]
url = $~[:url]
url = "#{uploads_prefix}#{url}" if url.start_with?('/uploads')
url = "/#{url}" unless url.start_with?('/')
"![#{$~[:title]}](#{Gitlab.config.gitlab.url}#{url})"
......@@ -33,6 +34,16 @@ module Gitlab
end
end
end
def uploads_prefix
project&.full_path || ''
end
def project
return unless object.respond_to?(:project)
object.project
end
end
end
end
......@@ -193,6 +193,23 @@ describe 'Project' do
end
end
describe 'when the project repository is disabled', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository_disabled, :repository, namespace: user.namespace) }
before do
sign_in(user)
project.add_maintainer(user)
visit project_path(project)
end
it 'does not show an error' do
wait_for_requests
expect(page).not_to have_selector('.flash-alert')
end
end
describe 'removal', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
......
......@@ -8,57 +8,94 @@ describe Gitlab::HookData::BaseBuilder do
end
end
subject { subclass.new(nil) }
using RSpec::Parameterized::TableSyntax
where do
{
'relative image URL' => {
input: '![an image](foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/foo.png)"
},
'HTTP URL' => {
input: '![an image](http://example.com/foo.png)',
output: '![an image](http://example.com/foo.png)'
},
'HTTPS URL' => {
input: '![an image](https://example.com/foo.png)',
output: '![an image](https://example.com/foo.png)'
},
'protocol-relative URL' => {
input: '![an image](//example.com/foo.png)',
output: '![an image](//example.com/foo.png)'
},
'URL reference by title' => {
input: "![foo]\n\n[foo]: foo.png",
output: "![foo]\n\n[foo]: foo.png"
},
'URL reference by label' => {
input: "![][foo]\n\n[foo]: foo.png",
output: "![][foo]\n\n[foo]: foo.png"
},
'in Markdown inline code block' => {
input: '`![an image](foo.png)`',
output: "`![an image](#{Gitlab.config.gitlab.url}/foo.png)`"
},
'in HTML tag on the same line' => {
input: '<p>![an image](foo.png)</p>',
output: "<p>![an image](#{Gitlab.config.gitlab.url}/foo.png)</p>"
},
'in Markdown multi-line code block' => {
input: "```\n![an image](foo.png)\n```",
output: "```\n![an image](foo.png)\n```"
},
'in HTML tag on different lines' => {
input: "<p>\n![an image](foo.png)\n</p>",
output: "<p>\n![an image](foo.png)\n</p>"
context 'with an upload prefix specified' do
let(:project_with_path) { double(full_path: 'baz/bar') }
let(:object_with_project) { double(project: project_with_path) }
subject { subclass.new(object_with_project) }
where do
{
'relative image URL' => {
input: '![an image](foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/foo.png)"
},
'absolute upload URL' => {
input: '![an image](/uploads/foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/baz/bar/uploads/foo.png)"
},
'absolute non-upload URL' => {
input: '![an image](/downloads/foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/downloads/foo.png)"
}
}
}
end
with_them do
it { expect(subject.absolute_image_urls(input)).to eq(output) }
end
end
with_them do
it { expect(subject.absolute_image_urls(input)).to eq(output) }
context 'without an upload prefix specified' do
subject { subclass.new(nil) }
where do
{
'relative image URL' => {
input: '![an image](foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/foo.png)"
},
'absolute upload URL' => {
input: '![an image](/uploads/foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/uploads/foo.png)"
},
'absolute non-upload URL' => {
input: '![an image](/downloads/foo.png)',
output: "![an image](#{Gitlab.config.gitlab.url}/downloads/foo.png)"
},
'HTTP URL' => {
input: '![an image](http://example.com/foo.png)',
output: '![an image](http://example.com/foo.png)'
},
'HTTPS URL' => {
input: '![an image](https://example.com/foo.png)',
output: '![an image](https://example.com/foo.png)'
},
'protocol-relative URL' => {
input: '![an image](//example.com/foo.png)',
output: '![an image](//example.com/foo.png)'
},
'URL reference by title' => {
input: "![foo]\n\n[foo]: foo.png",
output: "![foo]\n\n[foo]: foo.png"
},
'URL reference by label' => {
input: "![][foo]\n\n[foo]: foo.png",
output: "![][foo]\n\n[foo]: foo.png"
},
'in Markdown inline code block' => {
input: '`![an image](foo.png)`',
output: "`![an image](#{Gitlab.config.gitlab.url}/foo.png)`"
},
'in HTML tag on the same line' => {
input: '<p>![an image](foo.png)</p>',
output: "<p>![an image](#{Gitlab.config.gitlab.url}/foo.png)</p>"
},
'in Markdown multi-line code block' => {
input: "```\n![an image](foo.png)\n```",
output: "```\n![an image](foo.png)\n```"
},
'in HTML tag on different lines' => {
input: "<p>\n![an image](foo.png)\n</p>",
output: "<p>\n![an image](foo.png)\n</p>"
}
}
end
with_them do
it { expect(subject.absolute_image_urls(input)).to eq(output) }
end
end
end
end
......@@ -46,7 +46,10 @@ describe Gitlab::HookData::IssueBuilder do
let(:builder) { described_class.new(issue_with_description) }
it 'sets the image to use an absolute URL' do
expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)")
expected_path = "#{issue_with_description.project.path_with_namespace}/uploads/abc/Issue_Image.png"
expect(data[:description])
.to eq("test![Issue_Image](#{Settings.gitlab.url}/#{expected_path})")
end
end
end
......
......@@ -58,11 +58,14 @@ describe Gitlab::HookData::MergeRequestBuilder do
end
context 'when the MR has an image in the description' do
let(:mr_with_description) { create(:merge_request, description: 'test![Issue_Image](/uploads/abc/Issue_Image.png)') }
let(:mr_with_description) { create(:merge_request, description: 'test![MR_Image](/uploads/abc/MR_Image.png)') }
let(:builder) { described_class.new(mr_with_description) }
it 'sets the image to use an absolute URL' do
expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)")
expected_path = "#{mr_with_description.project.path_with_namespace}/uploads/abc/MR_Image.png"
expect(data[:description])
.to eq("test![MR_Image](#{Settings.gitlab.url}/#{expected_path})")
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment