Commit bade1d7b authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' into update-feature-categories-2020-10-01

parents 977be51c fb98fd37
......@@ -9,13 +9,13 @@ export default {
},
inject: {
svgPath: {
type: String,
default: '',
},
docsLink: {
type: String,
default: '',
},
primaryButtonPath: {
type: String,
default: '',
},
},
};
......
......@@ -10,16 +10,16 @@ export default {
},
inject: {
isAdmin: {
type: Boolean,
default: false,
},
svgPath: {
type: String,
default: '',
},
docsLink: {
type: String,
default: '',
},
primaryButtonPath: {
type: String,
default: '',
},
},
};
......
<script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
import pipelinesMixin from '~/pipelines/mixins/pipelines';
......@@ -126,16 +125,6 @@ export default {
(latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline)
);
},
/**
* When we are on Desktop and the button is visible
* we need to add a negative margin to the table
* to make it inline with the button
*
* @returns {Boolean}
*/
shouldAddNegativeMargin() {
return this.canRenderPipelineButton && bp.isDesktop();
},
},
created() {
this.service = new PipelinesService(this.endpoint);
......@@ -205,65 +194,76 @@ export default {
/>
<div v-else-if="shouldRenderTable" class="table-holder">
<div v-if="canRenderPipelineButton" class="nav justify-content-end">
<gl-button
variant="success"
class="js-run-mr-pipeline gl-mt-3 btn-wide-on-xs"
:disabled="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"
>
<gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline />
{{ s__('Pipelines|Run Pipeline') }}
</gl-button>
<gl-modal
:id="modalId"
ref="modal"
:modal-id="modalId"
:title="s__('Pipelines|Are you sure you want to run this pipeline?')"
:ok-title="s__('Pipelines|Run Pipeline')"
ok-variant="danger"
@ok="onClickRunPipeline"
>
<p>
{{
s__(
'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.',
)
}}
</p>
<p>
{{
s__(
"Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.",
)
}}
</p>
<p>
{{
s__(
'Pipelines|If you are unsure, please ask a project maintainer to review it for you.',
)
}}
</p>
<gl-link
href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
</div>
<gl-button
v-if="canRenderPipelineButton"
block
class="gl-mt-3 gl-mb-0 gl-display-md-none"
variant="success"
data-testid="run_pipeline_button_mobile"
:loading="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
</gl-button>
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
:class="{ 'negative-margin-top': shouldAddNegativeMargin }"
/>
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
<gl-button
variant="success"
data-testid="run_pipeline_button"
:loading="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
</gl-button>
</div>
</template>
</pipelines-table-component>
</div>
<gl-modal
v-if="canRenderPipelineButton"
:id="modalId"
ref="modal"
:modal-id="modalId"
:title="s__('Pipelines|Are you sure you want to run this pipeline?')"
:ok-title="s__('Pipelines|Run Pipeline')"
ok-variant="danger"
@ok="onClickRunPipeline"
>
<p>
{{
s__(
'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.',
)
}}
</p>
<p>
{{
s__(
"Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.",
)
}}
</p>
<p>
{{
s__('Pipelines|If you are unsure, please ask a project maintainer to review it for you.')
}}
</p>
<gl-link
href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
<table-pagination
v-if="shouldRenderPagination"
:change="onChangePage"
......
......@@ -32,7 +32,7 @@ export default {
<gl-icon :size="12" name="angle-down" class="position-absolute" />
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
<div class="dropdown-content" data-qa-selector="dropdown_content">
<ul>
<li v-for="version in versions" :key="version.id">
<a :class="{ 'is-active': version.selected }" :href="version.href">
......
......@@ -100,6 +100,7 @@ export default {
<compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown"
data-qa-selector="target_version_dropdown"
/>
</template>
<template #source>
......
......@@ -245,7 +245,14 @@ export default {
></strong>
</span>
<strong v-else v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
<strong
v-else
v-gl-tooltip
:title="filePath"
class="file-title-name"
data-container="body"
data-qa-selector="file_name_content"
>
{{ filePath }}
</strong>
</a>
......
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
GlIcon,
GlLoadingIcon,
},
props: {
......@@ -85,7 +86,7 @@ export default {
type="search"
class="dropdown-input-field qa-dropdown-filter-input"
/>
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" />
</div>
<div class="dropdown-content">
<gl-loading-icon v-if="showLoading" size="lg" />
......
......@@ -743,3 +743,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
return endDateInMS - startDateInMS;
};
/**
* A utility which returns a new date at the first day of the month for any given date.
*
* @param {Date} date
*
* @return {Date} the date at the first day of the month
*/
export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1));
/**
* A utility function which checks if two dates match.
*
* @param {Date|Int} date1 Can be either a date object or a unix timestamp.
* @param {Date|Int} date2 Can be either a date object or a unix timestamp.
*
* @return {Boolean} true if the dates match
*/
export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0;
......@@ -91,6 +91,10 @@ export default {
<div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader">
{{ s__('Pipeline|Stages') }}
</div>
<div class="table-section section-15" role="rowheader"></div>
<div class="table-section section-20" role="rowheader">
<slot name="table-header-actions"></slot>
</div>
</div>
<pipelines-table-row-component
v-for="model in pipelines"
......
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
},
props: {
user: {
type: Object,
required: true,
},
},
};
</script>
<template>
<button type="button" class="btn-link">
<reviewer-avatar :user="user" :img-size="24" />
<span class="author"> {{ user.name }} </span>
</button>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedReviewer from './collapsed_reviewer.vue';
const DEFAULT_MAX_COUNTER = 99;
const DEFAULT_RENDER_COUNT = 5;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CollapsedReviewer,
GlIcon,
},
props: {
users: {
type: Array,
required: true,
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
hasMoreThanOneReviewer() {
return this.users.length > 1;
},
hasMoreThanTwoReviewers() {
return this.users.length > 2;
},
allReviewersCanMerge() {
return this.users.every(user => user.can_merge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
return `${DEFAULT_MAX_COUNTER}+`;
}
return `+${this.users.length - 1}`;
},
collapsedUsers() {
const collapsedLength = this.hasMoreThanTwoReviewers ? 1 : this.users.length;
return this.users.slice(0, collapsedLength);
},
tooltipTitleMergeStatus() {
const mergeLength = this.users.filter(u => u.can_merge).length;
if (mergeLength === this.users.length) {
return '';
} else if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
});
}
return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
},
tooltipTitle() {
const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
const names = renderUsers.map(u => u.name);
if (!this.users.length) {
return __('Reviewer(s)');
}
if (this.users.length > names.length) {
names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
}
const text = names.join(', ');
return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
},
tooltipOptions() {
return { container: 'body', placement: 'left', boundary: 'viewport' };
},
},
};
</script>
<template>
<div
v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneReviewer }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
<gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
<button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i
v-if="!allReviewersCanMerge"
aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon"
></i>
</button>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
export default {
props: {
user: {
type: Object,
required: true,
},
imgSize: {
type: Number,
required: true,
},
},
computed: {
reviewerAlt() {
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
},
avatarUrl() {
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
},
hasMergeIcon() {
return !this.user.can_merge;
},
},
};
</script>
<template>
<span class="position-relative">
<img
:alt="reviewerAlt"
:src="avatarUrl"
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
data-qa-selector="avatar_image"
/>
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
</span>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
user: {
type: Object,
required: true,
},
rootPath: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
default: 'bottom',
required: false,
},
tooltipHasName: {
type: Boolean,
default: true,
required: false,
},
issuableType: {
type: String,
default: 'issue',
required: false,
},
},
computed: {
cannotMerge() {
return this.issuableType === 'merge_request' && !this.user.can_merge;
},
tooltipTitle() {
if (this.cannotMerge && this.tooltipHasName) {
return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
} else if (this.cannotMerge) {
return __('Cannot merge');
} else if (this.tooltipHasName) {
return this.user.name;
}
return '';
},
tooltipOption() {
return {
container: 'body',
placement: this.tooltipPlacement,
boundary: 'viewport',
};
},
reviewerUrl() {
return this.user.web_url;
},
},
};
</script>
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<gl-link
v-gl-tooltip="tooltipOption"
:href="reviewerUrl"
:title="tooltipTitle"
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlLoadingIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
name: 'ReviewerTitle',
components: {
GlLoadingIcon,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
numberOfReviewers: {
type: Number,
required: true,
},
editable: {
type: Boolean,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
reviewerTitle() {
const reviewers = this.numberOfReviewers;
return n__('Reviewer', `%d Reviewers`, reviewers);
},
},
};
</script>
<template>
<div class="title hide-collapsed">
{{ reviewerTitle }}
<gl-loading-icon v-if="loading" inline class="align-bottom" />
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
>
{{ __('Edit') }}
</a>
<a
v-if="showToggle"
:aria-label="__('Toggle sidebar')"
class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
</a>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import CollapsedReviewerList from './collapsed_reviewer_list.vue';
import UncollapsedReviewerList from './uncollapsed_reviewer_list.vue';
export default {
// name: 'Reviewers' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
CollapsedReviewerList,
UncollapsedReviewerList,
},
props: {
rootPath: {
type: String,
required: true,
},
users: {
type: Array,
required: true,
},
editable: {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
sortedReviewers() {
const canMergeUsers = this.users.filter(user => user.can_merge);
const canNotMergeUsers = this.users.filter(user => !user.can_merge);
return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
},
};
</script>
<template>
<div>
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
</span>
</template>
<uncollapsed-reviewer-list
v-else
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
/>
</div>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
import { __ } from '~/locale';
export default {
name: 'SidebarReviewers',
components: {
ReviewerTitle,
Reviewers,
},
mixins: [glFeatureFlagsMixin()],
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
issuableIid: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
data() {
return {
store: new Store(),
loading: false,
};
},
created() {
this.removeReviewer = this.store.removeReviewer.bind(this.store);
this.addReviewer = this.store.addReviewer.bind(this.store);
this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
// Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeReviewer', this.removeReviewer);
eventHub.$on('sidebar.addReviewer', this.addReviewer);
eventHub.$on('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$on('sidebar.saveReviewers', this.saveReviewers);
},
beforeDestroy() {
eventHub.$off('sidebar.removeReviewer', this.removeReviewer);
eventHub.$off('sidebar.addReviewer', this.addReviewer);
eventHub.$off('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
},
methods: {
saveReviewers() {
this.loading = true;
this.mediator
.saveReviewers(this.field)
.then(() => {
this.loading = false;
// Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922
// refreshUserMergeRequestCounts();
})
.catch(() => {
this.loading = false;
return new Flash(__('Error occurred when saving reviewers'));
});
},
},
};
</script>
<template>
<div>
<reviewer-title
:number-of-reviewers="store.reviewers.length"
:loading="loading || store.isFetching.reviewers"
:editable="store.editable"
:show-toggle="!signedIn"
/>
<reviewers
v-if="!store.isFetching.reviewers"
:root-path="store.rootPath"
:users="store.reviewers"
:editable="store.editable"
:issuable-type="issuableType"
class="value"
/>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
ReviewerAvatarLink,
},
props: {
users: {
type: Array,
required: true,
},
rootPath: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
data() {
return {
showLess: true,
};
},
computed: {
firstUser() {
return this.users[0];
},
hasOneUser() {
return this.users.length === 1;
},
hiddenReviewersLabel() {
const { numberOfHiddenReviewers } = this;
return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
},
renderShowMoreSection() {
return this.users.length > DEFAULT_RENDER_COUNT;
},
numberOfHiddenReviewers() {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
},
username() {
return `@${this.firstUser.username}`;
},
},
methods: {
toggleShowLess() {
this.showLess = !this.showLess;
},
},
};
</script>
<template>
<reviewer-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="ml-2">
<span class="author"> {{ user.name }} </span>
<span class="username"> {{ username }} </span>
</div>
</reviewer-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button
type="button"
class="btn-link"
data-qa-selector="more_reviewers_link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenReviewersLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
</div>
</div>
</template>
......@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
......@@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) {
});
}
function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return;
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
SidebarReviewers,
},
render: createElement =>
createElement('sidebar-reviewers', {
props: {
mediator,
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
}
export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels');
......@@ -245,6 +276,7 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
......
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
placeholderText: {
type: String,
......@@ -41,5 +45,6 @@ export default {
autocomplete="off"
/>
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" data-hidden="true" />
</div>
</template>
......@@ -230,13 +230,12 @@ export default {
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
:class="{
hidden: showClearInputButton,
}"
<gl-icon
name="search"
class="dropdown-input-search"
:class="{ hidden: showClearInputButton }"
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
/>
<gl-icon
name="close"
class="dropdown-input-clear"
......
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
......@@ -39,9 +40,9 @@ export default {
...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
visibleLabels() {
if (this.searchKey) {
return this.labels.filter(label =>
label.title.toLowerCase().includes(this.searchKey.toLowerCase()),
);
return fuzzaldrinPlus.filter(this.labels, this.searchKey, {
key: ['title'],
});
}
return this.labels;
},
......
......@@ -417,12 +417,6 @@
}
}
@include media-breakpoint-down(xs) {
.btn-wide-on-xs {
width: 100%;
}
}
.btn-blank {
padding: 0;
background: transparent;
......
......@@ -819,7 +819,6 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
$pipelines-table-header-height: 40px;
/*
CI variable lists
......
......@@ -431,10 +431,6 @@
margin-left: 0;
border-left: 0;
}
.file-actions .dropdown {
height: 28px;
}
}
table.code {
......
......@@ -117,7 +117,8 @@
}
}
.assignee {
.assignee,
.reviewer {
.merge-icon {
color: $orange-400;
position: absolute;
......
......@@ -26,10 +26,6 @@
}
.pipelines {
.negative-margin-top {
margin-top: -$pipelines-table-header-height;
}
.stage {
max-width: 90px;
width: 90px;
......
......@@ -21,11 +21,13 @@ module MultipleBoardsActions
end
def create
board = Boards::CreateService.new(parent, current_user, board_params).execute
response = Boards::CreateService.new(parent, current_user, board_params).execute
respond_to do |format|
format.json do
if board.persisted?
board = response.payload
if response.success?
extra_json = { board_path: board_path(board) }
render json: serialize_as_json(board).merge(extra_json)
else
......
......@@ -17,9 +17,8 @@ class GroupMembersFinder < UnionFinder
@params = params
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(include_relations: [:inherited, :direct])
group_members = group.members
group_members = group_members_list
relations = []
return group_members if include_relations == [:direct]
......@@ -27,17 +26,13 @@ class GroupMembersFinder < UnionFinder
relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.ancestors.select(:id))
.where.not(user_id: group.users.select(:id))
parents_members = relation_group_members(group.ancestors)
relations << parents_members
end
if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.descendants.select(:id))
.where.not(user_id: group.users.select(:id))
descendant_members = relation_group_members(group.descendants)
relations << descendant_members
end
......@@ -47,7 +42,6 @@ class GroupMembersFinder < UnionFinder
members = find_union(relations, GroupMember)
filter_members(members)
end
# rubocop: enable CodeReuse/ActiveRecord
private
......@@ -67,6 +61,22 @@ class GroupMembersFinder < UnionFinder
def can_manage_members
Ability.allowed?(user, :admin_group_member, group)
end
def group_members_list
group.members
end
def relation_group_members(relation)
all_group_members(relation).non_minimal_access
end
# rubocop: disable CodeReuse/ActiveRecord
def all_group_members(relation)
GroupMember.non_request
.where(source_id: relation.select(:id))
.where.not(user_id: group.users.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
......@@ -4,6 +4,7 @@ module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
prepend Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription
prepend ::Gitlab::Graphql::GlobalIDCompatibility
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
......
......@@ -3,13 +3,18 @@
module Mutations
module Ci
class Base < BaseMutation
argument :id, ::Types::GlobalIDType[::Ci::Pipeline],
PipelineID = ::Types::GlobalIDType[::Ci::Pipeline]
argument :id, PipelineID,
required: true,
description: 'The id of the pipeline to mutate'
private
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = PipelineID.coerce_isolated_input(id)
GlobalID::Locator.locate(id)
end
end
......
......@@ -29,11 +29,18 @@ module Mutations
private
def parameters(**args)
args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash|
args.transform_values { |id| find_design(id) }.transform_values(&:sync).tap do |hash|
hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) }
end
end
def find_design(id)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = DesignID.coerce_isolated_input(id)
GitlabSchema.object_from_id(id)
end
def not_found(gid)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}"
end
......
......@@ -4,6 +4,7 @@ module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility
def self.single
@single ||= Class.new(self) do
......
# frozen_string_literal: true
module GraphQLExtensions
module ScalarExtensions
# Allow ID to unify with GlobalID Types
def ==(other)
if name == 'ID' && other.is_a?(self.class) &&
other.type_class.ancestors.include?(::Types::GlobalIDType)
return true
end
super
end
end
end
::GraphQL::ScalarType.prepend(GraphQLExtensions::ScalarExtensions)
module Types
class GlobalIDType < BaseScalar
graphql_name 'GlobalID'
......
......@@ -49,8 +49,7 @@ module Types
field :milestone, ::Types::MilestoneType,
null: true,
description: 'Find a milestone',
resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do
description: 'Find a milestone' do
argument :id, ::Types::GlobalIDType[Milestone],
required: true,
description: 'Find a milestone by its ID'
......@@ -86,7 +85,17 @@ module Types
end
def issue(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Issue].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def milestone(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
......
......@@ -10,7 +10,7 @@ module Groups::GroupMembersHelper
end
def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level
end
def linked_groups_data_json(group_links)
......
......@@ -356,6 +356,7 @@ class Group < Namespace
end
group_hierarchy_members = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: source_ids)
GroupMember.from_union([group_hierarchy_members,
......@@ -550,6 +551,14 @@ class Group < Namespace
owners.first || parent&.default_owner || owner
end
def access_level_roles
GroupMember.access_level_roles
end
def access_level_values
access_level_roles.values
end
private
def update_two_factor_requirement
......
......@@ -132,6 +132,8 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
......
......@@ -3,7 +3,11 @@
module Boards
class CreateService < Boards::BaseService
def execute
create_board! if can_create_board?
unless can_create_board?
return ServiceResponse.error(message: "You don't have the permission to create a board for this resource.")
end
create_board!
end
private
......@@ -15,12 +19,16 @@ module Boards
def create_board!
board = parent.boards.create(params)
if board.persisted?
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :closed)
unless board.persisted?
return ServiceResponse.error(message: "There was an error when creating a board.", payload: board)
end
board.tap do |created_board|
created_board.lists.create(list_type: :backlog)
created_board.lists.create(list_type: :closed)
end
board
ServiceResponse.success(payload: board)
end
end
end
......
......@@ -113,7 +113,7 @@
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.gl-mt-3
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr
= button_tag _('Add users to group'), class: "btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
......
......@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
......@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- if Feature.enabled?(:merge_request_reviewers, @project) && reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone]
......
---
title: Add No Access Role for top group members
merge_request: 40942
author:
type: added
---
title: Don't expose http_request_duration_seconds metrics in sidekiq exporter
merge_request: 43941
author:
type: performance
---
title: Add fuzzy search support to labels dropdown
merge_request: 43969
author:
type: fixed
---
title: Add index for project_id and sha to deployments table
merge_request: 43836
author:
type: performance
---
title: Replace fa-search fontawesome icons with GitLab SVG in Vue components
merge_request: 43879
author:
type: changed
---
name: minimal_access_role
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942
rollout_issue_url:
group: group::access
type: licensed
default_enabled: true
......@@ -69,7 +69,9 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics.gauge(:deployments, 'GitLab Version', {}, :max).set({ version: Gitlab::VERSION }, 1)
Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds
unless Gitlab::Runtime.sidekiq?
Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds
end
rescue IOError => e
Gitlab::ErrorTracking.track_exception(e)
Gitlab::Metrics.error_detected!
......
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexOnProjectIdAndShaToDeployments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_deployments_on_project_id_sha'
disable_ddl_transaction!
def up
add_concurrent_index :deployments, [:project_id, :sha], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:deployments, INDEX_NAME)
end
end
45530bb3090d9e8df3a79f42a06b042e0c40f6e185078c6d79d7ec334175c7d5
\ No newline at end of file
......@@ -20007,6 +20007,8 @@ CREATE INDEX index_deployments_on_project_id_and_status_and_created_at ON deploy
CREATE INDEX index_deployments_on_project_id_and_updated_at_and_id ON deployments USING btree (project_id, updated_at DESC, id DESC);
CREATE INDEX index_deployments_on_project_id_sha ON deployments USING btree (project_id, sha);
CREATE INDEX index_deployments_on_user_id_and_status_and_created_at ON deployments USING btree (user_id, status, created_at);
CREATE INDEX index_description_versions_on_epic_id ON description_versions USING btree (epic_id) WHERE (epic_id IS NOT NULL);
......
......@@ -118,7 +118,7 @@ DRIs:
|------------------------------|------------------------|
| Product | Jackie Porter |
| Leadership | Daniel Croft |
| Engineering | TBD |
| Engineering | Kamil Trzciński |
Domain Experts:
......
......@@ -609,8 +609,8 @@ then available as environment variables on the running application
container.
CAUTION: **Caution:**
Variables with multi-line values are not currently supported due to
limitations with the current Auto DevOps scripting environment.
Variables with multi-line values are not supported due to
limitations with the Auto DevOps scripting environment.
### Override a variable by manually running a pipeline
......@@ -785,7 +785,7 @@ Examples:
##### Enable or disable parenthesis support for variables **(CORE ONLY)**
The feature is currently deployed behind a feature flag that is **enabled by default**.
The feature is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it for your instance.
......@@ -820,8 +820,7 @@ NOTE: **Note:**
The available regular expression syntax is limited. See [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/35438)
for more details.
If needed, you can use a test pipeline to determine whether a regular expression will
work in a variable. The example below tests the `^mast.*` regular expression directly,
If needed, you can use a test pipeline to determine whether a regular expression works in a variable. The example below tests the `^mast.*` regular expression directly,
as well as from within a variable:
```yaml
......
......@@ -49,6 +49,20 @@ See also:
- [Exposing Global IDs](#exposing-global-ids).
- [Mutation arguments](#object-identifier-arguments).
We have a custom scalar type (`Types::GlobalIDType`) which should be used as the
type of input and output arguments when the value is a `GlobalID`. The benefits
of using this type instead of `ID` are:
- it validates that the value is a `GlobalID`
- it parses it into a `GlobalID` before passing it to user code
- it can be parameterized on the type of the object (e.g.
`GlobalIDType[Project]`) which offers even better validation and security.
Consider using this type for all new arguments and result types. Remember that
it is perfectly possible to parameterize this type with a concern or a
supertype, if you want to accept a wider range of objects (e.g.
`GlobalIDType[Issuable]` vs `GlobalIDType[Issue]`).
## Types
We use a code-first schema, and we declare what type everything is in Ruby.
......
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment