Merge branch 'master' into sh-support-bitbucket-server-import
This commit is contained in:
commit
2d3fd6a142
|
@ -0,0 +1 @@
|
|||
Dangerfile gitlab-language=ruby
|
|
@ -348,6 +348,24 @@ retrieve-tests-metadata:
|
|||
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
|
||||
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
|
||||
|
||||
danger-review:
|
||||
image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest
|
||||
stage: prepare
|
||||
before_script:
|
||||
- source scripts/utils.sh
|
||||
- retry gem install danger --no-ri --no-rdoc
|
||||
cache: {}
|
||||
only:
|
||||
refs:
|
||||
- branches@gitlab-org/gitlab-ce
|
||||
- branches@gitlab-org/gitlab-ee
|
||||
except:
|
||||
variables:
|
||||
- $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
|
||||
script:
|
||||
- git version
|
||||
- danger --fail-on-errors=true
|
||||
|
||||
update-tests-metadata:
|
||||
<<: *tests-metadata-state
|
||||
<<: *only-canonical-masters
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
danger.import_dangerfile(path: 'danger/metadata')
|
||||
danger.import_dangerfile(path: 'danger/changes_size')
|
||||
danger.import_dangerfile(path: 'danger/changelog')
|
||||
danger.import_dangerfile(path: 'danger/specs')
|
||||
danger.import_dangerfile(path: 'danger/gemfile')
|
||||
danger.import_dangerfile(path: 'danger/database')
|
|
@ -1 +1 @@
|
|||
0.111.0
|
||||
0.112.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
7.1.4
|
||||
7.1.5
|
||||
|
|
|
@ -2,13 +2,16 @@ import $ from 'jquery';
|
|||
import { rstrip } from './lib/utils/common_utils';
|
||||
|
||||
function openConfirmDangerModal($form, text) {
|
||||
const $input = $('.js-confirm-danger-input');
|
||||
$input.val('');
|
||||
|
||||
$('.js-confirm-text').text(text || '');
|
||||
$('.js-confirm-danger-input').val('');
|
||||
$('#modal-confirm-danger').modal('show');
|
||||
|
||||
const confirmTextMatch = $('.js-confirm-danger-match').text();
|
||||
const $submit = $('.js-confirm-danger-submit');
|
||||
$submit.disable();
|
||||
$input.focus();
|
||||
|
||||
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
|
||||
const confirmText = rstrip($(this).val());
|
||||
|
|
|
@ -31,9 +31,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isDiscussionsExpanded() {
|
||||
return true; // TODO: @fatihacet - Fix this.
|
||||
},
|
||||
isCollapsed() {
|
||||
return this.file.collapsed || false;
|
||||
},
|
||||
|
@ -131,7 +128,6 @@ export default {
|
|||
:diff-file="file"
|
||||
:collapsible="true"
|
||||
:expanded="!isCollapsed"
|
||||
:discussions-expanded="isDiscussionsExpanded"
|
||||
:add-merge-request-buttons="true"
|
||||
class="js-file-title file-title"
|
||||
@toggleFile="handleToggle"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
@ -38,11 +39,6 @@ export default {
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
discussionsExpanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
currentUser: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
@ -54,6 +50,10 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['diffHasExpandedDiscussions']),
|
||||
hasExpandedDiscussions() {
|
||||
return this.diffHasExpandedDiscussions(this.diffFile);
|
||||
},
|
||||
icon() {
|
||||
if (this.diffFile.submodule) {
|
||||
return 'archive';
|
||||
|
@ -88,9 +88,6 @@ export default {
|
|||
collapseIcon() {
|
||||
return this.expanded ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
isDiscussionsExpanded() {
|
||||
return this.discussionsExpanded && this.expanded;
|
||||
},
|
||||
viewFileButtonText() {
|
||||
const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
|
||||
return sprintf(
|
||||
|
@ -113,7 +110,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
handleToggle(e, checkTarget) {
|
||||
...mapActions('diffs', ['toggleFileDiscussions']),
|
||||
handleToggleFile(e, checkTarget) {
|
||||
if (
|
||||
!checkTarget ||
|
||||
e.target === this.$refs.header ||
|
||||
|
@ -125,6 +123,9 @@ export default {
|
|||
showForkMessage() {
|
||||
this.$emit('showForkMessage');
|
||||
},
|
||||
handleToggleDiscussions() {
|
||||
this.toggleFileDiscussions(this.diffFile);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -133,7 +134,7 @@ export default {
|
|||
<div
|
||||
ref="header"
|
||||
class="js-file-title file-title file-title-flex-parent"
|
||||
@click="handleToggle($event, true)"
|
||||
@click="handleToggleFile($event, true)"
|
||||
>
|
||||
<div class="file-header-content">
|
||||
<icon
|
||||
|
@ -216,10 +217,11 @@ export default {
|
|||
v-if="diffFile.blob && diffFile.blob.readableText"
|
||||
>
|
||||
<button
|
||||
:class="{ active: isDiscussionsExpanded }"
|
||||
:class="{ active: hasExpandedDiscussions }"
|
||||
:title="s__('MergeRequests|Toggle comments for this file')"
|
||||
class="btn js-toggle-diff-comments"
|
||||
class="js-btn-vue-toggle-comments btn"
|
||||
type="button"
|
||||
@click="handleToggleDiscussions"
|
||||
>
|
||||
<icon name="comment" />
|
||||
</button>
|
||||
|
|
|
@ -82,14 +82,32 @@ export const expandAllFiles = ({ commit }) => {
|
|||
commit(types.EXPAND_ALL_FILES);
|
||||
};
|
||||
|
||||
export default {
|
||||
setBaseConfig,
|
||||
fetchDiffFiles,
|
||||
setInlineDiffViewType,
|
||||
setParallelDiffViewType,
|
||||
showCommentForm,
|
||||
cancelCommentForm,
|
||||
loadMoreLines,
|
||||
loadCollapsedDiff,
|
||||
expandAllFiles,
|
||||
/**
|
||||
* Toggles the file discussions after user clicked on the toggle discussions button.
|
||||
*
|
||||
* Gets the discussions for the provided diff.
|
||||
*
|
||||
* If all discussions are expanded, it will collapse all of them
|
||||
* If all discussions are collapsed, it will expand all of them
|
||||
* If some discussions are open and others closed, it will expand the closed ones.
|
||||
*
|
||||
* @param {Object} diff
|
||||
*/
|
||||
export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
|
||||
const discussions = getters.getDiffFileDiscussions(diff);
|
||||
const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff);
|
||||
const shouldExpandAll = getters.diffHasAllCollpasedDiscussions(diff);
|
||||
|
||||
discussions.forEach(discussion => {
|
||||
const data = { discussionId: discussion.id };
|
||||
|
||||
if (shouldCloseAll) {
|
||||
dispatch('collapseDiscussion', data, { root: true });
|
||||
} else if (shouldExpandAll || (!shouldCloseAll && !shouldExpandAll && !discussion.expanded)) {
|
||||
dispatch('expandDiscussion', data, { root: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'underscore';
|
||||
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
|
||||
|
||||
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
|
||||
|
@ -8,5 +9,52 @@ export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.
|
|||
|
||||
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
/**
|
||||
* Checks if the diff has all discussions expanded
|
||||
* @param {Object} diff
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
|
||||
const discussions = getters.getDiffFileDiscussions(diff);
|
||||
|
||||
return (discussions.length && discussions.every(discussion => discussion.expanded)) || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the diff has all discussions collpased
|
||||
* @param {Object} diff
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
|
||||
const discussions = getters.getDiffFileDiscussions(diff);
|
||||
|
||||
return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the diff has any open discussions
|
||||
* @param {Object} diff
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const diffHasExpandedDiscussions = (state, getters) => diff => {
|
||||
const discussions = getters.getDiffFileDiscussions(diff);
|
||||
|
||||
return (
|
||||
(discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array with the discussions of the given diff
|
||||
* @param {Object} diff
|
||||
* @returns {Array}
|
||||
*/
|
||||
export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff =>
|
||||
rootGetters.discussions.filter(
|
||||
discussion =>
|
||||
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
|
||||
) || [];
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
|
||||
export default () => {};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import diffsModule from './modules';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
diffs: diffsModule,
|
||||
},
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import actions from '../actions';
|
||||
import * as actions from '../actions';
|
||||
import * as getters from '../getters';
|
||||
import mutations from '../mutations';
|
||||
import createState from './diff_state';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
@ -33,7 +33,7 @@
|
|||
onClickAction(endpoint) {
|
||||
this.isLoading = true;
|
||||
|
||||
eventHub.$emit('postAction', endpoint);
|
||||
eventHub.$emit('postAction', { endpoint });
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
|
@ -44,7 +44,7 @@
|
|||
return !action.playable;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
|
@ -61,10 +61,7 @@
|
|||
data-toggle="dropdown"
|
||||
>
|
||||
<span>
|
||||
<icon
|
||||
:size="12"
|
||||
name="play"
|
||||
/>
|
||||
<icon name="play" />
|
||||
<i
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true"
|
||||
|
@ -85,10 +82,6 @@
|
|||
class="js-manual-action-link no-btn btn"
|
||||
@click="onClickAction(action.play_path)"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="play"
|
||||
/>
|
||||
<span>
|
||||
{{ action.name }}
|
||||
</span>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import { s__ } from '../../locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import { s__ } from '../../locale';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Renders the external url link in environments table.
|
||||
*/
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
|
@ -21,10 +21,10 @@
|
|||
},
|
||||
computed: {
|
||||
title() {
|
||||
return s__('Environments|Open');
|
||||
return s__('Environments|Open live environment');
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
|
@ -37,9 +37,6 @@
|
|||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="external-link"
|
||||
/>
|
||||
<icon name="external-link" />
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
<script>
|
||||
import Timeago from 'timeago.js';
|
||||
import _ from 'underscore';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import { humanize } from '~/lib/utils/text_utility';
|
||||
import ActionsComponent from './environment_actions.vue';
|
||||
import ExternalUrlComponent from './environment_external_url.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import RollbackComponent from './environment_rollback.vue';
|
||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||
import MonitoringButtonComponent from './environment_monitoring.vue';
|
||||
import CommitComponent from '../../vue_shared/components/commit.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import Timeago from 'timeago.js';
|
||||
import _ from 'underscore';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import { humanize } from '~/lib/utils/text_utility';
|
||||
import ActionsComponent from './environment_actions.vue';
|
||||
import ExternalUrlComponent from './environment_external_url.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import RollbackComponent from './environment_rollback.vue';
|
||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||
import MonitoringButtonComponent from './environment_monitoring.vue';
|
||||
import CommitComponent from '../../vue_shared/components/commit.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Envrionment Item Component
|
||||
*
|
||||
* Renders a table row for each environment.
|
||||
*/
|
||||
const timeagoInstance = new Timeago();
|
||||
const timeagoInstance = new Timeago();
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
UserAvatarLink,
|
||||
CommitComponent,
|
||||
|
@ -65,9 +65,7 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
hasLastDeploymentKey() {
|
||||
if (this.model &&
|
||||
this.model.last_deployment &&
|
||||
!_.isEmpty(this.model.last_deployment)) {
|
||||
if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -80,19 +78,21 @@
|
|||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
hasManualActions() {
|
||||
return this.model &&
|
||||
return (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.manual_actions &&
|
||||
this.model.last_deployment.manual_actions.length > 0;
|
||||
this.model.last_deployment.manual_actions.length > 0
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the value of the `stop_action?` key provided in the response.
|
||||
* Returns whether the environment can be stopped.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasStopAction() {
|
||||
return this.model && this.model['stop_action?'];
|
||||
canStopEnvironment() {
|
||||
return this.model && this.model.can_stop;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -102,10 +102,12 @@
|
|||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
canRetry() {
|
||||
return this.model &&
|
||||
return (
|
||||
this.model &&
|
||||
this.hasLastDeploymentKey &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable;
|
||||
this.model.last_deployment.deployable
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -114,10 +116,12 @@
|
|||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
canShowDate() {
|
||||
return this.model &&
|
||||
return (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable !== undefined;
|
||||
this.model.last_deployment.deployable !== undefined
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -126,10 +130,12 @@
|
|||
* @returns {String}
|
||||
*/
|
||||
createdDate() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.created_at) {
|
||||
this.model.last_deployment.deployable.created_at
|
||||
) {
|
||||
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
|
||||
}
|
||||
return '';
|
||||
|
@ -142,7 +148,7 @@
|
|||
*/
|
||||
manualActions() {
|
||||
if (this.hasManualActions) {
|
||||
return this.model.last_deployment.manual_actions.map((action) => {
|
||||
return this.model.last_deployment.manual_actions.map(action => {
|
||||
const parsedAction = {
|
||||
name: humanize(action.name),
|
||||
play_path: action.play_path,
|
||||
|
@ -160,10 +166,12 @@
|
|||
* @returns {String}
|
||||
*/
|
||||
userImageAltDescription() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.user &&
|
||||
this.model.last_deployment.user.username) {
|
||||
this.model.last_deployment.user.username
|
||||
) {
|
||||
return `${this.model.last_deployment.user.username}'s avatar'`;
|
||||
}
|
||||
return '';
|
||||
|
@ -175,9 +183,7 @@
|
|||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTag() {
|
||||
if (this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.tag) {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.tag) {
|
||||
return this.model.last_deployment.tag;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -189,9 +195,7 @@
|
|||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitRef() {
|
||||
if (this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.ref) {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.ref) {
|
||||
return this.model.last_deployment.ref;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -203,10 +207,12 @@
|
|||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitUrl() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.commit_path) {
|
||||
this.model.last_deployment.commit.commit_path
|
||||
) {
|
||||
return this.model.last_deployment.commit.commit_path;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -218,10 +224,12 @@
|
|||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitShortSha() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.short_id) {
|
||||
this.model.last_deployment.commit.short_id
|
||||
) {
|
||||
return this.model.last_deployment.commit.short_id;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -233,10 +241,12 @@
|
|||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTitle() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.title) {
|
||||
this.model.last_deployment.commit.title
|
||||
) {
|
||||
return this.model.last_deployment.commit.title;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -248,10 +258,12 @@
|
|||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitAuthor() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.author) {
|
||||
this.model.last_deployment.commit.author
|
||||
) {
|
||||
return this.model.last_deployment.commit.author;
|
||||
}
|
||||
|
||||
|
@ -264,10 +276,12 @@
|
|||
* @returns {String|Undefined}
|
||||
*/
|
||||
retryUrl() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.retry_path) {
|
||||
this.model.last_deployment.deployable.retry_path
|
||||
) {
|
||||
return this.model.last_deployment.deployable.retry_path;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -279,8 +293,7 @@
|
|||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
isLastDeployment() {
|
||||
return this.model && this.model.last_deployment &&
|
||||
this.model.last_deployment['last?'];
|
||||
return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -289,9 +302,7 @@
|
|||
* @returns {String}
|
||||
*/
|
||||
buildName() {
|
||||
if (this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable) {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) {
|
||||
const { deployable } = this.model.last_deployment;
|
||||
return `${deployable.name} #${deployable.id}`;
|
||||
}
|
||||
|
@ -304,9 +315,7 @@
|
|||
* @returns {String}
|
||||
*/
|
||||
deploymentInternalId() {
|
||||
if (this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.iid) {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.iid) {
|
||||
return `#${this.model.last_deployment.iid}`;
|
||||
}
|
||||
return '';
|
||||
|
@ -318,9 +327,11 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
deploymentHasUser() {
|
||||
return this.model &&
|
||||
return (
|
||||
this.model &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
!_.isEmpty(this.model.last_deployment.user);
|
||||
!_.isEmpty(this.model.last_deployment.user)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -330,9 +341,11 @@
|
|||
* @returns {Object}
|
||||
*/
|
||||
deploymentUser() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
!_.isEmpty(this.model.last_deployment.user)) {
|
||||
!_.isEmpty(this.model.last_deployment.user)
|
||||
) {
|
||||
return this.model.last_deployment.user;
|
||||
}
|
||||
return {};
|
||||
|
@ -346,9 +359,11 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
shouldRenderBuildName() {
|
||||
return !this.model.isFolder &&
|
||||
return (
|
||||
!this.model.isFolder &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
!_.isEmpty(this.model.last_deployment.deployable);
|
||||
!_.isEmpty(this.model.last_deployment.deployable)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -357,10 +372,12 @@
|
|||
* @return {String}
|
||||
*/
|
||||
buildPath() {
|
||||
if (this.model &&
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.build_path) {
|
||||
this.model.last_deployment.deployable.build_path
|
||||
) {
|
||||
return this.model.last_deployment.deployable.build_path;
|
||||
}
|
||||
|
||||
|
@ -388,9 +405,11 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
shouldRenderDeploymentID() {
|
||||
return !this.model.isFolder &&
|
||||
return (
|
||||
!this.model.isFolder &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
this.model.last_deployment.iid !== undefined;
|
||||
this.model.last_deployment.iid !== undefined
|
||||
);
|
||||
},
|
||||
|
||||
environmentPath() {
|
||||
|
@ -410,11 +429,13 @@
|
|||
},
|
||||
|
||||
displayEnvironmentActions() {
|
||||
return this.hasManualActions ||
|
||||
return (
|
||||
this.hasManualActions ||
|
||||
this.externalURL ||
|
||||
this.monitoringUrl ||
|
||||
this.hasStopAction ||
|
||||
this.canRetry;
|
||||
this.canStopEnvironment ||
|
||||
this.canRetry
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -423,7 +444,7 @@
|
|||
eventHub.$emit('toggleFolder', this.model);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
|
@ -580,11 +601,6 @@
|
|||
class="btn-group table-action-buttons"
|
||||
role="group">
|
||||
|
||||
<actions-component
|
||||
v-if="hasManualActions && canCreateDeployment"
|
||||
:actions="manualActions"
|
||||
/>
|
||||
|
||||
<external-url-component
|
||||
v-if="externalURL && canReadEnvironment"
|
||||
:external-url="externalURL"
|
||||
|
@ -595,21 +611,26 @@
|
|||
:monitoring-url="monitoringUrl"
|
||||
/>
|
||||
|
||||
<actions-component
|
||||
v-if="hasManualActions && canCreateDeployment"
|
||||
:actions="manualActions"
|
||||
/>
|
||||
|
||||
<terminal-button-component
|
||||
v-if="model && model.terminal_path"
|
||||
:terminal-path="model.terminal_path"
|
||||
/>
|
||||
|
||||
<stop-component
|
||||
v-if="hasStopAction && canCreateDeployment"
|
||||
:stop-url="model.stop_path"
|
||||
/>
|
||||
|
||||
<rollback-component
|
||||
v-if="canRetry && canCreateDeployment"
|
||||
:is-last-deployment="isLastDeployment"
|
||||
:retry-url="retryUrl"
|
||||
/>
|
||||
|
||||
<stop-component
|
||||
v-if="canStopEnvironment"
|
||||
:environment="model"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
/**
|
||||
/**
|
||||
* Renders the Monitoring (Metrics) link in environments table.
|
||||
*/
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
|
@ -23,7 +23,7 @@
|
|||
return 'Monitoring';
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
|
@ -35,9 +35,6 @@
|
|||
data-container="body"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="chart"
|
||||
/>
|
||||
<icon name="chart" />
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
<script>
|
||||
/**
|
||||
/**
|
||||
* Renders Rollback or Re deploy button in environments table depending
|
||||
* of the provided property `isLastDeployment`.
|
||||
*
|
||||
* Makes a post request when the button is clicked.
|
||||
*/
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import eventHub from '../event_hub';
|
||||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
retryUrl: {
|
||||
type: String,
|
||||
|
@ -28,29 +37,38 @@
|
|||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isLoading = true;
|
||||
|
||||
eventHub.$emit('postAction', this.retryUrl);
|
||||
eventHub.$emit('postAction', { endpoint: this.retryUrl });
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
v-tooltip
|
||||
:disabled="isLoading"
|
||||
:title="title"
|
||||
type="button"
|
||||
class="btn d-none d-sm-none d-md-block"
|
||||
@click="onClick"
|
||||
>
|
||||
|
||||
<span v-if="isLastDeployment">
|
||||
{{ s__("Environments|Re-deploy") }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ s__("Environments|Rollback") }}
|
||||
</span>
|
||||
<icon
|
||||
v-if="isLastDeployment"
|
||||
name="repeat" />
|
||||
<icon
|
||||
v-else
|
||||
name="redo"/>
|
||||
|
||||
<loading-icon v-if="isLoading" />
|
||||
</button>
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<script>
|
||||
/**
|
||||
/**
|
||||
* Renders the stop "button" that allows stop an environment.
|
||||
* Used in environments table.
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import $ from 'jquery';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import LoadingButton from '../../vue_shared/components/loading_button.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
Icon,
|
||||
LoadingButton,
|
||||
},
|
||||
|
||||
directives: {
|
||||
|
@ -19,9 +22,9 @@
|
|||
},
|
||||
|
||||
props: {
|
||||
stopUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
environment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -33,40 +36,43 @@
|
|||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Stop';
|
||||
return s__('Environments|Stop environment');
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
eventHub.$on('stopEnvironment', this.onStopEnvironment);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
eventHub.$off('stopEnvironment', this.onStopEnvironment);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm('Are you sure you want to stop this environment?')) {
|
||||
this.isLoading = true;
|
||||
|
||||
$(this.$el).tooltip('dispose');
|
||||
|
||||
eventHub.$emit('postAction', this.stopUrl);
|
||||
eventHub.$emit('requestStopEnvironment', this.environment);
|
||||
},
|
||||
onStopEnvironment(environment) {
|
||||
if (this.environment.id === environment.id) {
|
||||
this.isLoading = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
<loading-button
|
||||
v-tooltip
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
type="button"
|
||||
class="btn stop-env-link d-none d-sm-none d-md-block"
|
||||
container-class="btn btn-danger d-none d-sm-none d-md-block"
|
||||
data-container="body"
|
||||
data-toggle="modal"
|
||||
data-target="#stop-environment-modal"
|
||||
@click="onClick"
|
||||
>
|
||||
<i
|
||||
class="fa fa-stop stop-env-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<loading-icon v-if="isLoading" />
|
||||
</button>
|
||||
<icon name="stop"/>
|
||||
</loading-button>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
/**
|
||||
/**
|
||||
* Renders a terminal button to open a web terminal.
|
||||
* Used in environments table.
|
||||
*/
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
|
@ -25,7 +25,7 @@
|
|||
return 'Terminal';
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
|
@ -36,9 +36,6 @@
|
|||
class="btn terminal-button d-none d-sm-none d-md-block"
|
||||
data-container="body"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="terminal"
|
||||
/>
|
||||
<icon name="terminal" />
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
import eventHub from '../event_hub';
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import StopEnvironmentModal from './stop_environment_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
emptyState,
|
||||
StopEnvironmentModal,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
|
@ -90,6 +92,8 @@
|
|||
</script>
|
||||
<template>
|
||||
<div :class="cssContainerClass">
|
||||
<stop-environment-modal :environment="environmentInStopModal" />
|
||||
|
||||
<div class="top-area">
|
||||
<tabs
|
||||
:tabs="tabs"
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
id: 'stop-environment-modal',
|
||||
name: 'StopEnvironmentModal',
|
||||
|
||||
components: {
|
||||
GlModal,
|
||||
LoadingButton,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
environment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
noStopActionMessage() {
|
||||
return sprintf(
|
||||
s__(
|
||||
`Environments|Note that this action will stop the environment,
|
||||
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
|
||||
due to no “stop environment action” being defined
|
||||
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
|
||||
),
|
||||
{
|
||||
emphasisStart: '<strong>',
|
||||
emphasisEnd: '</strong>',
|
||||
ciConfigLinkStart:
|
||||
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
|
||||
ciConfigLinkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit() {
|
||||
eventHub.$emit('stopEnvironment', this.environment);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
:id="$options.id"
|
||||
:footer-primary-button-text="s__('Environments|Stop environment')"
|
||||
footer-primary-button-variant="danger"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template slot="header">
|
||||
<h4
|
||||
class="modal-title d-flex mw-100"
|
||||
>
|
||||
Stopping
|
||||
<span
|
||||
v-tooltip
|
||||
:title="environment.name"
|
||||
class="text-truncate ml-1 mr-1 flex-fill"
|
||||
>{{ environment.name }}</span>
|
||||
?
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
|
||||
|
||||
<div
|
||||
v-if="!environment.has_stop_action"
|
||||
class="warning_message"
|
||||
>
|
||||
<p v-html="noStopActionMessage"></p>
|
||||
<a
|
||||
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ s__('Environments|Learn more about stopping environments') }}</a>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -1,12 +1,18 @@
|
|||
<script>
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StopEnvironmentModal,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
environmentsMixin,
|
||||
CIPaginationMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
|
@ -38,6 +44,8 @@
|
|||
</script>
|
||||
<template>
|
||||
<div :class="cssContainerClass">
|
||||
<stop-environment-modal :environment="environmentInStopModal" />
|
||||
|
||||
<div
|
||||
v-if="!isLoading"
|
||||
class="top-area"
|
||||
|
|
|
@ -40,6 +40,7 @@ export default {
|
|||
scope: getParameterByName('scope') || 'available',
|
||||
page: getParameterByName('page') || '1',
|
||||
requestData: {},
|
||||
environmentInStopModal: {},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -85,7 +86,7 @@ export default {
|
|||
Flash(s__('Environments|An error occurred while fetching the environments.'));
|
||||
},
|
||||
|
||||
postAction(endpoint) {
|
||||
postAction({ endpoint, errorMessage }) {
|
||||
if (!this.isMakingRequest) {
|
||||
this.isLoading = true;
|
||||
|
||||
|
@ -93,7 +94,7 @@ export default {
|
|||
.then(() => this.fetchEnvironments())
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
Flash(s__('Environments|An error occurred while making the request.'));
|
||||
Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -106,6 +107,15 @@ export default {
|
|||
.catch(this.errorCallback);
|
||||
},
|
||||
|
||||
updateStopModal(environment) {
|
||||
this.environmentInStopModal = environment;
|
||||
},
|
||||
|
||||
stopEnvironment(environment) {
|
||||
const endpoint = environment.stop_path;
|
||||
const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again');
|
||||
this.postAction({ endpoint, errorMessage });
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -162,9 +172,13 @@ export default {
|
|||
});
|
||||
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
eventHub.$on('requestStopEnvironment', this.updateStopModal);
|
||||
eventHub.$on('stopEnvironment', this.stopEnvironment);
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('postAction');
|
||||
beforeDestroy() {
|
||||
eventHub.$off('postAction', this.postAction);
|
||||
eventHub.$off('requestStopEnvironment', this.updateStopModal);
|
||||
eventHub.$off('stopEnvironment', this.stopEnvironment);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class EnvironmentsService {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
postAction(endpoint) {
|
||||
return axios.post(endpoint, {}, { emulateJSON: true });
|
||||
return axios.post(endpoint, {});
|
||||
}
|
||||
|
||||
getFolderContent(folderUrl) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import NewModal from './new_dropdown/modal.vue';
|
||||
import IdeSidebar from './ide_side_bar.vue';
|
||||
import RepoTabs from './repo_tabs.vue';
|
||||
import IdeStatusBar from './ide_status_bar.vue';
|
||||
|
@ -13,6 +14,7 @@ const originalStopCallback = Mousetrap.stopCallback;
|
|||
|
||||
export default {
|
||||
components: {
|
||||
NewModal,
|
||||
IdeSidebar,
|
||||
RepoTabs,
|
||||
IdeStatusBar,
|
||||
|
@ -137,5 +139,6 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
<ide-status-bar :file="activeFile"/>
|
||||
<new-modal />
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import IdeTreeList from './ide_tree_list.vue';
|
||||
import Upload from './new_dropdown/upload.vue';
|
||||
import NewEntryButton from './new_dropdown/button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewDropdown,
|
||||
Icon,
|
||||
Upload,
|
||||
IdeTreeList,
|
||||
NewEntryButton,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId']),
|
||||
|
@ -20,23 +24,42 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ide-tree-list
|
||||
header-class="d-flex w-100"
|
||||
viewer-type="editor"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
<new-dropdown
|
||||
:project-id="currentProject.name_with_namespace"
|
||||
:branch="currentBranchId"
|
||||
<div class="ml-auto d-flex">
|
||||
<new-entry-button
|
||||
:label="__('New file')"
|
||||
:show-label="false"
|
||||
class="d-flex border-0 p-0 mr-3"
|
||||
icon="doc-new"
|
||||
@click="openNewEntryModal({ type: 'blob' })"
|
||||
/>
|
||||
<upload
|
||||
:show-label="false"
|
||||
class="d-flex mr-3"
|
||||
button-css-classes="border-0 p-0"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
<new-entry-button
|
||||
:label="__('New directory')"
|
||||
:show-label="false"
|
||||
class="d-flex border-0 p-0"
|
||||
icon="folder-new"
|
||||
@click="openNewEntryModal({ type: 'tree' })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ide-tree-list>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clicked() {
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:aria-label="label"
|
||||
type="button"
|
||||
@click.stop.prevent="clicked"
|
||||
>
|
||||
<icon
|
||||
:name="icon"
|
||||
:css-classes="iconClasses"
|
||||
/>
|
||||
<template v-if="showLabel">
|
||||
{{ label }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
|
@ -3,12 +3,14 @@ import { mapActions } from 'vuex';
|
|||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
import ItemButton from './button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
ItemButton,
|
||||
},
|
||||
props: {
|
||||
branch: {
|
||||
|
@ -20,11 +22,13 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mouseOver: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
dropdownOpen: false,
|
||||
};
|
||||
},
|
||||
|
@ -34,17 +38,18 @@ export default {
|
|||
this.$refs.dropdownMenu.scrollIntoView();
|
||||
});
|
||||
},
|
||||
mouseOver() {
|
||||
if (!this.mouseOver) {
|
||||
this.dropdownOpen = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createTempEntry']),
|
||||
...mapActions(['createTempEntry', 'openNewEntryModal']),
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.openModal = true;
|
||||
this.openNewEntryModal({ type, path: this.path });
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
},
|
||||
|
@ -58,23 +63,19 @@ export default {
|
|||
:class="{
|
||||
show: dropdownOpen,
|
||||
}"
|
||||
class="dropdown"
|
||||
class="dropdown d-flex"
|
||||
>
|
||||
<button
|
||||
:aria-label="__('Create new file or directory')"
|
||||
type="button"
|
||||
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
|
||||
aria-label="Create new file or directory"
|
||||
class="rounded border-0 d-flex ide-entry-dropdown-toggle"
|
||||
@click.stop="openDropdown()"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="plus"
|
||||
css-classes="float-left"
|
||||
name="hamburger"
|
||||
/>
|
||||
<icon
|
||||
:size="12"
|
||||
name="arrow-down"
|
||||
css-classes="float-left"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
|
@ -82,39 +83,30 @@ export default {
|
|||
class="dropdown-menu dropdown-menu-right"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="createNewItem('blob')"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</a>
|
||||
<item-button
|
||||
:label="__('New file')"
|
||||
class="d-flex"
|
||||
icon="doc-new"
|
||||
icon-classes="mr-2"
|
||||
@click="createNewItem('blob')"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<upload
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="createNewItem('tree')"
|
||||
>
|
||||
{{ __('New directory') }}
|
||||
</a>
|
||||
<item-button
|
||||
:label="__('New directory')"
|
||||
class="d-flex"
|
||||
icon="folder-new"
|
||||
icon-classes="mr-2"
|
||||
@click="createNewItem('tree')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<new-modal
|
||||
v-if="openModal"
|
||||
:type="modalType"
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
@hide="hideModal"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,78 +1,70 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
GlModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entryName: this.path !== '' ? `${this.path}/` : '',
|
||||
name: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['newEntryModal']),
|
||||
entryName: {
|
||||
get() {
|
||||
return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : '');
|
||||
},
|
||||
set(val) {
|
||||
this.name = val;
|
||||
},
|
||||
},
|
||||
modalTitle() {
|
||||
if (this.type === 'tree') {
|
||||
if (this.newEntryModal.type === 'tree') {
|
||||
return __('Create new directory');
|
||||
}
|
||||
|
||||
return __('Create new file');
|
||||
},
|
||||
buttonLabel() {
|
||||
if (this.type === 'tree') {
|
||||
if (this.newEntryModal.type === 'tree') {
|
||||
return __('Create directory');
|
||||
}
|
||||
|
||||
return __('Create file');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fieldName.focus();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createTempEntry']),
|
||||
createEntryInStore() {
|
||||
this.$emit('create', {
|
||||
branchId: this.branchId,
|
||||
name: this.entryName,
|
||||
type: this.type,
|
||||
this.createTempEntry({
|
||||
name: this.name,
|
||||
type: this.newEntryModal.type,
|
||||
});
|
||||
|
||||
this.hideModal();
|
||||
},
|
||||
hideModal() {
|
||||
this.$emit('hide');
|
||||
focusInput() {
|
||||
setTimeout(() => {
|
||||
this.$refs.fieldName.focus();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<deprecated-modal
|
||||
:title="modalTitle"
|
||||
:primary-button-label="buttonLabel"
|
||||
kind="success"
|
||||
@cancel="hideModal"
|
||||
<gl-modal
|
||||
id="ide-new-entry"
|
||||
:header-title-text="modalTitle"
|
||||
:footer-primary-button-text="buttonLabel"
|
||||
footer-primary-button-variant="success"
|
||||
@submit="createEntryInStore"
|
||||
@open="focusInput"
|
||||
>
|
||||
<form
|
||||
slot="body"
|
||||
<div
|
||||
class="form-group row"
|
||||
@submit.prevent="createEntryInStore"
|
||||
>
|
||||
<label class="label-light col-form-label col-sm-3">
|
||||
{{ __('Name') }}
|
||||
|
@ -85,6 +77,6 @@ export default {
|
|||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</deprecated-modal>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import ItemButton from './button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
ItemButton,
|
||||
},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
buttonCssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
||||
|
@ -28,8 +41,7 @@
|
|||
}
|
||||
|
||||
this.$emit('create', {
|
||||
name: `${(this.path ? `${this.path}/` : '')}${name}`,
|
||||
branchId: this.branchId,
|
||||
name: `${this.path ? `${this.path}/` : ''}${name}`,
|
||||
type: 'blob',
|
||||
content: result,
|
||||
base64: !isText,
|
||||
|
@ -54,18 +66,20 @@
|
|||
this.$refs.fileUpload.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="startFileUpload"
|
||||
>
|
||||
{{ __('Upload file') }}
|
||||
</a>
|
||||
<item-button
|
||||
:class="buttonCssClasses"
|
||||
:show-label="showLabel"
|
||||
:icon-classes="showLabel ? 'mr-2' : ''"
|
||||
:label="__('Upload file')"
|
||||
class="d-flex"
|
||||
icon="upload"
|
||||
@click="startFileUpload"
|
||||
/>
|
||||
<input
|
||||
id="file-upload"
|
||||
ref="fileUpload"
|
||||
|
|
|
@ -40,6 +40,11 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
|
@ -142,6 +147,9 @@ export default {
|
|||
hasUrlAtCurrentRoute() {
|
||||
return this.$router.currentRoute.path === `/project${this.file.url}`;
|
||||
},
|
||||
toggleHover(over) {
|
||||
this.mouseOver = over;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -153,6 +161,8 @@ export default {
|
|||
class="file"
|
||||
role="button"
|
||||
@click="clickFile"
|
||||
@mouseover="toggleHover(true)"
|
||||
@mouseout="toggleHover(false)"
|
||||
>
|
||||
<div
|
||||
class="file-name"
|
||||
|
@ -206,6 +216,7 @@ export default {
|
|||
:project-id="file.projectId"
|
||||
:branch="file.branchId"
|
||||
:path="file.path"
|
||||
:mouse-over="mouseOver"
|
||||
class="float-right prepend-left-8"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
|
|||
|
||||
export const createTempEntry = (
|
||||
{ state, commit, dispatch },
|
||||
{ branchId, name, type, content = '', base64 = false },
|
||||
{ name, type, content = '', base64 = false },
|
||||
) =>
|
||||
new Promise(resolve => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
|
@ -81,7 +81,7 @@ export const createTempEntry = (
|
|||
commit(types.CREATE_TMP_ENTRY, {
|
||||
data,
|
||||
projectId: state.currentProjectId,
|
||||
branchId,
|
||||
branchId: state.currentBranchId,
|
||||
});
|
||||
|
||||
if (type === 'blob') {
|
||||
|
@ -100,7 +100,7 @@ export const createTempEntry = (
|
|||
worker.postMessage({
|
||||
data: [fullName],
|
||||
projectId: state.currentProjectId,
|
||||
branchId,
|
||||
branchId: state.currentBranchId,
|
||||
type,
|
||||
tempFile: true,
|
||||
base64,
|
||||
|
@ -178,6 +178,13 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
|
|||
export const setErrorMessage = ({ commit }, errorMessage) =>
|
||||
commit(types.SET_ERROR_MESSAGE, errorMessage);
|
||||
|
||||
export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
|
||||
commit(types.OPEN_NEW_ENTRY_MODAL, { type, path });
|
||||
|
||||
// open the modal manually so we don't mess around with dropdown/rows
|
||||
$('#ide-new-entry').modal('show');
|
||||
};
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
|
|
|
@ -74,3 +74,5 @@ export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
|
|||
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
|
||||
|
||||
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
|
||||
|
||||
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
|
||||
|
|
|
@ -166,6 +166,11 @@ export default {
|
|||
[types.SET_ERROR_MESSAGE](state, errorMessage) {
|
||||
Object.assign(state, { errorMessage });
|
||||
},
|
||||
[types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
|
||||
Object.assign(state, {
|
||||
newEntryModal: { type, path },
|
||||
});
|
||||
},
|
||||
...projectMutations,
|
||||
...mergeRequestMutation,
|
||||
...fileMutations,
|
||||
|
|
|
@ -26,4 +26,8 @@ export default () => ({
|
|||
rightPane: null,
|
||||
links: {},
|
||||
errorMessage: null,
|
||||
newEntryModal: {
|
||||
type: '',
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -200,7 +200,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
|
|||
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
|
||||
type="button"
|
||||
@click="cancelHandler()">
|
||||
{{ __('Discard draft') }}
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -15,6 +15,8 @@ let eTagPoll;
|
|||
|
||||
export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
|
||||
|
||||
export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);
|
||||
|
||||
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
|
||||
|
||||
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
|
||||
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
|
||||
export const DELETE_NOTE = 'DELETE_NOTE';
|
||||
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
|
||||
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
|
||||
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
|
||||
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
|
||||
|
@ -11,12 +10,16 @@ export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
|
|||
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
|
||||
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
|
||||
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
|
||||
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
|
||||
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';
|
||||
|
||||
// DISCUSSION
|
||||
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
|
||||
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
|
||||
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
|
||||
|
||||
// Issue
|
||||
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
|
||||
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
|
||||
|
|
|
@ -58,6 +58,11 @@ export default {
|
|||
discussion.expanded = true;
|
||||
},
|
||||
|
||||
[types.COLLAPSE_DISCUSSION](state, { discussionId }) {
|
||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||
discussion.expanded = false;
|
||||
},
|
||||
|
||||
[types.REMOVE_PLACEHOLDER_NOTES](state) {
|
||||
const { discussions } = state;
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ export default class Todos {
|
|||
}
|
||||
|
||||
initFilters() {
|
||||
this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
|
||||
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
|
||||
this.initFilterDropdown($('.js-type-search'), 'type');
|
||||
this.initFilterDropdown($('.js-action-search'), 'action_id');
|
||||
|
@ -54,16 +53,7 @@ export default class Todos {
|
|||
filterable: searchFields ? true : false,
|
||||
search: { fields: searchFields },
|
||||
data: $dropdown.data('data'),
|
||||
clicked: () => {
|
||||
const $formEl = $dropdown.closest('form.filter-form');
|
||||
const mutexDropdowns = {
|
||||
group_id: 'project_id',
|
||||
project_id: 'group_id',
|
||||
};
|
||||
|
||||
$formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
|
||||
$formEl.submit();
|
||||
},
|
||||
clicked: () => $dropdown.closest('form.filter-form').submit(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
|
||||
const MARK_TEXT = __('Mark todo as done');
|
||||
const TODO_TEXT = __('Add todo');
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
},
|
||||
props: {
|
||||
issuableId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isTodo: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
isActionActive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
buttonClasses() {
|
||||
return this.collapsed ?
|
||||
'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
|
||||
'btn btn-default btn-todo issuable-header-btn float-right';
|
||||
},
|
||||
buttonLabel() {
|
||||
return this.isTodo ? MARK_TEXT : TODO_TEXT;
|
||||
},
|
||||
collapsedButtonIconClasses() {
|
||||
return this.isTodo ? 'todo-undone' : '';
|
||||
},
|
||||
collapsedButtonIcon() {
|
||||
return this.isTodo ? 'todo-done' : 'todo-add';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleButtonClick() {
|
||||
this.$emit('toggleTodo');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-tooltip
|
||||
:class="buttonClasses"
|
||||
:title="buttonLabel"
|
||||
:aria-label="buttonLabel"
|
||||
:data-issuable-id="issuableId"
|
||||
:data-issuable-type="issuableType"
|
||||
type="button"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
data-boundary="viewport"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
<icon
|
||||
v-show="collapsed"
|
||||
:css-classes="collapsedButtonIconClasses"
|
||||
:name="collapsedButtonIcon"
|
||||
/>
|
||||
<span
|
||||
v-show="!collapsed"
|
||||
class="issuable-todo-inner"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<loading-icon
|
||||
v-show="isActionActive"
|
||||
:inline="true"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
|
@ -45,6 +45,11 @@ export default {
|
|||
emitSubmit(event) {
|
||||
this.$emit('submit', event);
|
||||
},
|
||||
opened({ propertyName }) {
|
||||
if (propertyName === 'opacity') {
|
||||
this.$emit('open');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -55,6 +60,7 @@ export default {
|
|||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
@transitionend="opened"
|
||||
>
|
||||
<div
|
||||
:class="modalSizeClass"
|
||||
|
|
|
@ -12,11 +12,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tooltipLabel() {
|
||||
|
@ -35,12 +30,10 @@ export default {
|
|||
<button
|
||||
v-tooltip
|
||||
:title="tooltipLabel"
|
||||
:class="cssClasses"
|
||||
type="button"
|
||||
class="btn btn-blank gutter-toggle btn-sidebar-action"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
data-boundary="viewport"
|
||||
@click="toggle"
|
||||
>
|
||||
<i
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
|
||||
.btn-group {
|
||||
> a {
|
||||
> .btn:not(.btn-danger) {
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
|
|
|
@ -449,7 +449,6 @@
|
|||
|
||||
.todo-undone {
|
||||
color: $gl-link-color;
|
||||
fill: $gl-link-color;
|
||||
}
|
||||
|
||||
.author {
|
||||
|
|
|
@ -116,10 +116,8 @@
|
|||
|
||||
.modify-merge-commit-link {
|
||||
padding: 0;
|
||||
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
color: $gl-text-color;
|
||||
|
||||
&:hover,
|
||||
|
@ -501,10 +499,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request-details .content-block {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mr-source-target {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
padding-bottom: $grid-size;
|
||||
|
||||
.file {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&.file-active {
|
||||
|
@ -716,32 +717,6 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.ide-new-btn {
|
||||
.btn {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dropdown-toggle svg {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
|
||||
label {
|
||||
font-weight: $gl-font-weight-normal;
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ide {
|
||||
overflow: hidden;
|
||||
|
||||
|
@ -1340,3 +1315,24 @@
|
|||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-entry-dropdown-toggle {
|
||||
padding: $gl-padding-4;
|
||||
background-color: $theme-gray-100;
|
||||
|
||||
&:hover {
|
||||
background-color: $theme-gray-200;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $white-normal;
|
||||
background-color: $blue-500;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
|
||||
color: $white-normal;
|
||||
background-color: $blue-500;
|
||||
}
|
||||
|
|
|
@ -174,18 +174,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
.todos-filters {
|
||||
.filter-categories {
|
||||
width: 75%;
|
||||
|
||||
.filter-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.todo {
|
||||
.avatar {
|
||||
|
@ -211,10 +199,6 @@
|
|||
}
|
||||
|
||||
.todos-filters {
|
||||
.filter-categories {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,14 @@ class ApplicationController < ActionController::Base
|
|||
protect_from_forgery with: :exception, prepend: true
|
||||
|
||||
helper_method :can?
|
||||
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?, :bitbucket_server_import_enabled?
|
||||
helper_method :import_sources_enabled?, :github_import_enabled?,
|
||||
:gitea_import_enabled?, :github_import_configured?,
|
||||
:gitlab_import_enabled?, :gitlab_import_configured?,
|
||||
:bitbucket_import_enabled?, :bitbucket_import_configured?,
|
||||
:bitbucket_server_import_enabled?,
|
||||
:google_code_import_enabled?, :fogbugz_import_enabled?,
|
||||
:git_import_enabled?, :gitlab_project_import_enabled?,
|
||||
:manifest_import_enabled?
|
||||
|
||||
rescue_from Encoding::CompatibilityError do |exception|
|
||||
log_exception(exception)
|
||||
|
@ -355,6 +362,10 @@ class ApplicationController < ActionController::Base
|
|||
Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
|
||||
end
|
||||
|
||||
def manifest_import_enabled?
|
||||
Group.supports_nested_groups? && Gitlab::CurrentSettings.import_sources.include?('manifest')
|
||||
end
|
||||
|
||||
# U2F (universal 2nd factor) devices need a unique identifier for the application
|
||||
# to perform authentication.
|
||||
# https://developers.yubico.com/U2F/App_ID.html
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
module TodosActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def create
|
||||
todo = TodoService.new.mark_todo(issuable, current_user)
|
||||
|
||||
render json: {
|
||||
count: TodosFinder.new(current_user, state: :pending).execute.count,
|
||||
delete_path: dashboard_todo_path(todo)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def todo_params
|
||||
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
|
||||
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
|
||||
end
|
||||
|
||||
def redirect_out_of_range(todos)
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
class Import::ManifestController < Import::BaseController
|
||||
before_action :whitelist_query_limiting, only: [:create]
|
||||
before_action :verify_import_enabled
|
||||
before_action :ensure_import_vars, only: [:create, :status]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def status
|
||||
@already_added_projects = find_already_added_projects
|
||||
already_added_import_urls = @already_added_projects.pluck(:import_url)
|
||||
|
||||
@pending_repositories = repositories.to_a.reject do |repository|
|
||||
already_added_import_urls.include?(repository[:url])
|
||||
end
|
||||
end
|
||||
|
||||
def upload
|
||||
group = Group.find(params[:group_id])
|
||||
|
||||
unless can?(current_user, :create_projects, group)
|
||||
@errors = ["You don't have enough permissions to create projects in the selected group"]
|
||||
|
||||
render :new && return
|
||||
end
|
||||
|
||||
manifest = Gitlab::ManifestImport::Manifest.new(params[:manifest].tempfile)
|
||||
|
||||
if manifest.valid?
|
||||
session[:manifest_import_repositories] = manifest.projects
|
||||
session[:manifest_import_group_id] = group.id
|
||||
|
||||
redirect_to status_import_manifest_path
|
||||
else
|
||||
@errors = manifest.errors
|
||||
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def jobs
|
||||
render json: find_jobs
|
||||
end
|
||||
|
||||
def create
|
||||
repository = repositories.find do |project|
|
||||
project[:id] == params[:repo_id].to_i
|
||||
end
|
||||
|
||||
project = Gitlab::ManifestImport::ProjectCreator.new(repository, group, current_user).execute
|
||||
|
||||
if project.persisted?
|
||||
render json: ProjectSerializer.new.represent(project)
|
||||
else
|
||||
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_import_vars
|
||||
unless group && repositories.present?
|
||||
redirect_to(new_import_manifest_path)
|
||||
end
|
||||
end
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(id: session[:manifest_import_group_id])
|
||||
end
|
||||
|
||||
def repositories
|
||||
@repositories ||= session[:manifest_import_repositories]
|
||||
end
|
||||
|
||||
def find_jobs
|
||||
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
|
||||
end
|
||||
|
||||
def find_already_added_projects
|
||||
group.all_projects
|
||||
.where(import_type: 'manifest')
|
||||
.where(creator_id: current_user)
|
||||
.includes(:import_state)
|
||||
end
|
||||
|
||||
def verify_import_enabled
|
||||
render_404 unless manifest_import_enabled?
|
||||
end
|
||||
|
||||
def whitelist_query_limiting
|
||||
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/48939')
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
layout 'project'
|
||||
before_action :authorize_read_environment!
|
||||
before_action :authorize_create_environment!, only: [:new, :create]
|
||||
before_action :authorize_create_deployment!, only: [:stop]
|
||||
before_action :authorize_stop_environment!, only: [:stop]
|
||||
before_action :authorize_update_environment!, only: [:edit, :update]
|
||||
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
|
||||
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
|
||||
|
@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
def environment
|
||||
@environment ||= project.environments.find(params[:id])
|
||||
end
|
||||
|
||||
def authorize_stop_environment!
|
||||
access_denied! unless can?(current_user, :stop_environment, environment)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
|
||||
|
||||
stop_url =
|
||||
if environment.stop_action? && can?(current_user, :create_deployment, environment)
|
||||
if can?(current_user, :stop_environment, environment)
|
||||
stop_project_environment_path(project, environment)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
class Projects::TodosController < Projects::ApplicationController
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include TodosActions
|
||||
|
||||
before_action :authenticate_user!, only: [:create]
|
||||
|
||||
def create
|
||||
todo = TodoService.new.mark_todo(issuable, current_user)
|
||||
|
||||
render json: {
|
||||
count: TodosFinder.new(current_user, state: :pending).execute.count,
|
||||
delete_path: dashboard_todo_path(todo)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issuable
|
||||
strong_memoize(:issuable) do
|
||||
@issuable ||= begin
|
||||
case params[:issuable_type]
|
||||
when "issue"
|
||||
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
class TodosFinder
|
||||
prepend FinderWithCrossProjectAccess
|
||||
include FinderMethods
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
requires_cross_project_access unless: -> { project? }
|
||||
|
||||
|
@ -35,11 +34,9 @@ class TodosFinder
|
|||
items = by_author(items)
|
||||
items = by_state(items)
|
||||
items = by_type(items)
|
||||
items = by_group(items)
|
||||
# Filtering by project HAS TO be the last because we use
|
||||
# the project IDs yielded by the todos query thus far
|
||||
items = by_project(items)
|
||||
items = visible_to_user(items)
|
||||
|
||||
sort(items)
|
||||
end
|
||||
|
@ -85,10 +82,6 @@ class TodosFinder
|
|||
params[:project_id].present?
|
||||
end
|
||||
|
||||
def group?
|
||||
params[:group_id].present?
|
||||
end
|
||||
|
||||
def project
|
||||
return @project if defined?(@project)
|
||||
|
||||
|
@ -107,14 +100,18 @@ class TodosFinder
|
|||
@project
|
||||
end
|
||||
|
||||
def group
|
||||
strong_memoize(:group) do
|
||||
Group.find(params[:group_id])
|
||||
def project_ids(items)
|
||||
ids = items.except(:order).select(:project_id)
|
||||
if Gitlab::Database.mysql?
|
||||
# To make UPDATE work on MySQL, wrap it in a SELECT with an alias
|
||||
ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t")
|
||||
end
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
def type?
|
||||
type.present? && %w(Issue MergeRequest Epic).include?(type)
|
||||
type.present? && %w(Issue MergeRequest).include?(type)
|
||||
end
|
||||
|
||||
def type
|
||||
|
@ -151,37 +148,12 @@ class TodosFinder
|
|||
|
||||
def by_project(items)
|
||||
if project?
|
||||
items = items.where(project: project)
|
||||
end
|
||||
|
||||
items
|
||||
end
|
||||
|
||||
def by_group(items)
|
||||
if group?
|
||||
groups = group.self_and_descendants
|
||||
items = items.where(
|
||||
'project_id IN (?) OR group_id IN (?)',
|
||||
Project.where(group: groups).select(:id),
|
||||
groups.select(:id)
|
||||
)
|
||||
end
|
||||
|
||||
items
|
||||
end
|
||||
|
||||
def visible_to_user(items)
|
||||
items.where(project: project)
|
||||
else
|
||||
projects = Project.public_or_visible_to_user(current_user)
|
||||
groups = Group.public_or_visible_to_user(current_user)
|
||||
|
||||
items
|
||||
.joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id')
|
||||
.joins('LEFT JOIN projects ON projects.id = todos.project_id')
|
||||
.where(
|
||||
'project_id IN (?) OR group_id IN (?)',
|
||||
projects.select(:id),
|
||||
groups.select(:id)
|
||||
)
|
||||
items.joins(:project).merge(projects)
|
||||
end
|
||||
end
|
||||
|
||||
def by_state(items)
|
||||
|
|
|
@ -251,6 +251,7 @@ module ApplicationSettingsHelper
|
|||
:user_oauth_applications,
|
||||
:version_check_enabled,
|
||||
:allow_local_requests_from_hooks_and_services,
|
||||
:hide_third_party_offers,
|
||||
:enforce_terms,
|
||||
:terms,
|
||||
:mirror_available
|
||||
|
|
|
@ -4,6 +4,7 @@ module ClustersHelper
|
|||
end
|
||||
|
||||
def render_gcp_signup_offer
|
||||
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
|
||||
return unless show_gcp_signup_offer?
|
||||
|
||||
content_tag :section, class: 'no-animate expanded' do
|
||||
|
|
|
@ -131,19 +131,6 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def group_dropdown_label(group_id, default_label)
|
||||
return default_label if group_id.nil?
|
||||
return "Any group" if group_id == "0"
|
||||
|
||||
group = ::Group.find_by(id: group_id)
|
||||
|
||||
if group
|
||||
group.full_name
|
||||
else
|
||||
default_label
|
||||
end
|
||||
end
|
||||
|
||||
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
|
||||
title =
|
||||
case milestone_title
|
||||
|
|
|
@ -3,7 +3,7 @@ module NamespacesHelper
|
|||
params.dig(:project, :namespace_id) || params[:namespace_id]
|
||||
end
|
||||
|
||||
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
|
||||
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil, groups_only: false)
|
||||
groups = current_user.manageable_groups
|
||||
.joins(:route)
|
||||
.includes(:route)
|
||||
|
@ -20,11 +20,14 @@ module NamespacesHelper
|
|||
|
||||
options = []
|
||||
options << options_for_group(groups, display_path: display_path, type: 'group')
|
||||
|
||||
unless groups_only
|
||||
options << options_for_group(users, display_path: display_path, type: 'user')
|
||||
|
||||
if selected == :current_user && current_user.namespace
|
||||
selected = current_user.namespace.id
|
||||
end
|
||||
end
|
||||
|
||||
grouped_options_for_select(options, selected)
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ module PipelineSchedulesHelper
|
|||
ActiveSupport::TimeZone.all.map do |timezone|
|
||||
{
|
||||
name: timezone.name,
|
||||
offset: timezone.utc_offset,
|
||||
offset: timezone.now.utc_offset,
|
||||
identifier: timezone.tzinfo.identifier
|
||||
}
|
||||
end
|
||||
|
|
|
@ -5,9 +5,13 @@ module TimeHelper
|
|||
seconds = interval_in_seconds - minutes * 60
|
||||
|
||||
if minutes >= 1
|
||||
"#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
|
||||
if seconds % 60 == 0
|
||||
pluralize(minutes, "minute")
|
||||
else
|
||||
"#{pluralize(seconds, "second")}"
|
||||
[pluralize(minutes, "minute"), pluralize(seconds, "second")].to_sentence
|
||||
end
|
||||
else
|
||||
pluralize(seconds, "second")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ module TodosHelper
|
|||
project_commit_path(todo.project,
|
||||
todo.target, anchor: anchor)
|
||||
else
|
||||
path = [todo.parent, todo.target]
|
||||
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
|
||||
|
||||
path.unshift(:pipelines) if todo.build_failed?
|
||||
|
||||
|
@ -167,12 +167,4 @@ module TodosHelper
|
|||
def show_todo_state?(todo)
|
||||
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
|
||||
end
|
||||
|
||||
def todo_group_options
|
||||
groups = current_user.authorized_groups.map do |group|
|
||||
{ id: group.id, text: group.full_name }
|
||||
end
|
||||
|
||||
groups.unshift({ id: '', text: 'Any Group' }).to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -294,6 +294,7 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
gitaly_timeout_medium: 30,
|
||||
gitaly_timeout_default: 55,
|
||||
allow_local_requests_from_hooks_and_services: false,
|
||||
hide_third_party_offers: false,
|
||||
mirror_available: true
|
||||
}
|
||||
end
|
||||
|
|
|
@ -437,9 +437,9 @@ module Ci
|
|||
end
|
||||
|
||||
def artifacts_metadata_entry(path, **options)
|
||||
artifacts_metadata.use_file do |metadata_path|
|
||||
artifacts_metadata.open do |metadata_stream|
|
||||
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
|
||||
metadata_path,
|
||||
metadata_stream,
|
||||
path,
|
||||
**options)
|
||||
|
||||
|
|
|
@ -243,12 +243,6 @@ module Issuable
|
|||
opened?
|
||||
end
|
||||
|
||||
def overdue?
|
||||
return false unless respond_to?(:due_date)
|
||||
|
||||
due_date.try(:past?) || false
|
||||
end
|
||||
|
||||
def user_notes_count
|
||||
if notes.loaded?
|
||||
# Use the in-memory association to select and count to avoid hitting the db
|
||||
|
|
|
@ -2,19 +2,20 @@ module ProtectedRefAccess
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
ALLOWED_ACCESS_LEVELS = [
|
||||
Gitlab::Access::MASTER,
|
||||
Gitlab::Access::MAINTAINER,
|
||||
Gitlab::Access::DEVELOPER,
|
||||
Gitlab::Access::NO_ACCESS
|
||||
].freeze
|
||||
|
||||
HUMAN_ACCESS_LEVELS = {
|
||||
Gitlab::Access::MASTER => "Maintainers".freeze,
|
||||
Gitlab::Access::MAINTAINER => "Maintainers".freeze,
|
||||
Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze,
|
||||
Gitlab::Access::NO_ACCESS => "No one".freeze
|
||||
}.freeze
|
||||
|
||||
included do
|
||||
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
|
||||
scope :master, -> { maintainer } # @deprecated
|
||||
scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
|
||||
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
|
||||
|
||||
validates :access_level, presence: true, if: :role?, inclusion: {
|
||||
|
|
|
@ -6,8 +6,11 @@ module SelectForProjectAuthorization
|
|||
select("projects.id AS project_id, members.access_level")
|
||||
end
|
||||
|
||||
def select_as_master_for_project_authorization
|
||||
select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
|
||||
def select_as_maintainer_for_project_authorization
|
||||
select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :select_as_master_for_project_authorization, :select_as_maintainer_for_project_authorization
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,8 +39,6 @@ class Group < Namespace
|
|||
has_many :boards
|
||||
has_many :badges, class_name: 'GroupBadge'
|
||||
|
||||
has_many :todos
|
||||
|
||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||
|
||||
validate :visibility_level_allowed_by_projects
|
||||
|
@ -84,12 +82,6 @@ class Group < Namespace
|
|||
where(id: user.authorized_groups.select(:id).reorder(nil))
|
||||
end
|
||||
|
||||
def public_or_visible_to_user(user)
|
||||
where('id IN (?) OR namespaces.visibility_level IN (?)',
|
||||
user.authorized_groups.select(:id),
|
||||
Gitlab::VisibilityLevel.levels_for_user(user))
|
||||
end
|
||||
|
||||
def select_for_project_authorization
|
||||
if current_scope.joins_values.include?(:shared_projects)
|
||||
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
|
||||
|
@ -186,10 +178,13 @@ class Group < Namespace
|
|||
add_user(user, :developer, current_user: current_user)
|
||||
end
|
||||
|
||||
def add_master(user, current_user = nil)
|
||||
add_user(user, :master, current_user: current_user)
|
||||
def add_maintainer(user, current_user = nil)
|
||||
add_user(user, :maintainer, current_user: current_user)
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :add_master, :add_maintainer
|
||||
|
||||
def add_owner(user, current_user = nil)
|
||||
add_user(user, :owner, current_user: current_user)
|
||||
end
|
||||
|
@ -206,12 +201,15 @@ class Group < Namespace
|
|||
members_with_parents.owners.where(user_id: user).any?
|
||||
end
|
||||
|
||||
def has_master?(user)
|
||||
def has_maintainer?(user)
|
||||
return false unless user
|
||||
|
||||
members_with_parents.masters.where(user_id: user).any?
|
||||
members_with_parents.maintainers.where(user_id: user).any?
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :has_master?, :has_maintainer?
|
||||
|
||||
# Check if user is a last owner of the group.
|
||||
# Parent owners are ignored for nested groups.
|
||||
def last_owner?(user)
|
||||
|
|
|
@ -275,6 +275,10 @@ class Issue < ActiveRecord::Base
|
|||
user ? readable_by?(user) : publicly_visible?
|
||||
end
|
||||
|
||||
def overdue?
|
||||
due_date.try(:past?) || false
|
||||
end
|
||||
|
||||
def check_for_spam?
|
||||
project.public? && (title_changed? || description_changed?)
|
||||
end
|
||||
|
|
|
@ -69,9 +69,11 @@ class Member < ActiveRecord::Base
|
|||
scope :guests, -> { active.where(access_level: GUEST) }
|
||||
scope :reporters, -> { active.where(access_level: REPORTER) }
|
||||
scope :developers, -> { active.where(access_level: DEVELOPER) }
|
||||
scope :masters, -> { active.where(access_level: MASTER) }
|
||||
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
|
||||
scope :masters, -> { maintainers } # @deprecated
|
||||
scope :owners, -> { active.where(access_level: OWNER) }
|
||||
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
|
||||
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
|
||||
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
|
||||
|
||||
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
|
||||
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
|
||||
|
|
|
@ -17,19 +17,19 @@ class ProjectMember < Member
|
|||
# Add users to projects with passed access option
|
||||
#
|
||||
# access can be an integer representing a access code
|
||||
# or symbol like :master representing role
|
||||
# or symbol like :maintainer representing role
|
||||
#
|
||||
# Ex.
|
||||
# add_users_to_projects(
|
||||
# project_ids,
|
||||
# user_ids,
|
||||
# ProjectMember::MASTER
|
||||
# ProjectMember::MAINTAINER
|
||||
# )
|
||||
#
|
||||
# add_users_to_projects(
|
||||
# project_ids,
|
||||
# user_ids,
|
||||
# :master
|
||||
# :maintainer
|
||||
# )
|
||||
#
|
||||
def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
|
||||
|
|
|
@ -229,10 +229,6 @@ class Note < ActiveRecord::Base
|
|||
!for_personal_snippet?
|
||||
end
|
||||
|
||||
def for_issuable?
|
||||
for_issue? || for_merge_request?
|
||||
end
|
||||
|
||||
def skip_project_check?
|
||||
!for_project_noteable?
|
||||
end
|
||||
|
|
|
@ -269,7 +269,8 @@ class Project < ActiveRecord::Base
|
|||
delegate :name, to: :owner, allow_nil: true, prefix: true
|
||||
delegate :members, to: :team, prefix: true
|
||||
delegate :add_user, :add_users, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
|
||||
delegate :add_master, to: :team # @deprecated
|
||||
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
|
||||
|
||||
# Validations
|
||||
|
@ -1647,10 +1648,10 @@ class Project < ActiveRecord::Base
|
|||
params = {
|
||||
name: default_branch,
|
||||
push_access_levels_attributes: [{
|
||||
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
|
||||
}],
|
||||
merge_access_levels_attributes: [{
|
||||
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
|
||||
}]
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ class ProjectGroupLink < ActiveRecord::Base
|
|||
GUEST = 10
|
||||
REPORTER = 20
|
||||
DEVELOPER = 30
|
||||
MASTER = 40
|
||||
MAINTAINER = 40
|
||||
MASTER = MAINTAINER # @deprecated
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :group
|
||||
|
|
|
@ -19,10 +19,13 @@ class ProjectTeam
|
|||
add_user(user, :developer, current_user: current_user)
|
||||
end
|
||||
|
||||
def add_master(user, current_user: nil)
|
||||
add_user(user, :master, current_user: current_user)
|
||||
def add_maintainer(user, current_user: nil)
|
||||
add_user(user, :maintainer, current_user: current_user)
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :add_master, :add_maintainer
|
||||
|
||||
def add_role(user, role, current_user: nil)
|
||||
public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
@ -81,10 +84,13 @@ class ProjectTeam
|
|||
@developers ||= fetch_members(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
|
||||
def masters
|
||||
@masters ||= fetch_members(Gitlab::Access::MASTER)
|
||||
def maintainers
|
||||
@maintainers ||= fetch_members(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :masters, :maintainers
|
||||
|
||||
def owners
|
||||
@owners ||=
|
||||
if group
|
||||
|
@ -136,10 +142,13 @@ class ProjectTeam
|
|||
max_member_access(user.id) == Gitlab::Access::DEVELOPER
|
||||
end
|
||||
|
||||
def master?(user)
|
||||
max_member_access(user.id) == Gitlab::Access::MASTER
|
||||
def maintainer?(user)
|
||||
max_member_access(user.id) == Gitlab::Access::MAINTAINER
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :master?, :maintainer?
|
||||
|
||||
# Checks if `user` is authorized for this project, with at least the
|
||||
# `min_access_level` (if given).
|
||||
def member?(user, min_access_level = Gitlab::Access::GUEST)
|
||||
|
|
|
@ -20,7 +20,6 @@ class ProjectWiki
|
|||
@user = user
|
||||
end
|
||||
|
||||
delegate :empty?, to: :pages
|
||||
delegate :repository_storage, :hashed_storage?, to: :project
|
||||
|
||||
def path
|
||||
|
@ -74,6 +73,10 @@ class ProjectWiki
|
|||
!!find_page('home')
|
||||
end
|
||||
|
||||
def empty?
|
||||
pages(limit: 1).empty?
|
||||
end
|
||||
|
||||
# Returns an Array of Gitlab WikiPage instances or an
|
||||
# empty Array if this Wiki has no pages.
|
||||
def pages(limit: nil)
|
||||
|
|
|
@ -22,18 +22,15 @@ class Todo < ActiveRecord::Base
|
|||
belongs_to :author, class_name: "User"
|
||||
belongs_to :note
|
||||
belongs_to :project
|
||||
belongs_to :group
|
||||
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
|
||||
belongs_to :user
|
||||
|
||||
delegate :name, :email, to: :author, prefix: true, allow_nil: true
|
||||
|
||||
validates :action, :target_type, :user, presence: true
|
||||
validates :action, :project, :target_type, :user, presence: true
|
||||
validates :author, presence: true
|
||||
validates :target_id, presence: true, unless: :for_commit?
|
||||
validates :commit_id, presence: true, if: :for_commit?
|
||||
validates :project, presence: true, unless: :group_id
|
||||
validates :group, presence: true, unless: :project_id
|
||||
|
||||
scope :pending, -> { with_state(:pending) }
|
||||
scope :done, -> { with_state(:done) }
|
||||
|
@ -47,7 +44,7 @@ class Todo < ActiveRecord::Base
|
|||
state :done
|
||||
end
|
||||
|
||||
after_save :keep_around_commit, if: :commit_id
|
||||
after_save :keep_around_commit
|
||||
|
||||
class << self
|
||||
# Priority sorting isn't displayed in the dropdown, because we don't show
|
||||
|
@ -82,10 +79,6 @@ class Todo < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def parent
|
||||
project
|
||||
end
|
||||
|
||||
def unmergeable?
|
||||
action == UNMERGEABLE
|
||||
end
|
||||
|
|
|
@ -99,7 +99,8 @@ class User < ActiveRecord::Base
|
|||
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
|
||||
has_many :groups, through: :group_members
|
||||
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
|
||||
has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
|
||||
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
|
||||
alias_attribute :masters_groups, :maintainers_groups
|
||||
|
||||
# Projects
|
||||
has_many :groups_projects, through: :groups, source: :projects
|
||||
|
@ -728,7 +729,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def several_namespaces?
|
||||
owned_groups.any? || masters_groups.any?
|
||||
owned_groups.any? || maintainers_groups.any?
|
||||
end
|
||||
|
||||
def namespace_id
|
||||
|
@ -974,15 +975,15 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def manageable_groups
|
||||
union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
|
||||
union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql
|
||||
|
||||
# Update this line to not use raw SQL when migrated to Rails 5.2.
|
||||
# Either ActiveRecord or Arel constructions are fine.
|
||||
# This was replaced with the raw SQL construction because of bugs in the arel gem.
|
||||
# Bugs were fixed in arel 9.0.0 (Rails 5.2).
|
||||
owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
|
||||
owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
|
||||
|
||||
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
|
||||
Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants
|
||||
end
|
||||
|
||||
def namespaces
|
||||
|
@ -1023,11 +1024,11 @@ class User < ActiveRecord::Base
|
|||
def ci_owned_runners
|
||||
@ci_owned_runners ||= begin
|
||||
project_runner_ids = Ci::RunnerProject
|
||||
.where(project: authorized_projects(Gitlab::Access::MASTER))
|
||||
.where(project: authorized_projects(Gitlab::Access::MAINTAINER))
|
||||
.select(:runner_id)
|
||||
|
||||
group_runner_ids = Ci::RunnerNamespace
|
||||
.where(namespace_id: owned_or_masters_groups.select(:id))
|
||||
.where(namespace_id: owned_or_maintainers_groups.select(:id))
|
||||
.select(:runner_id)
|
||||
|
||||
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
|
||||
|
@ -1236,11 +1237,14 @@ class User < ActiveRecord::Base
|
|||
!terms_accepted?
|
||||
end
|
||||
|
||||
def owned_or_masters_groups
|
||||
union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
|
||||
def owned_or_maintainers_groups
|
||||
union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups])
|
||||
Group.from("(#{union.to_sql}) namespaces")
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
|
||||
|
||||
protected
|
||||
|
||||
# override, from Devise::Validatable
|
||||
|
|
|
@ -4,7 +4,7 @@ module Clusters
|
|||
|
||||
delegate { cluster.project }
|
||||
|
||||
rule { can?(:master_access) }.policy do
|
||||
rule { can?(:maintainer_access) }.policy do
|
||||
enable :update_cluster
|
||||
enable :admin_cluster
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
class DeployTokenPolicy < BasePolicy
|
||||
with_options scope: :subject, score: 0
|
||||
condition(:master) { @subject.project.team.master?(@user) }
|
||||
condition(:maintainer) { @subject.project.team.maintainer?(@user) }
|
||||
|
||||
rule { anonymous }.prevent_all
|
||||
|
||||
rule { master }.policy do
|
||||
rule { maintainer }.policy do
|
||||
enable :create_deploy_token
|
||||
enable :update_deploy_token
|
||||
end
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
class EnvironmentPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
|
||||
condition(:stop_action_allowed) do
|
||||
@subject.stop_action? && can?(:update_build, @subject.stop_action)
|
||||
condition(:stop_with_deployment_allowed) do
|
||||
@subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action)
|
||||
end
|
||||
|
||||
rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment
|
||||
condition(:stop_with_update_allowed) do
|
||||
!@subject.stop_action? && can?(:update_environment, @subject)
|
||||
end
|
||||
|
||||
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ class GroupPolicy < BasePolicy
|
|||
condition(:guest) { access_level >= GroupMember::GUEST }
|
||||
condition(:developer) { access_level >= GroupMember::DEVELOPER }
|
||||
condition(:owner) { access_level >= GroupMember::OWNER }
|
||||
condition(:master) { access_level >= GroupMember::MASTER }
|
||||
condition(:maintainer) { access_level >= GroupMember::MAINTAINER }
|
||||
condition(:reporter) { access_level >= GroupMember::REPORTER }
|
||||
|
||||
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
|
||||
|
@ -59,7 +59,7 @@ class GroupPolicy < BasePolicy
|
|||
enable :admin_issue
|
||||
end
|
||||
|
||||
rule { master }.policy do
|
||||
rule { maintainer }.policy do
|
||||
enable :create_projects
|
||||
enable :admin_pipeline
|
||||
enable :admin_build
|
||||
|
|
|
@ -46,7 +46,7 @@ class ProjectPolicy < BasePolicy
|
|||
condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER }
|
||||
|
||||
desc "User has maintainer access"
|
||||
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
|
||||
condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER }
|
||||
|
||||
desc "Project is public"
|
||||
condition(:public_project, scope: :subject, score: 0) { project.public? }
|
||||
|
@ -123,14 +123,14 @@ class ProjectPolicy < BasePolicy
|
|||
rule { guest }.enable :guest_access
|
||||
rule { reporter }.enable :reporter_access
|
||||
rule { developer }.enable :developer_access
|
||||
rule { master }.enable :master_access
|
||||
rule { maintainer }.enable :maintainer_access
|
||||
rule { owner | admin }.enable :owner_access
|
||||
|
||||
rule { can?(:owner_access) }.policy do
|
||||
enable :guest_access
|
||||
enable :reporter_access
|
||||
enable :developer_access
|
||||
enable :master_access
|
||||
enable :maintainer_access
|
||||
|
||||
enable :change_namespace
|
||||
enable :change_visibility_level
|
||||
|
@ -228,7 +228,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :create_deployment
|
||||
end
|
||||
|
||||
rule { can?(:master_access) }.policy do
|
||||
rule { can?(:maintainer_access) }.policy do
|
||||
enable :push_to_delete_protected_branch
|
||||
enable :update_project_snippet
|
||||
enable :update_environment
|
||||
|
|
|
@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
|
|||
expose :external_url
|
||||
expose :environment_type
|
||||
expose :last_deployment, using: DeploymentEntity
|
||||
expose :stop_action?
|
||||
expose :stop_action?, as: :has_stop_action
|
||||
|
||||
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
|
||||
metrics_project_environment_path(environment.project, environment)
|
||||
|
@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity
|
|||
end
|
||||
|
||||
expose :created_at, :updated_at
|
||||
|
||||
expose :can_stop do |environment|
|
||||
environment.available? && can?(current_user, :stop_environment, environment)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
request.current_user
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
module Groups
|
||||
class NestedCreateService < Groups::BaseService
|
||||
attr_reader :group_path
|
||||
attr_reader :group_path, :visibility_level
|
||||
|
||||
def initialize(user, params)
|
||||
@current_user, @params = user, params.dup
|
||||
|
||||
@group_path = @params.delete(:group_path)
|
||||
@visibility_level = @params.delete(:visibility_level) ||
|
||||
Gitlab::CurrentSettings.current_application_settings.default_group_visibility
|
||||
end
|
||||
|
||||
def execute
|
||||
|
@ -36,11 +37,12 @@ module Groups
|
|||
new_params = params.reverse_merge(
|
||||
path: subgroup_name,
|
||||
name: subgroup_name,
|
||||
parent: last_group
|
||||
parent: last_group,
|
||||
visibility_level: visibility_level
|
||||
)
|
||||
new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility
|
||||
|
||||
last_group = namespace_or_group(partial_path) || Groups::CreateService.new(current_user, new_params).execute
|
||||
last_group = namespace_or_group(partial_path) ||
|
||||
Groups::CreateService.new(current_user, new_params).execute
|
||||
end
|
||||
|
||||
last_group
|
||||
|
|
|
@ -274,9 +274,9 @@ class NotificationService
|
|||
def new_access_request(member)
|
||||
return true unless member.notifiable?(:subscription)
|
||||
|
||||
recipients = member.source.members.active_without_invites_and_requests.owners_and_masters
|
||||
if fallback_to_group_owners_masters?(recipients, member)
|
||||
recipients = member.source.group.members.active_without_invites_and_requests.owners_and_masters
|
||||
recipients = member.source.members.active_without_invites_and_requests.owners_and_maintainers
|
||||
if fallback_to_group_owners_maintainers?(recipients, member)
|
||||
recipients = member.source.group.members.active_without_invites_and_requests.owners_and_maintainers
|
||||
end
|
||||
|
||||
recipients.each { |recipient| deliver_access_request_email(recipient, member) }
|
||||
|
@ -519,7 +519,7 @@ class NotificationService
|
|||
|
||||
return [] unless project
|
||||
|
||||
notifiable_users(project.team.masters, :watch, target: project)
|
||||
notifiable_users(project.team.maintainers, :watch, target: project)
|
||||
end
|
||||
|
||||
def notifiable?(*args)
|
||||
|
@ -534,7 +534,7 @@ class NotificationService
|
|||
mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.notification_email).deliver_later
|
||||
end
|
||||
|
||||
def fallback_to_group_owners_masters?(recipients, member)
|
||||
def fallback_to_group_owners_maintainers?(recipients, member)
|
||||
return false if recipients.present?
|
||||
|
||||
member.source.respond_to?(:group) && member.source.group
|
||||
|
|
|
@ -115,7 +115,7 @@ module Projects
|
|||
@project.group.refresh_members_authorized_projects(blocking: false)
|
||||
current_user.refresh_authorized_projects
|
||||
else
|
||||
@project.add_master(@project.namespace.owner, current_user: current_user)
|
||||
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ module ProtectedBranches
|
|||
private
|
||||
|
||||
def params_with_default(params)
|
||||
params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
|
||||
params[:"#{type}_access_level"] ||= Gitlab::Access::MAINTAINER if use_default_access_level?(params)
|
||||
params
|
||||
end
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ module ProtectedBranches
|
|||
if params.delete(:developers_can_push)
|
||||
Gitlab::Access::DEVELOPER
|
||||
else
|
||||
Gitlab::Access::MASTER
|
||||
Gitlab::Access::MAINTAINER
|
||||
end
|
||||
|
||||
merge_access_level =
|
||||
if params.delete(:developers_can_merge)
|
||||
Gitlab::Access::DEVELOPER
|
||||
else
|
||||
Gitlab::Access::MASTER
|
||||
Gitlab::Access::MAINTAINER
|
||||
end
|
||||
|
||||
@params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
|
||||
|
|
|
@ -17,14 +17,14 @@ module ProtectedBranches
|
|||
when true
|
||||
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
|
||||
when false
|
||||
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
|
||||
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
|
||||
end
|
||||
|
||||
case @developers_can_merge
|
||||
when true
|
||||
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
|
||||
when false
|
||||
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
|
||||
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
|
||||
end
|
||||
|
||||
service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
|
||||
|
|
|
@ -260,15 +260,15 @@ class TodoService
|
|||
end
|
||||
end
|
||||
|
||||
def create_mention_todos(parent, target, author, note = nil, skip_users = [])
|
||||
def create_mention_todos(project, target, author, note = nil, skip_users = [])
|
||||
# Create Todos for directly addressed users
|
||||
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
|
||||
directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
|
||||
create_todos(directly_addressed_users, attributes)
|
||||
|
||||
# Create Todos for mentioned users
|
||||
mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
|
||||
mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
|
||||
create_todos(mentioned_users, attributes)
|
||||
end
|
||||
|
||||
|
@ -299,36 +299,36 @@ class TodoService
|
|||
|
||||
def attributes_for_todo(project, target, author, action, note = nil)
|
||||
attributes_for_target(target).merge!(
|
||||
project_id: project&.id,
|
||||
project_id: project.id,
|
||||
author_id: author.id,
|
||||
action: action,
|
||||
note: note
|
||||
)
|
||||
end
|
||||
|
||||
def filter_todo_users(users, parent, target)
|
||||
reject_users_without_access(users, parent, target).uniq
|
||||
def filter_todo_users(users, project, target)
|
||||
reject_users_without_access(users, project, target).uniq
|
||||
end
|
||||
|
||||
def filter_mentioned_users(parent, target, author, skip_users = [])
|
||||
def filter_mentioned_users(project, target, author, skip_users = [])
|
||||
mentioned_users = target.mentioned_users(author) - skip_users
|
||||
filter_todo_users(mentioned_users, parent, target)
|
||||
filter_todo_users(mentioned_users, project, target)
|
||||
end
|
||||
|
||||
def filter_directly_addressed_users(parent, target, author, skip_users = [])
|
||||
def filter_directly_addressed_users(project, target, author, skip_users = [])
|
||||
directly_addressed_users = target.directly_addressed_users(author) - skip_users
|
||||
filter_todo_users(directly_addressed_users, parent, target)
|
||||
filter_todo_users(directly_addressed_users, project, target)
|
||||
end
|
||||
|
||||
def reject_users_without_access(users, parent, target)
|
||||
if target.is_a?(Note) && target.for_issuable?
|
||||
def reject_users_without_access(users, project, target)
|
||||
if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
|
||||
target = target.noteable
|
||||
end
|
||||
|
||||
if target.is_a?(Issuable)
|
||||
select_users(users, :"read_#{target.to_ability_name}", target)
|
||||
else
|
||||
select_users(users, :read_project, parent)
|
||||
select_users(users, :read_project, project)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -71,6 +71,28 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
File.join('/', self.class.base_dir, dynamic_segment, filename)
|
||||
end
|
||||
|
||||
def cached_size
|
||||
size
|
||||
end
|
||||
|
||||
def open
|
||||
stream =
|
||||
if file_storage?
|
||||
File.open(path, "rb") if path
|
||||
else
|
||||
::Gitlab::HttpIO.new(url, cached_size) if url
|
||||
end
|
||||
|
||||
return unless stream
|
||||
return stream unless block_given?
|
||||
|
||||
begin
|
||||
yield(stream)
|
||||
ensure
|
||||
stream.close
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Designed to be overridden by child uploaders that have a dynamic path
|
||||
|
|
|
@ -18,14 +18,6 @@ class JobArtifactUploader < GitlabUploader
|
|||
dynamic_segment
|
||||
end
|
||||
|
||||
def open
|
||||
if file_storage?
|
||||
File.open(path, "rb") if path
|
||||
else
|
||||
::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dynamic_segment
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
- application_setting = local_assigns.fetch(:application_setting)
|
||||
|
||||
= form_for application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(application_setting)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :hide_third_party_offers, class: 'form-check-input'
|
||||
= f.label :hide_third_party_offers, class: 'form-check-label' do
|
||||
Do not display offers from third parties within GitLab
|
||||
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
|
@ -325,5 +325,16 @@
|
|||
.settings-content
|
||||
= render partial: 'repository_mirrors_form'
|
||||
|
||||
%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
= _('Third party offers')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Control the display of third party offers.')
|
||||
.settings-content
|
||||
= render 'third_party_offers', application_setting: @application_setting
|
||||
|
||||
= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
%ol
|
||||
%li
|
||||
= _("Install a Runner compatible with GitLab CI")
|
||||
= (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe
|
||||
= (_("(check out the %{link} for information on how to install it).") % { link: link }).html_safe
|
||||
%li
|
||||
= _("Specify the following URL during the Runner setup:")
|
||||
%code#coordinator_address= root_url(only_path: false)
|
||||
|
|
|
@ -30,13 +30,7 @@
|
|||
|
||||
.todos-filters
|
||||
.row-content-block.second-block
|
||||
= form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do
|
||||
.filter-categories.flex-fill
|
||||
.filter-item.inline
|
||||
- if params[:group_id].present?
|
||||
= hidden_field_tag(:group_id, params[:group_id])
|
||||
= dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
|
||||
placeholder: 'Search groups', data: { data: todo_group_options, default_label: 'Group', display: 'static' } })
|
||||
= form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
|
||||
.filter-item.inline
|
||||
- if params[:project_id].present?
|
||||
= hidden_field_tag(:project_id, params[:project_id])
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.explore-title.text-center
|
||||
%h2
|
||||
Explore GitLab
|
||||
= _("Explore GitLab")
|
||||
%p.lead
|
||||
Discover projects, groups and snippets. Share your projects with others
|
||||
= _("Discover projects, groups and snippets. Share your projects with others")
|
||||
%br
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
%ul.nav-links.nav.nav-tabs
|
||||
= nav_link(page: explore_groups_path) do
|
||||
= link_to explore_groups_path do
|
||||
Explore Groups
|
||||
= _("Explore Groups")
|
||||
.nav-controls
|
||||
= render 'shared/groups/search_form'
|
||||
= render 'shared/groups/dropdown'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue