Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-24 15:09:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-24 15:09:37 +0300
commit6f15c2c2723dbe14e907379b75201e2ef70a48d3 (patch)
treef917af54164003d021a4af518ced7271978c5537 /app
parent7a8d983c19c9fe14e7c0b8b6256b8cbacbff1959 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue193
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue2
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue99
-rw-r--r--app/assets/javascripts/diffs/store/getters.js48
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue105
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue2
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql26
-rw-r--r--app/assets/javascripts/snippets/index.js2
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js11
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql10
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.query.graphql12
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue55
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss19
-rw-r--r--app/assets/stylesheets/pages/notes.scss50
-rw-r--r--app/controllers/boards/lists_controller.rb2
-rw-r--r--app/controllers/concerns/wiki_actions.rb2
-rw-r--r--app/graphql/mutations/boards/lists/destroy.rb41
-rw-r--r--app/graphql/resolvers/snippets/blobs_resolver.rb40
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/snippet_type.rb5
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/models/wiki.rb2
-rw-r--r--app/models/wiki_directory.rb39
-rw-r--r--app/models/wiki_page.rb23
-rw-r--r--app/presenters/snippet_presenter.rb10
-rw-r--r--app/services/boards/lists/destroy_service.rb8
-rw-r--r--app/services/spam/spam_action_service.rb2
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/shared/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml4
36 files changed, 451 insertions, 408 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index dbc7ff67d9d..e77120fd12a 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -64,6 +64,7 @@ const Api = {
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
+ usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -686,6 +687,19 @@ const Api = {
return axios.post(url, freezePeriod);
},
+ trackRedisHllUserEvent(event) {
+ if (!gon.features?.usageDataApi) {
+ return null;
+ }
+
+ const url = Api.buildUrl(this.usageDataIncrementUniqueUsersPath);
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ return axios.post(url, { event }, { headers });
+ },
+
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d3fb3d82247..cea98d6dcd3 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -2,32 +2,36 @@
import { escape } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import {
- GlDeprecatedButton,
GlTooltipDirective,
GlSafeHtmlDirective,
- GlLoadingIcon,
GlIcon,
GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
} from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
-import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
export default {
components: {
- GlLoadingIcon,
- GlDeprecatedButton,
ClipboardButton,
- EditButton,
GlIcon,
FileIcon,
DiffStats,
GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -70,7 +74,7 @@ export default {
},
data() {
return {
- hasDropdownOpen: false,
+ moreActionsShown: false,
};
},
computed: {
@@ -155,6 +159,13 @@ export default {
}
return s__('MRDiff|Show full file');
},
+ showEditButton() {
+ return (
+ this.diffFile.blob?.readable_text &&
+ !this.diffFile.deleted_file &&
+ (this.diffFile.edit_path || this.diffFile.ide_edit_path)
+ );
+ },
},
methods: {
...mapActions('diffs', [
@@ -166,8 +177,11 @@ export default {
handleToggleFile() {
this.$emit('toggleFile');
},
- showForkMessage() {
- this.$emit('showForkMessage');
+ showForkMessage(e) {
+ if (this.canCurrentUserFork && !this.diffFile.can_modify_blob) {
+ e.preventDefault();
+ this.$emit('showForkMessage');
+ }
},
handleFileNameClick(e) {
const isLinkToOtherPage =
@@ -183,8 +197,8 @@ export default {
}
}
},
- setDropdownOpen(val) {
- this.hasDropdownOpen = val;
+ setMoreActionsShown(val) {
+ this.moreActionsShown = val;
},
},
};
@@ -193,8 +207,8 @@ export default {
<template>
<div
ref="header"
+ :class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
class="js-file-title file-title file-title-flex-parent"
- :class="{ 'gl-z-dropdown-menu!': hasDropdownOpen }"
@click.self="handleToggleFile"
>
<div class="file-header-content">
@@ -255,96 +269,95 @@ export default {
<div
v-if="!diffFile.submodule && addMergeRequestButtons"
- class="file-actions d-none d-sm-flex align-items-center flex-wrap"
+ class="file-actions d-flex align-items-center flex-wrap"
>
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
- <div class="btn-group" role="group">
- <template v-if="diffFile.blob && diffFile.blob.readable_text">
- <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
- <gl-deprecated-button
- ref="toggleDiscussionsButton"
- :disabled="!diffHasDiscussions(diffFile)"
- :class="{ active: diffHasExpandedDiscussions(diffFile) }"
- class="js-btn-vue-toggle-comments btn"
- data-qa-selector="toggle_comments_button"
- data-track-event="click_toggle_comments_button"
- data-track-label="diff_toggle_comments_button"
- data-track-property="diff_toggle_comments"
- type="button"
- @click="toggleFileDiscussionWrappers(diffFile)"
- >
- <gl-icon name="comment" />
- </gl-deprecated-button>
- </span>
-
- <edit-button
- v-if="!diffFile.deleted_file"
- :can-current-user-fork="canCurrentUserFork"
- :edit-path="diffFile.edit_path"
- :ide-edit-path="diffFile.ide_edit_path"
- :can-modify-blob="diffFile.can_modify_blob"
- data-track-event="click_toggle_edit_button"
- data-track-label="diff_toggle_edit_button"
- data-track-property="diff_toggle_edit"
- @showForkMessage="showForkMessage"
- @open="setDropdownOpen(true)"
- @close="setDropdownOpen(false)"
- />
- </template>
-
- <a
- v-if="diffFile.replaced_view_path"
- ref="replacedFileButton"
- v-safe-html="viewReplacedFileButtonText"
- :href="diffFile.replaced_view_path"
- class="btn view-file"
- >
- </a>
- <gl-deprecated-button
- v-if="!diffFile.is_fully_expanded"
- ref="expandDiffToFullFileButton"
- v-gl-tooltip.hover
- :title="expandDiffToFullFileTitle"
- class="expand-file"
- data-track-event="click_toggle_view_full_button"
- data-track-label="diff_toggle_view_full_button"
- data-track-property="diff_toggle_view_full"
- @click="toggleFullDiff(diffFile.file_path)"
- >
- <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
- <gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
- <gl-icon v-else name="doc-expand" />
- </gl-deprecated-button>
- <gl-deprecated-button
- ref="viewButton"
- v-gl-tooltip.hover
- :href="diffFile.view_path"
- target="_blank"
- class="view-file"
- data-track-event="click_toggle_view_sha_button"
- data-track-label="diff_toggle_view_sha_button"
- data-track-property="diff_toggle_view_sha"
- :title="viewFileButtonText"
- >
- <gl-icon name="doc-text" />
- </gl-deprecated-button>
-
- <a
+ <gl-button-group class="gl-pt-0!">
+ <gl-button
v-if="diffFile.external_url"
ref="externalLink"
v-gl-tooltip.hover
:href="diffFile.external_url"
:title="`View on ${diffFile.formatted_external_url}`"
target="_blank"
- rel="noopener noreferrer"
data-track-event="click_toggle_external_button"
data-track-label="diff_toggle_external_button"
data-track-property="diff_toggle_external"
- class="btn btn-file-option"
+ icon="external-link"
+ />
+ <gl-dropdown
+ v-gl-tooltip.hover.focus="__('More actions')"
+ right
+ toggle-class="btn-icon js-diff-more-actions"
+ class="gl-pt-0!"
+ @show="setMoreActionsShown(true)"
+ @hidden="setMoreActionsShown(false)"
>
- <gl-icon name="external-link" />
- </a>
- </div>
+ <template #button-content>
+ <gl-icon name="ellipsis_v" class="mr-0" />
+ <span class="sr-only">{{ __('More actions') }}</span>
+ </template>
+ <gl-dropdown-section-header>
+ {{ __('More actions') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-if="diffFile.replaced_view_path"
+ ref="replacedFileButton"
+ v-safe-html="viewReplacedFileButtonText"
+ :href="diffFile.replaced_view_path"
+ target="_blank"
+ />
+ <gl-dropdown-item ref="viewButton" :href="diffFile.view_path" target="_blank">
+ {{ viewFileButtonText }}
+ </gl-dropdown-item>
+ <template v-if="showEditButton">
+ <gl-dropdown-item
+ v-if="diffFile.edit_path"
+ ref="editButton"
+ :href="diffFile.edit_path"
+ class="js-edit-blob"
+ @click="showForkMessage"
+ >
+ {{ __('Edit in single-file editor') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="diffFile.edit_path"
+ ref="ideEditButton"
+ :href="diffFile.ide_edit_path"
+ class="js-ide-edit-blob"
+ >
+ {{ __('Edit in Web IDE') }}
+ </gl-dropdown-item>
+ </template>
+
+ <template v-if="!diffFile.viewer.collapsed">
+ <gl-dropdown-divider
+ v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
+ />
+
+ <gl-dropdown-item
+ v-if="diffHasDiscussions(diffFile)"
+ ref="toggleDiscussionsButton"
+ data-qa-selector="toggle_comments_button"
+ @click="toggleFileDiscussionWrappers(diffFile)"
+ >
+ <template v-if="diffHasExpandedDiscussions(diffFile)">
+ {{ __('Hide comments on this file') }}
+ </template>
+ <template v-else>
+ {{ __('Show comments on this file') }}
+ </template>
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="!diffFile.is_fully_expanded"
+ ref="expandDiffToFullFileButton"
+ @click="toggleFullDiff(diffFile.file_path)"
+ >
+ {{ expandDiffToFullFileTitle }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ </gl-button-group>
</div>
<div
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 05fbbd39fae..f229fc4cf60 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -42,7 +42,7 @@ export default {
class="diff-stats"
:class="{
'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
- 'd-inline-flex': !isCompareVersionsHeader,
+ 'd-none d-sm-inline-flex': !isCompareVersionsHeader,
}"
>
<div v-if="hasDiffFiles" class="diff-stats-group">
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
deleted file mode 100644
index a0d66ae2994..00000000000
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<script>
-import { uniqueId } from 'lodash';
-import {
- GlTooltipDirective,
- GlIcon,
- GlDeprecatedDropdown as GlDropdown,
- GlDeprecatedDropdownItem as GlDropdownItem,
-} from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownItem,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- editPath: {
- type: String,
- required: false,
- default: '',
- },
- ideEditPath: {
- type: String,
- required: false,
- default: '',
- },
- canCurrentUserFork: {
- type: Boolean,
- required: true,
- },
- canModifyBlob: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return { tooltipId: uniqueId('edit_button_tooltip_') };
- },
- computed: {
- tooltipTitle() {
- if (this.isDisabled) {
- return __("Can't edit as source branch was deleted");
- }
-
- return __('Edit file in...');
- },
- isDisabled() {
- return !this.editPath;
- },
- },
- methods: {
- handleShow(evt) {
- // We must hide the tooltip because it is redundant and doesn't close itself
- // when dropdown opens because we are still "focused".
- this.$root.$emit('bv::hide::tooltip', this.tooltipId);
-
- if (this.canCurrentUserFork && !this.canModifyBlob) {
- evt.preventDefault();
- this.$emit('showForkMessage');
- } else {
- this.$emit('open');
- }
- },
- handleHide() {
- this.$emit('close');
- },
- },
-};
-</script>
-
-<template>
- <div v-gl-tooltip.top="{ title: tooltipTitle, id: tooltipId }" class="gl-display-flex">
- <gl-dropdown
- toggle-class="rounded-0"
- :disabled="isDisabled"
- :class="{ 'cursor-not-allowed': isDisabled }"
- right
- data-testid="edit_file"
- @show="handleShow"
- @hide="handleHide"
- >
- <template #button-content>
- <span class="gl-dropdown-toggle-text"><gl-icon name="pencil"/></span>
- <gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
- </template>
- <gl-dropdown-item v-if="editPath" :href="editPath">{{
- __('Edit in single-file editor')
- }}</gl-dropdown-item>
- <gl-dropdown-item v-if="ideEditPath" :href="ideEditPath">{{
- __('Edit in Web IDE')
- }}</gl-dropdown-item>
- </gl-dropdown>
- </div>
-</template>
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 42df5873a41..0a33f978f4f 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -46,15 +46,24 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
* @param {Object} diff
* @returns {Boolean}
*/
-export const diffHasExpandedDiscussions = (state, getters) => diff => {
- const discussions = getters.getDiffFileDiscussions(diff);
-
- return (
- (discussions &&
- discussions.length &&
- discussions.find(discussion => discussion.expanded) !== undefined) ||
- false
- );
+export const diffHasExpandedDiscussions = state => diff => {
+ const lines = {
+ [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
+ [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
+ if (line.left) {
+ acc.push(line.left);
+ }
+
+ if (line.right) {
+ acc.push(line.right);
+ }
+
+ return acc;
+ }, []),
+ };
+ return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType]
+ .filter(l => l.discussions.length >= 1)
+ .some(l => l.discussionsExpanded);
};
/**
@@ -62,8 +71,25 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
* @param {Boolean} diff
* @returns {Boolean}
*/
-export const diffHasDiscussions = (state, getters) => diff =>
- getters.getDiffFileDiscussions(diff).length > 0;
+export const diffHasDiscussions = state => diff => {
+ const lines = {
+ [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
+ [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
+ if (line.left) {
+ acc.push(line.left);
+ }
+
+ if (line.right) {
+ acc.push(line.right);
+ }
+
+ return acc;
+ }, []),
+ };
+ return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType].some(
+ l => l.discussions.length >= 1,
+ );
+};
/**
* Returns an array with the discussions of the given diff
diff --git a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
index 780b62c7938..b2aa5265331 100644
--- a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
+++ b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
@@ -1,11 +1,10 @@
<script>
-import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
- GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -26,16 +25,12 @@ export default {
<template>
<div
- class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
+ class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div class="gl-pr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
- <gl-sprintf :message="__('Alert #%{alertId}')">
- <template #alertId>
- <span>{{ alert.iid }}</span>
- </template>
- </gl-sprintf>
+ #{{ alert.iid }}
</gl-link>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index c6fab271376..2427a3f98ad 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,6 +1,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui';
+import { __ } from '~/locale';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
@@ -9,6 +10,8 @@ export default {
},
components: {
GlIcon,
+ GlButton,
+ GlButtonGroup,
},
mixins: [discussionNavigation],
computed: {
@@ -34,6 +37,12 @@ export default {
allExpanded() {
return this.toggeableDiscussions.every(discussion => discussion.expanded);
},
+ lineResolveClass() {
+ return this.allResolved ? 'line-resolve-btn is-active' : 'line-resolve-text';
+ },
+ toggleThreadsLabel() {
+ return this.allExpanded ? __('Collapse all threads') : __('Expand all threads');
+ },
},
methods: {
...mapActions(['setExpandDiscussions']),
@@ -51,59 +60,49 @@ export default {
<div
v-if="resolvableDiscussionsCount > 0"
ref="discussionCounter"
- class="line-resolve-all-container full-width-mobile"
+ class="line-resolve-all-container full-width-mobile gl-display-flex d-sm-flex"
>
- <div class="full-width-mobile d-flex d-sm-flex">
- <div class="line-resolve-all">
- <span
- :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
- >
- <template v-if="allResolved">
- <gl-icon name="check-circle-filled" />
- {{ __('All threads resolved') }}
- </template>
- <template v-else>
- {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
- </template>
- </span>
- </div>
- <div
- v-if="resolveAllDiscussionsIssuePath && !allResolved"
- class="btn-group btn-group-sm"
- role="group"
- >
- <a
- v-gl-tooltip
- :href="resolveAllDiscussionsIssuePath"
- :title="s__('Resolve all threads in new issue')"
- class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
- >
- <gl-icon name="issue-new" />
- </a>
- </div>
- <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
- <button
- v-gl-tooltip
- :title="__('Jump to next unresolved thread')"
- class="btn btn-default discussion-next-btn"
- data-track-event="click_button"
- data-track-label="mr_next_unresolved_thread"
- data-track-property="click_next_unresolved_thread_top"
- @click="jumpToNextDiscussion"
- >
- <gl-icon name="comment-next" />
- </button>
- </div>
- <div class="btn-group btn-group-sm" role="group">
- <button
- v-gl-tooltip
- :title="__('Toggle all threads')"
- class="btn btn-default toggle-all-discussions-btn"
- @click="handleExpandDiscussions"
- >
- <gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
- </button>
- </div>
+ <div class="line-resolve-all">
+ <span :class="lineResolveClass">
+ <template v-if="allResolved">
+ <gl-icon name="check-circle-filled" />
+ {{ __('All threads resolved') }}
+ </template>
+ <template v-else>
+ {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
+ </template>
+ </span>
</div>
+ <gl-button-group>
+ <gl-button
+ v-if="resolveAllDiscussionsIssuePath && !allResolved"
+ v-gl-tooltip
+ :href="resolveAllDiscussionsIssuePath"
+ :title="s__('Resolve all threads in new issue')"
+ :aria-label="s__('Resolve all threads in new issue')"
+ class="new-issue-for-discussion discussion-create-issue-btn"
+ icon="issue-new"
+ />
+ <gl-button
+ v-if="isLoggedIn && !allResolved"
+ v-gl-tooltip
+ :title="__('Jump to next unresolved thread')"
+ :aria-label="__('Jump to next unresolved thread')"
+ class="discussion-next-btn"
+ data-track-event="click_button"
+ data-track-label="mr_next_unresolved_thread"
+ data-track-property="click_next_unresolved_thread_top"
+ icon="comment-next"
+ @click="jumpToNextDiscussion"
+ />
+ <gl-button
+ v-gl-tooltip
+ :title="toggleThreadsLabel"
+ :aria-label="toggleThreadsLabel"
+ class="toggle-all-discussions-btn"
+ :icon="allExpanded ? 'angle-up' : 'angle-down'"
+ @click="handleExpandDiscussions"
+ />
+ </gl-button-group>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 0143770ed25..c2f40b2d21a 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -160,7 +160,7 @@ export default {
});
},
displayMemberBadgeText() {
- return sprintf(__('This user is a %{access} of the %{name} project.'), {
+ return sprintf(__('This user has the %{access} role in the %{name} project.'), {
access: this.accessLevel.toLowerCase(),
name: this.projectName,
});
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 1a539aa0876..e15aa10bd81 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -151,7 +151,7 @@ export default {
this.newSnippet = false;
},
onSnippetFetch(snippetRes) {
- if (snippetRes.data.snippets.edges.length === 0) {
+ if (snippetRes.data.snippets.nodes.length === 0) {
this.onNewSnippetFetched();
} else {
this.onExistingSnippetFetched();
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index b38be5bb9a4..e88126ea56a 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -23,6 +23,7 @@ export default {
return {
ids: this.snippet.id,
rich: this.activeViewerType === RICH_BLOB_VIEWER,
+ paths: [this.blob.path],
};
},
update(data) {
@@ -79,8 +80,10 @@ export default {
},
onContentUpdate(data) {
const { path: blobPath } = this.blob;
- const { blobs } = data.snippets.edges[0].node;
- const updatedBlobData = blobs.find(blob => blob.path === blobPath);
+ const {
+ blobs: { nodes: dataBlobs },
+ } = data.snippets.nodes[0];
+ const updatedBlobData = dataBlobs.find(blob => blob.path === blobPath);
return updatedBlobData.richData || updatedBlobData.plainData;
},
},
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 0ca69f3161a..30de5a9d0e0 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -18,6 +18,7 @@ import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
+import { fetchPolicies } from '~/lib/graphql';
export default {
components: {
@@ -37,6 +38,7 @@ export default {
},
apollo: {
canCreateSnippet: {
+ fetchPolicy: fetchPolicies.NO_CACHE,
query() {
return this.snippet.project ? CanCreateProjectSnippet : CanCreatePersonalSnippet;
},
diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
index 2cca71708ca..d75b4011d1c 100644
--- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
+++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
@@ -12,18 +12,20 @@ fragment SnippetBase on Snippet {
httpUrlToRepo
sshUrlToRepo
blobs {
- binary
- name
- path
- rawPath
- size
- externalStorage
- renderedAsText
- simpleViewer {
- ...BlobViewer
- }
- richViewer {
- ...BlobViewer
+ nodes {
+ binary
+ name
+ path
+ rawPath
+ size
+ externalStorage
+ renderedAsText
+ simpleViewer {
+ ...BlobViewer
+ }
+ richViewer {
+ ...BlobViewer
+ }
}
}
userPermissions {
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index c70ad9b95f8..16635a081a2 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -16,7 +16,7 @@ function appFactory(el, Component) {
}
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { batchMax: 1 }),
});
const {
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 15daaa8d84a..d5e69e2a889 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -11,9 +11,16 @@ export const getSnippetMixin = {
ids: this.snippetGid,
};
},
- update: data => data.snippets.edges[0]?.node,
+ update: data => {
+ const res = data.snippets.nodes[0];
+ if (res) {
+ res.blobs = res.blobs.nodes;
+ }
+
+ return res;
+ },
result(res) {
- this.blobs = res.data.snippets.edges[0]?.node?.blobs || blobsDefault;
+ this.blobs = res.data.snippets.nodes[0]?.blobs || blobsDefault;
if (this.onSnippetFetch) {
this.onSnippetFetch(res);
}
diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
index 8f1f16b76c2..0e04ee9b7b8 100644
--- a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
+++ b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
@@ -1,9 +1,9 @@
-query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
+query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
snippets(ids: $ids) {
- edges {
- node {
- id
- blobs {
+ nodes {
+ id
+ blobs(paths: $paths) {
+ nodes {
path
richData @include(if: $rich)
plainData @skip(if: $rich)
diff --git a/app/assets/javascripts/snippets/queries/snippet.query.graphql b/app/assets/javascripts/snippets/queries/snippet.query.graphql
index b23ab862439..2f385050d89 100644
--- a/app/assets/javascripts/snippets/queries/snippet.query.graphql
+++ b/app/assets/javascripts/snippets/queries/snippet.query.graphql
@@ -4,13 +4,11 @@
query GetSnippetQuery($ids: [ID!]) {
snippets(ids: $ids) {
- edges {
- node {
- ...SnippetBase
- ...SnippetProject
- author {
- ...Author
- }
+ nodes {
+ ...SnippetBase
+ ...SnippetProject
+ author {
+ ...Author
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index 7e89fa2afe1..b90cc24387c 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -1,5 +1,5 @@
<script>
-import { escape } from 'lodash';
+import { escape, last } from 'lodash';
import Tribute from 'tributejs';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -12,6 +12,8 @@ const AutoComplete = {
MergeRequests: 'mergeRequests',
};
+const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
+
function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
const currentLine = fullText.split('\n')[currentLineNumber - 1];
@@ -79,30 +81,40 @@ const autoCompleteMap = {
return this.members;
},
menuItemTemplate({ original }) {
- const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
-
- const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
- gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
-
- const avatarTag = original.avatar_url
- ? `<img
- src="${original.avatar_url}"
- alt="${original.username}'s avatar"
- class="${avatarClasses}"/>`
- : `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
-
- const name = escape(original.name);
+ const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const noAvatarClasses = `${commonClasses} gl-rounded-small
+ gl-display-flex gl-align-items-center gl-justify-content-center`;
+
+ const avatar = original.avatar_url
+ ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
+ : `<div class="${noAvatarClasses}" aria-hidden="true">
+ ${original.username.charAt(0).toUpperCase()}</div>`;
+
+ let displayName = original.name;
+ let parentGroupOrUsername = `@${original.username}`;
+
+ if (original.type === groupType) {
+ const splitName = original.name.split(' / ');
+ displayName = splitName.pop();
+ parentGroupOrUsername = splitName.pop();
+ }
const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
- const icon = original.mentionsDisabled
- ? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
+ const disabledMentionsIcon = original.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 gl-ml-3')
: '';
- return `${avatarTag}
- ${original.username}
- <small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
- ${icon}`;
+ return `
+ <div class="gl-display-flex gl-align-items-center">
+ ${avatar}
+ <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div>${escape(displayName)}${count}</div>
+ <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
+ </div>
+ ${disabledMentionsIcon}
+ </div>
+ `;
},
},
[AutoComplete.MergeRequests]: {
@@ -139,7 +151,8 @@ export default {
{
trigger: '@',
fillAttr: 'username',
- lookup: value => value.name + value.username,
+ lookup: value =>
+ value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
values: this.getValues(AutoComplete.Members),
},
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 76c6e03377c..f8710cc1346 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -50,7 +50,7 @@
right: 15px;
margin-left: auto;
- .btn {
+ .btn:not(.btn-icon) {
padding: 0 10px;
font-size: 13px;
line-height: 28px;
@@ -372,7 +372,7 @@ span.idiff {
color: $gl-text-color;
}
- .file-actions .btn {
+ .file-actions .btn:not(.btn-icon) {
padding: 0 10px;
font-size: 13px;
line-height: 28px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 62af7103b39..fb13e776e13 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -13,6 +13,21 @@
box-shadow: 0 -2px 0 0 var(--white);
cursor: pointer;
+ .dropdown-menu {
+ cursor: auto;
+ }
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ .file-header-content {
+ width: 0;
+ flex: 1;
+ }
+
+ .file-actions {
+ margin-left: $gl-spacing-scale-2;
+ }
+ }
+
@media (min-width: map-get($grid-breakpoints, md)) {
// The `+11` is to ensure the file header border shows when scrolled -
// the bottom of the compare-versions header and the top of the file header
@@ -420,6 +435,10 @@
margin-left: 0;
border-left: 0;
}
+
+ .file-actions .dropdown {
+ height: 28px;
+ }
}
table.code {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index c144fb13322..b510822a20a 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -858,68 +858,28 @@ $note-form-margin-left: 72px;
}
.line-resolve-all-container {
- margin: $gl-padding-4;
-
> div {
white-space: nowrap;
}
- .discussion-next-btn {
- border-radius: 0;
- }
-
- .toggle-all-discussions-btn {
+ .btn-group .btn:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
-
- .btn {
- line-height: $gl-line-height;
-
- svg {
- fill: $gray-500;
- }
-
- &.discussion-create-issue-btn {
- border-radius: 0;
- border-right: 0;
-
- a {
- padding: 0;
- line-height: 0;
-
- &:hover {
- text-decoration: none;
- border: 0;
- }
- }
- }
-
- &.discussion-next-btn {
- border-right: 0;
- }
- }
}
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: $gl-padding-4 10px;
+ padding: $gl-padding-8 $gl-padding-12;
background-color: $gray-light;
border: 1px solid $border-color;
+ border-right: 0;
border-radius: $border-radius-default;
- font-size: $gl-btn-small-font-size;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
- border-right: 0;
-
- .line-resolve-btn {
- color: $gray-500;
-
- svg {
- vertical-align: text-top;
- }
- }
+ font-size: $gl-font-size;
+ line-height: 1rem;
@include media-breakpoint-down(xs) {
flex: 1;
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 0b8469e8290..4eb436e4ce7 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -42,7 +42,7 @@ module Boards
list = board.lists.destroyable.find(params[:id])
service = Boards::Lists::DestroyService.new(board_parent, current_user)
- if service.execute(list)
+ if service.execute(list).success?
head :ok
else
head :unprocessable_entity
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 5a5b634da40..4d1684ec3a2 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -44,7 +44,7 @@ module WikiActions
wiki.list_pages(sort: params[:sort], direction: params[:direction])
).page(params[:page])
- @wiki_entries = WikiPage.group_by_directory(@wiki_pages)
+ @wiki_entries = WikiDirectory.group_pages(@wiki_pages)
render 'shared/wikis/pages'
end
diff --git a/app/graphql/mutations/boards/lists/destroy.rb b/app/graphql/mutations/boards/lists/destroy.rb
new file mode 100644
index 00000000000..61ffae7c047
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/destroy.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class Destroy < ::Mutations::BaseMutation
+ graphql_name 'DestroyBoardList'
+
+ field :list,
+ Types::BoardListType,
+ null: true,
+ description: 'The list after mutation.'
+
+ argument :list_id, ::Types::GlobalIDType[::List],
+ required: true,
+ loads: Types::BoardListType,
+ description: 'Global ID of the list to destroy. Only label lists are accepted.'
+
+ def resolve(list:)
+ raise_resource_not_available_error! unless can_admin_list?(list)
+
+ response = ::Boards::Lists::DestroyService.new(list.board.resource_parent, current_user)
+ .execute(list)
+
+ {
+ list: response.success? ? nil : list,
+ errors: response.errors
+ }
+ end
+
+ private
+
+ def can_admin_list?(list)
+ return false unless list.present?
+
+ Ability.allowed?(current_user, :admin_list, list.board)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb
new file mode 100644
index 00000000000..dc28358cab6
--- /dev/null
+++ b/app/graphql/resolvers/snippets/blobs_resolver.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Snippets
+ class BlobsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ alias_method :snippet, :object
+
+ argument :paths, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Paths of the blobs'
+
+ def resolve(**args)
+ authorize!(snippet)
+
+ return [snippet.blob] if snippet.empty_repo?
+
+ paths = Array(args.fetch(:paths, []))
+
+ if paths.empty?
+ snippet.blobs
+ else
+ snippet.repository.blobs_at(transformed_blob_paths(paths))
+ end
+ end
+
+ def authorized_resource?(snippet)
+ Ability.allowed?(context[:current_user], :read_snippet, snippet)
+ end
+
+ private
+
+ def transformed_blob_paths(paths)
+ ref = snippet.default_branch
+ paths.map { |path| [ref, path] }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 3d41218a29e..3a9d5529266 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -18,6 +18,7 @@ module Types
mount_mutation Mutations::Boards::Issues::IssueMoveList
mount_mutation Mutations::Boards::Lists::Create
mount_mutation Mutations::Boards::Lists::Update
+ mount_mutation Mutations::Boards::Lists::Destroy
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index db98e62c10a..f29b513c26c 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -69,10 +69,11 @@ module Types
null: false,
deprecated: { reason: 'Use `blobs`', milestone: '13.3' }
- field :blobs, type: [Types::Snippets::BlobType],
+ field :blobs, type: Types::Snippets::BlobType.connection_type,
description: 'Snippet blobs',
calls_gitaly: true,
- null: false
+ null: true,
+ resolver: Resolvers::Snippets::BlobsResolver
field :ssh_url_to_repo, type: GraphQL::STRING_TYPE,
description: 'SSH URL to the snippet repository',
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index b255597b18d..f1030061690 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -206,7 +206,7 @@ module IssuablesHelper
end
if access = project.team.human_max_access(issuable.author_id)
- output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: project.name })
+ output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: project.name })
elsif project.team.contributor?(issuable.author_id)
output << content_tag(:span, _("Contributor"), class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3", title: _("This user has previously committed to the %{name} project.") % { name: project.name })
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 9462f7401c4..ac973f7f655 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -103,7 +103,7 @@ class Wiki
limited = pages.size > limit
pages = pages.first(limit) if limited
- [WikiPage.group_by_directory(pages), limited]
+ [WikiDirectory.group_pages(pages), limited]
end
# Finds a page within the repository based on a tile
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index df2fe25b08b..3a2613e15d9 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -3,13 +3,46 @@
class WikiDirectory
include ActiveModel::Validations
- attr_accessor :slug, :pages
+ attr_accessor :slug, :entries
validates :slug, presence: true
- def initialize(slug, pages = [])
+ # Groups a list of wiki pages into a nested collection of WikiPage and WikiDirectory objects,
+ # preserving the order of the passed pages.
+ #
+ # Returns an array with all entries for the toplevel directory.
+ #
+ # @param [Array<WikiPage>] pages
+ # @return [Array<WikiPage, WikiDirectory>]
+ #
+ def self.group_pages(pages)
+ # Build a hash to map paths to created WikiDirectory objects,
+ # and recursively create them for each level of the path.
+ # For the toplevel directory we use '' as path, as that's what WikiPage#directory returns.
+ directories = Hash.new do |_, path|
+ directories[path] = new(path).tap do |directory|
+ if path.present?
+ parent = File.dirname(path)
+ parent = '' if parent == '.'
+ directories[parent].entries << directory
+ end
+ end
+ end
+
+ pages.each do |page|
+ directories[page.directory].entries << page
+ end
+
+ directories[''].entries
+ end
+
+ def initialize(slug, entries = [])
@slug = slug
- @pages = pages
+ @entries = entries
+ end
+
+ def title
+ WikiPage.unhyphenize(File.basename(slug))
end
# Relative path to the partial to be used when rendering collections
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index faf3d19d936..989128987d5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -31,29 +31,6 @@ class WikiPage
alias_method :==, :eql?
- # Sorts and groups pages by directory.
- #
- # pages - an array of WikiPage objects.
- #
- # Returns an array of WikiPage and WikiDirectory objects. The entries are
- # sorted by alphabetical order (directories and pages inside each directory).
- # Pages at the root level come before everything.
- def self.group_by_directory(pages)
- return [] if pages.blank?
-
- pages.each_with_object([]) do |page, grouped_pages|
- next grouped_pages << page unless page.directory.present?
-
- directory = grouped_pages.find do |obj|
- obj.is_a?(WikiDirectory) && obj.slug == page.directory
- end
-
- next directory.pages << page if directory
-
- grouped_pages << WikiDirectory.new(page.directory, [page])
- end
- end
-
def self.unhyphenize(name)
name.gsub(/-+/, ' ')
end
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index d814c4404b6..695aa266e2c 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -32,15 +32,9 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
end
def blob
- blobs.first
- end
+ return snippet.blob if snippet.empty_repo?
- def blobs
- if snippet.empty_repo?
- [snippet.blob]
- else
- snippet.blobs
- end
+ blobs.first
end
private
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index e20805d0405..ebac0f07fe1 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -4,7 +4,9 @@ module Boards
module Lists
class DestroyService < Boards::BaseService
def execute(list)
- return false unless list.destroyable?
+ unless list.destroyable?
+ return ServiceResponse.error(message: "The list cannot be destroyed. Only label lists can be destroyed.")
+ end
@board = list.board
@@ -12,6 +14,8 @@ module Boards
decrement_higher_lists(list)
remove_list(list)
end
+
+ ServiceResponse.success
end
private
@@ -26,7 +30,7 @@ module Boards
# rubocop: enable CodeReuse/ActiveRecord
def remove_list(list)
- list.destroy
+ list.destroy!
end
end
end
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index b745b67f566..ab83fc401e9 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -45,7 +45,7 @@ module Spam
attr_reader :user, :context
def allowlisted?(user)
- user.respond_to?(:gitlab_employee) && user.gitlab_employee?
+ user.try(:gitlab_employee?) || user.try(:gitlab_bot?)
end
def perform_spam_service_check(api)
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 058366eb75d..a785e36fad5 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -2,7 +2,7 @@
- if note.noteable_author?(@noteable)
%span{ class: 'note-role user-access-role has-tooltip d-none d-md-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name } }= _("Author")
- if access
- %span{ class: 'note-role user-access-role has-tooltip', title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
+ %span{ class: 'note-role user-access-role has-tooltip', title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
- elsif note.contributor?
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
diff --git a/app/views/shared/wikis/_pages_wiki_page.html.haml b/app/views/shared/wikis/_pages_wiki_page.html.haml
index b56ae2bf9b1..fb6f58d044d 100644
--- a/app/views/shared/wikis/_pages_wiki_page.html.haml
+++ b/app/views/shared/wikis/_pages_wiki_page.html.haml
@@ -1,5 +1,5 @@
%li
- = link_to wiki_page.title, wiki_page_path(@wiki, wiki_page), data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.slug }
+ = link_to wiki_page.human_title, wiki_page_path(@wiki, wiki_page), data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.slug }
%small (#{wiki_page.format})
.float-right
- if wiki_page.last_version
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index 21e829d86a6..a492d1e5aa0 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,4 +1,4 @@
%li{ data: { qa_selector: 'wiki_directory_content' } }
- = wiki_directory.slug
+ = wiki_directory.title
%ul
- = render wiki_directory.pages, context: context
+ = render wiki_directory.entries, context: context