Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
67cdffe4de
commit
e2999d09ec
|
@ -1 +1 @@
|
|||
0fc3e28a00fe119679257707dadfb1b9e3354b28
|
||||
0cc0f3d488f96261608d7c06261be8a0cce0d668
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlIntersectionObserver } from '@gitlab/ui';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { humanize } from '~/lib/utils/text_utility';
|
||||
import EmojiGroup from './emoji_group.vue';
|
||||
|
||||
export default {
|
||||
|
@ -25,7 +25,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
categoryTitle() {
|
||||
return capitalizeFirstCharacter(this.category);
|
||||
return humanize(this.category);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -33,9 +33,6 @@ export default {
|
|||
this.renderGroup = true;
|
||||
this.$emit('appear', this.category);
|
||||
},
|
||||
categoryDissappeared() {
|
||||
this.renderGroup = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { findLastIndex } from 'lodash';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
import { CATEGORY_NAMES } from '~/emoji';
|
||||
import { CATEGORY_ICON_MAP } from '../constants';
|
||||
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
|
||||
import Category from './category.vue';
|
||||
import EmojiList from './emoji_list.vue';
|
||||
import { getEmojiCategories } from './utils';
|
||||
import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -25,13 +26,16 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
currentCategory: null,
|
||||
currentCategory: 0,
|
||||
searchValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
categoryNames() {
|
||||
return CATEGORY_NAMES.map((category) => ({
|
||||
return CATEGORY_NAMES.filter((c) => {
|
||||
if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis();
|
||||
return true;
|
||||
}).map((category) => ({
|
||||
name: category,
|
||||
icon: CATEGORY_ICON_MAP[category],
|
||||
}));
|
||||
|
@ -50,6 +54,7 @@ export default {
|
|||
selectEmoji(name) {
|
||||
this.$emit('click', name);
|
||||
this.$refs.dropdown.hide();
|
||||
addToFrequentlyUsed(name);
|
||||
},
|
||||
getBoundaryElement() {
|
||||
return document.querySelector('.content-wrapper') || 'scrollParent';
|
||||
|
@ -58,6 +63,11 @@ export default {
|
|||
this.$refs.virtualScoller.setScrollTop(0);
|
||||
this.$refs.virtualScoller.forceRender();
|
||||
},
|
||||
async onScroll(event, { offset }) {
|
||||
const categories = await getEmojiCategories();
|
||||
|
||||
this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -86,10 +96,10 @@ export default {
|
|||
class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
|
||||
>
|
||||
<button
|
||||
v-for="category in categoryNames"
|
||||
v-for="(category, index) in categoryNames"
|
||||
:key="category.name"
|
||||
:class="{
|
||||
'gl-text-black-normal! emoji-picker-category-active': category.name === currentCategory,
|
||||
'gl-text-black-normal! emoji-picker-category-active': index === currentCategory,
|
||||
}"
|
||||
type="button"
|
||||
class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-fill-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab"
|
||||
|
@ -100,18 +110,20 @@ export default {
|
|||
</div>
|
||||
<emoji-list :search-value="searchValue">
|
||||
<template #default="{ filteredCategories }">
|
||||
<virtual-list ref="virtualScoller" :size="258" :remain="1" :bench="2" variable>
|
||||
<virtual-list
|
||||
ref="virtualScoller"
|
||||
:size="258"
|
||||
:remain="1"
|
||||
:bench="2"
|
||||
variable
|
||||
:onscroll="onScroll"
|
||||
>
|
||||
<div
|
||||
v-for="(category, categoryKey) in filteredCategories"
|
||||
:key="categoryKey"
|
||||
:style="{ height: category.height + 'px' }"
|
||||
>
|
||||
<category
|
||||
:category="categoryKey"
|
||||
:emojis="category.emojis"
|
||||
@appear="categoryAppeared"
|
||||
@click="selectEmoji"
|
||||
/>
|
||||
<category :category="categoryKey" :emojis="category.emojis" @click="selectEmoji" />
|
||||
</div>
|
||||
</virtual-list>
|
||||
</template>
|
||||
|
|
|
@ -1,27 +1,68 @@
|
|||
import { chunk, memoize } from 'lodash';
|
||||
import Cookies from 'js-cookie';
|
||||
import { chunk, memoize, uniq } from 'lodash';
|
||||
import { initEmojiMap, getEmojiCategoryMap } from '~/emoji';
|
||||
import { EMOJIS_PER_ROW, EMOJI_ROW_HEIGHT, CATEGORY_ROW_HEIGHT } from '../constants';
|
||||
import {
|
||||
EMOJIS_PER_ROW,
|
||||
EMOJI_ROW_HEIGHT,
|
||||
CATEGORY_ROW_HEIGHT,
|
||||
FREQUENTLY_USED_KEY,
|
||||
FREQUENTLY_USED_COOKIE_KEY,
|
||||
} from '../constants';
|
||||
|
||||
export const generateCategoryHeight = (emojisLength) =>
|
||||
emojisLength * EMOJI_ROW_HEIGHT + CATEGORY_ROW_HEIGHT;
|
||||
|
||||
export const getFrequentlyUsedEmojis = () => {
|
||||
const savedEmojis = Cookies.get(FREQUENTLY_USED_COOKIE_KEY);
|
||||
|
||||
if (!savedEmojis) return null;
|
||||
|
||||
const emojis = chunk(uniq(savedEmojis.split(',')), 9);
|
||||
|
||||
return {
|
||||
frequently_used: {
|
||||
emojis,
|
||||
top: 0,
|
||||
height: generateCategoryHeight(emojis.length),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const addToFrequentlyUsed = (emoji) => {
|
||||
const frequentlyUsedEmojis = uniq(
|
||||
(Cookies.get(FREQUENTLY_USED_COOKIE_KEY) || '')
|
||||
.split(',')
|
||||
.filter((e) => e)
|
||||
.concat(emoji),
|
||||
);
|
||||
|
||||
Cookies.set(FREQUENTLY_USED_COOKIE_KEY, frequentlyUsedEmojis.join(','), { expires: 365 });
|
||||
};
|
||||
|
||||
export const hasFrequentlyUsedEmojis = () => getFrequentlyUsedEmojis() !== null;
|
||||
|
||||
export const getEmojiCategories = memoize(async () => {
|
||||
await initEmojiMap();
|
||||
|
||||
const categories = await getEmojiCategoryMap();
|
||||
let top = 0;
|
||||
const frequentlyUsedEmojis = getFrequentlyUsedEmojis();
|
||||
let top = frequentlyUsedEmojis
|
||||
? frequentlyUsedEmojis.frequently_used.top + frequentlyUsedEmojis.frequently_used.height
|
||||
: 0;
|
||||
|
||||
return Object.freeze(
|
||||
Object.keys(categories).reduce((acc, category) => {
|
||||
const emojis = chunk(categories[category], EMOJIS_PER_ROW);
|
||||
const height = generateCategoryHeight(emojis.length);
|
||||
const newAcc = {
|
||||
...acc,
|
||||
[category]: { emojis, height, top },
|
||||
};
|
||||
top += height;
|
||||
Object.keys(categories)
|
||||
.filter((c) => c !== FREQUENTLY_USED_KEY)
|
||||
.reduce((acc, category) => {
|
||||
const emojis = chunk(categories[category], EMOJIS_PER_ROW);
|
||||
const height = generateCategoryHeight(emojis.length);
|
||||
const newAcc = {
|
||||
...acc,
|
||||
[category]: { emojis, height, top },
|
||||
};
|
||||
top += height;
|
||||
|
||||
return newAcc;
|
||||
}, {}),
|
||||
return newAcc;
|
||||
}, frequentlyUsedEmojis || {}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
export const FREQUENTLY_USED_KEY = 'frequently_used';
|
||||
export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
|
||||
|
||||
export const CATEGORY_ICON_MAP = {
|
||||
[FREQUENTLY_USED_KEY]: 'history',
|
||||
activity: 'dumbbell',
|
||||
people: 'smiley',
|
||||
nature: 'nature',
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GlDropdownSectionHeader,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
|
||||
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
|
@ -12,12 +13,15 @@ import { __ } from '../../locale';
|
|||
import getRefMixin from '../mixins/get_ref';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
|
||||
import UploadBlobModal from './upload_blob_modal.vue';
|
||||
|
||||
const ROW_TYPES = {
|
||||
header: 'header',
|
||||
divider: 'divider',
|
||||
};
|
||||
|
||||
const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
|
@ -25,6 +29,7 @@ export default {
|
|||
GlDropdownSectionHeader,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
UploadBlobModal,
|
||||
},
|
||||
apollo: {
|
||||
projectShortPath: {
|
||||
|
@ -46,6 +51,9 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [getRefMixin],
|
||||
props: {
|
||||
currentPath: {
|
||||
|
@ -63,6 +71,21 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
canPushCode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
selectedBranch: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
originalBranch: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
newBranchPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -93,7 +116,13 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
uploadPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
|
||||
data() {
|
||||
return {
|
||||
projectShortPath: '',
|
||||
|
@ -126,7 +155,10 @@ export default {
|
|||
);
|
||||
},
|
||||
canCreateMrFromFork() {
|
||||
return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn;
|
||||
return this.userPermissions?.forkProject && this.userPermissions?.createMergeRequestIn;
|
||||
},
|
||||
showUploadModal() {
|
||||
return this.canEditTree && !this.$apollo.queries.userPermissions.loading;
|
||||
},
|
||||
dropdownItems() {
|
||||
const items = [];
|
||||
|
@ -149,10 +181,9 @@ export default {
|
|||
{
|
||||
attrs: {
|
||||
href: '#modal-upload-blob',
|
||||
'data-target': '#modal-upload-blob',
|
||||
'data-toggle': 'modal',
|
||||
},
|
||||
text: __('Upload file'),
|
||||
modalId: UPLOAD_BLOB_MODAL_ID,
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
|
@ -253,12 +284,26 @@ export default {
|
|||
<gl-icon name="chevron-down" :size="16" class="float-left" />
|
||||
</template>
|
||||
<template v-for="(item, i) in dropdownItems">
|
||||
<component :is="getComponent(item.type)" :key="i" v-bind="item.attrs">
|
||||
<component
|
||||
:is="getComponent(item.type)"
|
||||
:key="i"
|
||||
v-bind="item.attrs"
|
||||
v-gl-modal="item.modalId || null"
|
||||
>
|
||||
{{ item.text }}
|
||||
</component>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</li>
|
||||
</ol>
|
||||
<upload-blob-modal
|
||||
v-if="showUploadModal"
|
||||
:modal-id="$options.uploadBlobModalId"
|
||||
:commit-message="__('Upload New File')"
|
||||
:target-branch="selectedBranch"
|
||||
:original-branch="originalBranch"
|
||||
:can-push-code="canPushCode"
|
||||
:path="uploadPath"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
||||
import { parseBoolean } from '../lib/utils/common_utils';
|
||||
import { escapeFileUrl } from '../lib/utils/url_utility';
|
||||
import { __ } from '../locale';
|
||||
import App from './components/app.vue';
|
||||
import Breadcrumbs from './components/breadcrumbs.vue';
|
||||
import DirectoryDownloadLinks from './components/directory_download_links.vue';
|
||||
|
@ -55,6 +55,8 @@ export default function setupVueRepositoryList() {
|
|||
const {
|
||||
canCollaborate,
|
||||
canEditTree,
|
||||
canPushCode,
|
||||
selectedBranch,
|
||||
newBranchPath,
|
||||
newTagPath,
|
||||
newBlobPath,
|
||||
|
@ -65,8 +67,7 @@ export default function setupVueRepositoryList() {
|
|||
newDirPath,
|
||||
} = breadcrumbEl.dataset;
|
||||
|
||||
router.afterEach(({ params: { path = '/' } }) => {
|
||||
updateFormAction('.js-upload-blob-form', uploadPath, path);
|
||||
router.afterEach(({ params: { path } }) => {
|
||||
updateFormAction('.js-create-dir-form', newDirPath, path);
|
||||
});
|
||||
|
||||
|
@ -81,12 +82,16 @@ export default function setupVueRepositoryList() {
|
|||
currentPath: this.$route.params.path,
|
||||
canCollaborate: parseBoolean(canCollaborate),
|
||||
canEditTree: parseBoolean(canEditTree),
|
||||
canPushCode: parseBoolean(canPushCode),
|
||||
originalBranch: ref,
|
||||
selectedBranch,
|
||||
newBranchPath,
|
||||
newTagPath,
|
||||
newBlobPath,
|
||||
forkNewBlobPath,
|
||||
forkNewDirectoryPath,
|
||||
forkUploadBlobPath,
|
||||
uploadPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -172,9 +172,11 @@ export default {
|
|||
after: this.handleVuexActionDispatch,
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', this.handleDocumentMousedown);
|
||||
document.addEventListener('click', this.handleDocumentClick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('mousedown', this.handleDocumentMousedown);
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
},
|
||||
methods: {
|
||||
|
@ -196,12 +198,37 @@ export default {
|
|||
this.handleDropdownClose(state.labels.filter(filterFn));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This method stores a mousedown event's target.
|
||||
* Required by the click listener because the click
|
||||
* event itself has no reference to this element.
|
||||
*/
|
||||
handleDocumentMousedown({ target }) {
|
||||
this.mousedownTarget = target;
|
||||
},
|
||||
/**
|
||||
* This method listens for document-wide click event
|
||||
* and toggle dropdown if user clicks anywhere outside
|
||||
* the dropdown while dropdown is visible.
|
||||
*/
|
||||
handleDocumentClick({ target }) {
|
||||
// We also perform the toggle exception check for the
|
||||
// last mousedown event's target to avoid hiding the
|
||||
// box when the mousedown happened inside the box and
|
||||
// only the mouseup did not.
|
||||
if (
|
||||
this.showDropdownContents &&
|
||||
!this.preventDropdownToggleOnClick(target) &&
|
||||
!this.preventDropdownToggleOnClick(this.mousedownTarget)
|
||||
) {
|
||||
this.toggleDropdownContents();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This method checks whether a given click target
|
||||
* should prevent the dropdown from being toggled.
|
||||
*/
|
||||
preventDropdownToggleOnClick(target) {
|
||||
// This approach of element detection is needed
|
||||
// as the dropdown wrapper is not using `GlDropdown` as
|
||||
// it will also require us to use `BDropdownForm`
|
||||
|
@ -216,19 +243,20 @@ export default {
|
|||
target?.parentElement?.classList.contains(className),
|
||||
);
|
||||
|
||||
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
|
||||
const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
|
||||
(className) => $(target).parents(className).length,
|
||||
);
|
||||
|
||||
if (
|
||||
this.showDropdownContents &&
|
||||
!hadExceptionParent &&
|
||||
!hasExceptionClass &&
|
||||
!this.$refs.dropdownButtonCollapsed?.$el.contains(target) &&
|
||||
!this.$refs.dropdownContents?.$el.contains(target)
|
||||
) {
|
||||
this.toggleDropdownContents();
|
||||
}
|
||||
const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target);
|
||||
|
||||
const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target);
|
||||
|
||||
return (
|
||||
hasExceptionClass ||
|
||||
hasExceptionParent ||
|
||||
isInDropdownButtonCollapsed ||
|
||||
isInDropdownContents
|
||||
);
|
||||
},
|
||||
handleDropdownClose(labels) {
|
||||
// Only emit label updates if there are any labels to update
|
||||
|
|
|
@ -16,7 +16,6 @@ class ApplicationController < ActionController::Base
|
|||
include SessionlessAuthentication
|
||||
include SessionsHelper
|
||||
include ConfirmEmailWarning
|
||||
include Gitlab::Tracking::ControllerConcern
|
||||
include Gitlab::Experimentation::ControllerConcern
|
||||
include InitializesCurrentUserMode
|
||||
include Impersonation
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
class Groups::EmailCampaignsController < Groups::ApplicationController
|
||||
include InProductMarketingHelper
|
||||
include Gitlab::Tracking::ControllerConcern
|
||||
|
||||
EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
|
||||
|
||||
|
@ -25,7 +24,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
|||
subject_line: subject_line(@track, @series)
|
||||
}
|
||||
|
||||
track_self_describing_event(EMAIL_CAMPAIGNS_SCHEMA_URL, data: data)
|
||||
::Gitlab::Tracking.self_describing_event(EMAIL_CAMPAIGNS_SCHEMA_URL, data: data)
|
||||
end
|
||||
|
||||
def redirect_link
|
||||
|
|
|
@ -10,7 +10,7 @@ module MergedAtFilter
|
|||
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
|
||||
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
|
||||
|
||||
items.join_metrics.merge(mr_metrics_scope)
|
||||
join_metrics(items, mr_metrics_scope)
|
||||
end
|
||||
|
||||
def merged_after
|
||||
|
@ -20,4 +20,22 @@ module MergedAtFilter
|
|||
def merged_before
|
||||
params[:merged_before]
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
#
|
||||
# This join optimizes merged_at queries when the finder is invoked for a project by moving
|
||||
# the target_project_id condition from merge_requests table to merge_request_metrics table.
|
||||
def join_metrics(items, mr_metrics_scope)
|
||||
scope = if project_id = items.where_values_hash["target_project_id"]
|
||||
# removing the original merge_requests.target_project_id condition
|
||||
items = items.unscope(where: :target_project_id)
|
||||
# adding the target_project_id condition to merge_request_metrics
|
||||
items.join_metrics(project_id)
|
||||
else
|
||||
items.join_metrics
|
||||
end
|
||||
|
||||
scope.merge(mr_metrics_scope)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -50,6 +50,7 @@ module PackagesHelper
|
|||
|
||||
def track_package_event(event_name, scope, **args)
|
||||
::Packages::CreateEventService.new(nil, current_user, event_name: event_name, scope: scope).execute
|
||||
track_event(event_name, **args)
|
||||
category = args.delete(:category) || self.class.name
|
||||
::Gitlab::Tracking.event(category, event_name.to_s, **args)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -131,6 +131,8 @@ module TreeHelper
|
|||
|
||||
def breadcrumb_data_attributes
|
||||
attrs = {
|
||||
selected_branch: selected_branch,
|
||||
can_push_code: can?(current_user, :push_code, @project).to_s,
|
||||
can_collaborate: can_collaborate_with_project?(@project).to_s,
|
||||
new_blob_path: project_new_blob_path(@project, @ref),
|
||||
upload_path: project_create_blob_path(@project, @ref),
|
||||
|
|
|
@ -289,10 +289,19 @@ class MergeRequest < ApplicationRecord
|
|||
joins(:notes).where(notes: { commit_id: sha })
|
||||
end
|
||||
scope :join_project, -> { joins(:target_project) }
|
||||
scope :join_metrics, -> do
|
||||
scope :join_metrics, -> (target_project_id = nil) do
|
||||
# Do not join the relation twice
|
||||
return self if self.arel.join_sources.any? { |join| join.left.try(:name).eql?(MergeRequest::Metrics.table_name) }
|
||||
|
||||
query = joins(:metrics)
|
||||
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
|
||||
query
|
||||
|
||||
project_condition = if target_project_id
|
||||
MergeRequest::Metrics.arel_table[:target_project_id].eq(target_project_id)
|
||||
else
|
||||
MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id])
|
||||
end
|
||||
|
||||
query.where(project_condition)
|
||||
end
|
||||
scope :references_project, -> { references(:target_project) }
|
||||
scope :with_api_entity_associations, -> {
|
||||
|
|
|
@ -21,5 +21,4 @@
|
|||
|
||||
#js-tree-list{ data: vue_file_list_data(project, ref) }
|
||||
- if can_edit_tree?
|
||||
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
|
||||
= render 'projects/blob/new_dir'
|
||||
|
|
|
@ -1781,9 +1781,9 @@
|
|||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: mailers
|
||||
:feature_category:
|
||||
:feature_category: :issue_tracking
|
||||
:has_external_dependencies:
|
||||
:urgency:
|
||||
:urgency: low
|
||||
:resource_boundary:
|
||||
:weight: 2
|
||||
:idempotent:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate bootstrap modal to GlModal for repo single file uploads
|
||||
merge_request: 55587
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Don't close issue label select box on click if only mouseup outside
|
||||
merge_request: 56721
|
||||
author: Simon Stieger @sim0
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug in wiki link rewriter filter
|
||||
merge_request: 56636
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve the performance of Merge Request Analytics table
|
||||
merge_request: 56380
|
||||
author:
|
||||
type: performance
|
|
@ -36,7 +36,8 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/)
|
|||
`~/.ssh/authorized_keys`) or add it as a [deploy key](../../user/project/deploy_keys/index.md)
|
||||
if you are accessing a private GitLab repository.
|
||||
|
||||
The private key is displayed in the job log, unless you enable
|
||||
In the following example, the `ssh-add -` command does not display the value of
|
||||
`$SSH_PRIVATE_KEY` in the job log, though it could be exposed if you enable
|
||||
[debug logging](../variables/README.md#debug-logging). You might also want to
|
||||
check the [visibility of your pipelines](../pipelines/settings.md#visibility-of-pipelines).
|
||||
|
||||
|
|
|
@ -767,6 +767,66 @@ export default {
|
|||
};
|
||||
```
|
||||
|
||||
#### Polling and Performance
|
||||
|
||||
While the Apollo client has support for simple polling, for performance reasons, our [Etag-based caching](../polling.md) is preferred to hitting the database each time.
|
||||
|
||||
Once the backend is set up, there are a few changes to make on the frontend.
|
||||
|
||||
First, get your resource Etag path from the backend. In the example of the pipelines graph, this is called the `graphql_resource_etag`. This will be used to create new headers to add to the Apollo context:
|
||||
|
||||
```javascript
|
||||
/* pipelines/components/graph/utils.js */
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
const getQueryHeaders = (etagResource) => {
|
||||
return {
|
||||
fetchOptions: {
|
||||
method: 'GET',
|
||||
},
|
||||
headers: {
|
||||
/* This will depend on your feature */
|
||||
'X-GITLAB-GRAPHQL-FEATURE-CORRELATION': 'verify/ci/pipeline-graph',
|
||||
'X-GITLAB-GRAPHQL-RESOURCE-ETAG': etagResource,
|
||||
'X-REQUESTED-WITH': 'XMLHttpRequest',
|
||||
},
|
||||
};
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
/* component.vue */
|
||||
|
||||
apollo: {
|
||||
pipeline: {
|
||||
context() {
|
||||
return getQueryHeaders(this.graphqlResourceEtag);
|
||||
},
|
||||
query: getPipelineDetails,
|
||||
pollInterval: 10000,
|
||||
..
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Then, becasue Etags depend on the request being a `GET` instead of GraphQL's usual `POST`, but our default link library does not support `GET` we need to let our defaut Apollo client know to use a different library.
|
||||
|
||||
```javascript
|
||||
/* componentMountIndex.js */
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(
|
||||
{},
|
||||
{
|
||||
useGet: true,
|
||||
},
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Keep in mind, this means your app will not batch queries.
|
||||
|
||||
Once subscriptions are mature, this process can be replaced by using them and we can remove the separate link library and return to batching queries.
|
||||
|
||||
### Testing
|
||||
|
||||
#### Generating the GraphQL schema
|
||||
|
|
|
@ -6,7 +6,7 @@ module Banzai
|
|||
class Rewriter
|
||||
def initialize(link_string, wiki:, slug:)
|
||||
@uri = Addressable::URI.parse(link_string)
|
||||
@wiki_base_path = wiki && wiki.wiki_base_path
|
||||
@wiki_base_path = wiki&.wiki_base_path
|
||||
@slug = slug
|
||||
end
|
||||
|
||||
|
@ -41,7 +41,8 @@ module Banzai
|
|||
# Any link _not_ of the form `http://example.com/`
|
||||
def apply_relative_link_rules!
|
||||
if @uri.relative? && @uri.path.present?
|
||||
link = ::File.join(@wiki_base_path, @uri.path)
|
||||
link = @uri.path
|
||||
link = ::File.join(@wiki_base_path, link) unless link.starts_with?(@wiki_base_path)
|
||||
link = "#{link}##{@uri.fragment}" if @uri.fragment
|
||||
@uri = Addressable::URI.parse(link)
|
||||
end
|
||||
|
|
|
@ -13,10 +13,17 @@ module Gitlab
|
|||
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?)
|
||||
].compact.freeze
|
||||
|
||||
DEFAULT_WORKERS = [
|
||||
DummyWorker.new('default', weight: 1, tags: []),
|
||||
DummyWorker.new('mailers', weight: 2, tags: [])
|
||||
].map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze
|
||||
# This maps workers not in our application code to queues. We need
|
||||
# these queues in our YAML files to ensure we don't accidentally
|
||||
# miss jobs from these queues.
|
||||
#
|
||||
# The default queue should be unused, which is why it maps to an
|
||||
# invalid class name. We keep it in the YAML file for safety, just
|
||||
# in case anything does get scheduled to run there.
|
||||
DEFAULT_WORKERS = {
|
||||
'_' => DummyWorker.new('default', weight: 1, tags: []),
|
||||
'ActionMailer::MailDeliveryJob' => DummyWorker.new('mailers', feature_category: :issue_tracking, urgency: 'low', weight: 2, tags: [])
|
||||
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze
|
||||
|
||||
class << self
|
||||
include Gitlab::SidekiqConfig::CliMethods
|
||||
|
@ -40,7 +47,7 @@ module Gitlab
|
|||
def workers
|
||||
@workers ||= begin
|
||||
result = []
|
||||
result.concat(DEFAULT_WORKERS)
|
||||
result.concat(DEFAULT_WORKERS.values)
|
||||
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false))
|
||||
|
||||
if Gitlab.ee?
|
||||
|
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
|
||||
def create_labels(worker_class, queue, job)
|
||||
worker_name = (job['wrapped'].presence || worker_class).to_s
|
||||
worker = find_worker(worker_name, worker_class)
|
||||
|
||||
labels = { queue: queue.to_s,
|
||||
worker: worker_name,
|
||||
|
@ -18,15 +19,15 @@ module Gitlab
|
|||
feature_category: "",
|
||||
boundary: "" }
|
||||
|
||||
return labels unless worker_class && worker_class.include?(WorkerAttributes)
|
||||
return labels unless worker.respond_to?(:get_urgency)
|
||||
|
||||
labels[:urgency] = worker_class.get_urgency.to_s
|
||||
labels[:external_dependencies] = bool_as_label(worker_class.worker_has_external_dependencies?)
|
||||
labels[:urgency] = worker.get_urgency.to_s
|
||||
labels[:external_dependencies] = bool_as_label(worker.worker_has_external_dependencies?)
|
||||
|
||||
feature_category = worker_class.get_feature_category
|
||||
feature_category = worker.get_feature_category
|
||||
labels[:feature_category] = feature_category.to_s
|
||||
|
||||
resource_boundary = worker_class.get_worker_resource_boundary
|
||||
resource_boundary = worker.get_worker_resource_boundary
|
||||
labels[:boundary] = resource_boundary == :unknown ? "" : resource_boundary.to_s
|
||||
|
||||
labels
|
||||
|
@ -35,6 +36,10 @@ module Gitlab
|
|||
def bool_as_label(value)
|
||||
value ? TRUE_LABEL : FALSE_LABEL
|
||||
end
|
||||
|
||||
def find_worker(worker_name, worker_class)
|
||||
Gitlab::SidekiqConfig::DEFAULT_WORKERS.fetch(worker_name, worker_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,21 +4,6 @@ module Gitlab
|
|||
module Tracking
|
||||
SNOWPLOW_NAMESPACE = 'gl'
|
||||
|
||||
module ControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
protected
|
||||
|
||||
def track_event(action = action_name, **args)
|
||||
category = args.delete(:category) || self.class.name
|
||||
Gitlab::Tracking.event(category, action.to_s, **args)
|
||||
end
|
||||
|
||||
def track_self_describing_event(schema_url, data:, **args)
|
||||
Gitlab::Tracking.self_describing_event(schema_url, data: data, **args)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def enabled?
|
||||
Gitlab::CurrentSettings.snowplow_enabled?
|
||||
|
|
|
@ -56,8 +56,6 @@ RSpec.describe ApplicationController do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a Trackable Controller'
|
||||
|
||||
describe '#add_gon_variables' do
|
||||
before do
|
||||
Gon.clear
|
||||
|
|
|
@ -42,7 +42,7 @@ RSpec.describe Projects::Registry::TagsController do
|
|||
it 'tracks the event', :snowplow do
|
||||
get_tags
|
||||
|
||||
expect_snowplow_event(category: anything, action: 'list_tags')
|
||||
expect_snowplow_event(category: 'Projects::Registry::TagsController', action: 'list_tags')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -107,11 +107,12 @@ RSpec.describe Projects::Registry::TagsController do
|
|||
destroy_tag('test.')
|
||||
end
|
||||
|
||||
it 'tracks the event' do
|
||||
it 'tracks the event', :snowplow do
|
||||
expect_delete_tags(%w[test.])
|
||||
expect(controller).to receive(:track_event).with(:delete_tag)
|
||||
|
||||
destroy_tag('test.')
|
||||
|
||||
expect_snowplow_event(category: 'Projects::Registry::TagsController', action: 'delete_tag')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Projects > Files > User uploads files' do
|
||||
include DropzoneHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
|
||||
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
|
||||
|
@ -17,36 +15,15 @@ RSpec.describe 'Projects > Files > User uploads files' do
|
|||
context 'when a user has write access' do
|
||||
before do
|
||||
visit(project_tree_path(project))
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
include_examples 'it uploads and commit a new text file'
|
||||
|
||||
include_examples 'it uploads and commit a new image file'
|
||||
|
||||
it 'uploads a file to a sub-directory', :js do
|
||||
click_link 'files'
|
||||
|
||||
page.within('.repo-breadcrumb') do
|
||||
expect(page).to have_content('files')
|
||||
end
|
||||
|
||||
find('.add-to-tree').click
|
||||
click_link('Upload file')
|
||||
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
|
||||
|
||||
page.within('#modal-upload-blob') do
|
||||
fill_in(:commit_message, with: 'New commit message')
|
||||
end
|
||||
|
||||
click_button('Upload file')
|
||||
|
||||
expect(page).to have_content('New commit message')
|
||||
|
||||
page.within('.repo-breadcrumb') do
|
||||
expect(page).to have_content('files')
|
||||
expect(page).to have_content('doc_sample.txt')
|
||||
end
|
||||
end
|
||||
include_examples 'it uploads a file to a sub-directory'
|
||||
end
|
||||
|
||||
context 'when a user does not have write access' do
|
||||
|
|
|
@ -17,11 +17,15 @@ RSpec.describe 'Projects > Show > User uploads files' do
|
|||
context 'when a user has write access' do
|
||||
before do
|
||||
visit(project_path(project))
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
include_examples 'it uploads and commit a new text file'
|
||||
|
||||
include_examples 'it uploads and commit a new image file'
|
||||
|
||||
include_examples 'it uploads a file to a sub-directory'
|
||||
end
|
||||
|
||||
context 'when a user does not have write access' do
|
||||
|
|
|
@ -156,6 +156,18 @@ RSpec.describe MergeRequestsFinder do
|
|||
|
||||
it { is_expected.to eq([merge_request2]) }
|
||||
end
|
||||
|
||||
context 'when project_id is given' do
|
||||
subject(:query) { described_class.new(user, merged_after: 15.days.ago, merged_before: 6.days.ago, project_id: merge_request2.project).execute }
|
||||
|
||||
it { is_expected.to eq([merge_request2]) }
|
||||
|
||||
it 'queries merge_request_metrics.target_project_id table' do
|
||||
expect(query.to_sql).to include(%{"merge_request_metrics"."target_project_id" = #{merge_request2.target_project_id}})
|
||||
|
||||
expect(query.to_sql).not_to include(%{"merge_requests"."target_project_id"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by group' do
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components/utils';
|
||||
|
||||
jest.mock('js-cookie');
|
||||
|
||||
describe('getFrequentlyUsedEmojis', () => {
|
||||
it('it returns null when no saved emojis set', () => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue(null);
|
||||
|
||||
expect(getFrequentlyUsedEmojis()).toBe(null);
|
||||
});
|
||||
|
||||
it('it returns frequently used emojis object', () => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup,thumbsdown');
|
||||
|
||||
expect(getFrequentlyUsedEmojis()).toEqual({
|
||||
frequently_used: {
|
||||
emojis: [['thumbsup', 'thumbsdown']],
|
||||
top: 0,
|
||||
height: 71,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToFrequentlyUsed', () => {
|
||||
it('sets cookie value', () => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue(null);
|
||||
|
||||
addToFrequentlyUsed('thumbsup');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
|
||||
expires: 365,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets cookie value to include previously set cookie value', () => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsdown');
|
||||
|
||||
addToFrequentlyUsed('thumbsup');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsdown,thumbsup', {
|
||||
expires: 365,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets cookie value with uniq values', () => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup');
|
||||
|
||||
addToFrequentlyUsed('thumbsup');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
|
||||
expires: 365,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,24 +1,36 @@
|
|||
import { GlDropdown } from '@gitlab/ui';
|
||||
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
|
||||
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
|
||||
|
||||
let vm;
|
||||
|
||||
function factory(currentPath, extraProps = {}) {
|
||||
vm = shallowMount(Breadcrumbs, {
|
||||
propsData: {
|
||||
currentPath,
|
||||
...extraProps,
|
||||
},
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
},
|
||||
});
|
||||
}
|
||||
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
|
||||
|
||||
describe('Repository breadcrumbs component', () => {
|
||||
let wrapper;
|
||||
|
||||
const factory = (currentPath, extraProps = {}) => {
|
||||
const $apollo = {
|
||||
queries: {
|
||||
userPermissions: {
|
||||
loading: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
wrapper = shallowMount(Breadcrumbs, {
|
||||
propsData: {
|
||||
currentPath,
|
||||
...extraProps,
|
||||
},
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
},
|
||||
mocks: { $apollo },
|
||||
});
|
||||
};
|
||||
|
||||
const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
|
||||
|
||||
afterEach(() => {
|
||||
vm.destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -30,13 +42,13 @@ describe('Repository breadcrumbs component', () => {
|
|||
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
|
||||
factory(path);
|
||||
|
||||
expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
|
||||
expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount);
|
||||
});
|
||||
|
||||
it('escapes hash in directory path', () => {
|
||||
factory('app/assets/javascripts#');
|
||||
|
||||
expect(vm.findAll(RouterLinkStub).at(3).props('to')).toEqual(
|
||||
expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual(
|
||||
'/-/tree/app/assets/javascripts%23',
|
||||
);
|
||||
});
|
||||
|
@ -44,26 +56,44 @@ describe('Repository breadcrumbs component', () => {
|
|||
it('renders last link as active', () => {
|
||||
factory('app/assets');
|
||||
|
||||
expect(vm.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page');
|
||||
expect(wrapper.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page');
|
||||
});
|
||||
|
||||
it('does not render add to tree dropdown when permissions are false', () => {
|
||||
it('does not render add to tree dropdown when permissions are false', async () => {
|
||||
factory('/', { canCollaborate: false });
|
||||
|
||||
vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
|
||||
wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
|
||||
|
||||
return vm.vm.$nextTick(() => {
|
||||
expect(vm.find(GlDropdown).exists()).toBe(false);
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders add to tree dropdown when permissions are true', () => {
|
||||
it('renders add to tree dropdown when permissions are true', async () => {
|
||||
factory('/', { canCollaborate: true });
|
||||
|
||||
vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
|
||||
wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
|
||||
|
||||
return vm.vm.$nextTick(() => {
|
||||
expect(vm.find(GlDropdown).exists()).toBe(true);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('renders the upload blob modal', () => {
|
||||
beforeEach(() => {
|
||||
factory('/', { canEditTree: true });
|
||||
});
|
||||
|
||||
it('does not render the modal while loading', () => {
|
||||
expect(findUploadBlobModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders the modal once loaded', async () => {
|
||||
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findUploadBlobModal().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,69 +1,67 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
|
||||
import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
|
||||
import eventHub from '~/vue_merge_request_widget/event_hub';
|
||||
|
||||
describe('MRWidgetFailedToMerge', () => {
|
||||
const dummyIntervalId = 1337;
|
||||
let Component;
|
||||
let mr;
|
||||
let vm;
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}, data = {}) => {
|
||||
wrapper = shallowMount(MrWidgetFailedToMerge, {
|
||||
propsData: {
|
||||
mr: {
|
||||
mergeError: 'Merge error happened',
|
||||
},
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Component = Vue.extend(failedToMergeComponent);
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId);
|
||||
jest.spyOn(window, 'clearInterval').mockImplementation();
|
||||
mr = {
|
||||
mergeError: 'Merge error happened',
|
||||
};
|
||||
vm = mountComponent(Component, {
|
||||
mr,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('sets interval to refresh', () => {
|
||||
expect(window.setInterval).toHaveBeenCalledWith(vm.updateTimer, 1000);
|
||||
expect(vm.intervalId).toBe(dummyIntervalId);
|
||||
});
|
||||
describe('interval', () => {
|
||||
it('sets interval to refresh', () => {
|
||||
createComponent();
|
||||
|
||||
it('clears interval when destroying ', () => {
|
||||
vm.$destroy();
|
||||
|
||||
expect(window.clearInterval).toHaveBeenCalledWith(dummyIntervalId);
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('timerText', () => {
|
||||
it('should return correct timer text', () => {
|
||||
expect(vm.timerText).toEqual('Refreshing in 10 seconds to show the updated status...');
|
||||
|
||||
vm.timer = 1;
|
||||
|
||||
expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...');
|
||||
});
|
||||
expect(window.setInterval).toHaveBeenCalledWith(wrapper.vm.updateTimer, 1000);
|
||||
expect(wrapper.vm.intervalId).toBe(dummyIntervalId);
|
||||
});
|
||||
|
||||
describe('mergeError', () => {
|
||||
it('removes forced line breaks', (done) => {
|
||||
mr.mergeError = 'contains<br />line breaks<br />';
|
||||
it('clears interval when destroying ', () => {
|
||||
createComponent();
|
||||
wrapper.destroy();
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.mergeError).toBe('contains line breaks.');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
expect(window.clearInterval).toHaveBeenCalledWith(dummyIntervalId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeError', () => {
|
||||
it('removes forced line breaks', async () => {
|
||||
createComponent({ mr: { mergeError: 'contains<br />line breaks<br />' } });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.mergeError).toBe('contains line breaks.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('created', () => {
|
||||
it('should disable polling', () => {
|
||||
createComponent();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling');
|
||||
});
|
||||
});
|
||||
|
@ -71,11 +69,13 @@ describe('MRWidgetFailedToMerge', () => {
|
|||
describe('methods', () => {
|
||||
describe('refresh', () => {
|
||||
it('should emit event to request component refresh', () => {
|
||||
expect(vm.isRefreshing).toEqual(false);
|
||||
createComponent();
|
||||
|
||||
vm.refresh();
|
||||
expect(wrapper.vm.isRefreshing).toBe(false);
|
||||
|
||||
expect(vm.isRefreshing).toEqual(true);
|
||||
wrapper.vm.refresh();
|
||||
|
||||
expect(wrapper.vm.isRefreshing).toBe(true);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling');
|
||||
});
|
||||
|
@ -83,78 +83,76 @@ describe('MRWidgetFailedToMerge', () => {
|
|||
|
||||
describe('updateTimer', () => {
|
||||
it('should update timer and emit event when timer end', () => {
|
||||
jest.spyOn(vm, 'refresh').mockImplementation(() => {});
|
||||
createComponent();
|
||||
|
||||
expect(vm.timer).toEqual(10);
|
||||
jest.spyOn(wrapper.vm, 'refresh').mockImplementation(() => {});
|
||||
|
||||
expect(wrapper.vm.timer).toEqual(10);
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
expect(vm.timer).toEqual(10 - i);
|
||||
vm.updateTimer();
|
||||
expect(wrapper.vm.timer).toEqual(10 - i);
|
||||
wrapper.vm.updateTimer();
|
||||
}
|
||||
|
||||
expect(vm.refresh).toHaveBeenCalled();
|
||||
expect(wrapper.vm.refresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('while it is refreshing', () => {
|
||||
it('renders Refresing now', (done) => {
|
||||
vm.isRefreshing = true;
|
||||
it('renders Refresing now', async () => {
|
||||
createComponent({}, { isRefreshing: true });
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual(
|
||||
'Refreshing now',
|
||||
);
|
||||
done();
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('.js-refresh-label').text().trim()).toBe('Refreshing now');
|
||||
});
|
||||
});
|
||||
|
||||
describe('while it is not regresing', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders warning icon and disabled merge button', () => {
|
||||
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
|
||||
expect(
|
||||
vm.$el.querySelector('[data-testid="disabled-merge-button"]').getAttribute('disabled'),
|
||||
).toEqual('disabled');
|
||||
expect(wrapper.find('.js-ci-status-icon-warning')).not.toBeNull();
|
||||
expect(wrapper.find(StatusIcon).props('showDisabledButton')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders given error', () => {
|
||||
expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
|
||||
'Merge error happened.',
|
||||
);
|
||||
expect(wrapper.find('.has-error-message').text().trim()).toBe('Merge error happened.');
|
||||
});
|
||||
|
||||
it('renders refresh button', () => {
|
||||
expect(
|
||||
vm.$el
|
||||
.querySelector('[data-testid="merge-request-failed-refresh-button"]')
|
||||
.textContent.trim(),
|
||||
).toEqual('Refresh now');
|
||||
wrapper.find('[data-testid="merge-request-failed-refresh-button"]').text().trim(),
|
||||
).toBe('Refresh now');
|
||||
});
|
||||
|
||||
it('renders remaining time', () => {
|
||||
expect(vm.$el.querySelector('.has-custom-error').textContent.trim()).toEqual(
|
||||
expect(wrapper.find('.has-custom-error').text().trim()).toBe(
|
||||
'Refreshing in 10 seconds to show the updated status...',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should just generic merge failed message if merge_error is not available', (done) => {
|
||||
vm.mr.mergeError = null;
|
||||
it('should just generic merge failed message if merge_error is not available', async () => {
|
||||
createComponent({ mr: { mergeError: null } });
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('Merge failed.');
|
||||
expect(vm.$el.innerText).not.toContain('Merge error happened.');
|
||||
done();
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text().trim()).toContain('Merge failed.');
|
||||
expect(wrapper.text().trim()).not.toContain('Merge error happened.');
|
||||
});
|
||||
|
||||
it('should show refresh label when refresh requested', (done) => {
|
||||
vm.refresh();
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing');
|
||||
expect(vm.$el.innerText).toContain('Refreshing now');
|
||||
done();
|
||||
});
|
||||
it('should show refresh label when refresh requested', async () => {
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.refresh();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text().trim()).not.toContain('Merge failed. Refreshing');
|
||||
expect(wrapper.text().trim()).toContain('Refreshing now');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,15 @@ RSpec.describe Banzai::Filter::WikiLinkFilter do
|
|||
expect(filtered_link.attribute('href').value).to eq('/uploads/a.test')
|
||||
end
|
||||
|
||||
describe 'when links point to the relative wiki path' do
|
||||
it 'does not rewrite links' do
|
||||
path = "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg"
|
||||
filtered_link = filter("<a href='#{path}'>Link</a>", wiki: wiki, page_slug: 'home').children[0]
|
||||
|
||||
expect(filtered_link.attribute('href').value).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when links point to the #{Wikis::CreateAttachmentService::ATTACHMENT_PATH} folder" do
|
||||
context 'with an "a" html tag' do
|
||||
it 'rewrites links' do
|
||||
|
|
|
@ -229,6 +229,15 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
|
|||
it_behaves_like "a metrics middleware"
|
||||
end
|
||||
|
||||
context 'for ActionMailer::MailDeliveryJob' do
|
||||
let(:job) { { 'class' => ActionMailer::MailDeliveryJob } }
|
||||
let(:worker) { ActionMailer::MailDeliveryJob.new }
|
||||
let(:worker_class) { ActionMailer::MailDeliveryJob }
|
||||
let(:labels) { default_labels.merge(feature_category: 'issue_tracking') }
|
||||
|
||||
it_behaves_like 'a metrics middleware'
|
||||
end
|
||||
|
||||
context "when workers are attributed" do
|
||||
def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
|
||||
Class.new do
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a Trackable Controller' do
|
||||
describe '#track_event', :snowplow do
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
context 'with no params' do
|
||||
controller(described_class) do
|
||||
def index
|
||||
track_event
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
it 'tracks the action name', :snowplow do
|
||||
get :index
|
||||
|
||||
expect_snowplow_event(category: 'AnonymousController', action: 'index')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with params' do
|
||||
controller(described_class) do
|
||||
def index
|
||||
track_event('some_event', category: 'SomeCategory', label: 'errorlabel')
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
it 'tracks with the specified param' do
|
||||
get :index
|
||||
|
||||
expect_snowplow_event(category: 'SomeCategory', action: 'some_event', label: 'errorlabel')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
|
||||
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
|
||||
|
||||
page.within('#modal-upload-blob') do
|
||||
fill_in(:commit_message, with: 'New commit message')
|
||||
|
@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
|
||||
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'), make_visible: true)
|
||||
|
||||
page.within('#modal-upload-blob') do
|
||||
fill_in(:commit_message, with: 'New commit message')
|
||||
|
@ -70,9 +70,11 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
|
|||
|
||||
expect(page).to have_content(fork_message)
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
find('.add-to-tree').click
|
||||
click_link('Upload file')
|
||||
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
|
||||
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
|
||||
|
||||
page.within('#modal-upload-blob') do
|
||||
fill_in(:commit_message, with: 'New commit message')
|
||||
|
@ -95,6 +97,33 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'it uploads a file to a sub-directory' do
|
||||
it 'uploads a file to a sub-directory', :js do
|
||||
click_link 'files'
|
||||
|
||||
page.within('.repo-breadcrumb') do
|
||||
expect(page).to have_content('files')
|
||||
end
|
||||
|
||||
find('.add-to-tree').click
|
||||
click_link('Upload file')
|
||||
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
|
||||
|
||||
page.within('#modal-upload-blob') do
|
||||
fill_in(:commit_message, with: 'New commit message')
|
||||
end
|
||||
|
||||
click_button('Upload file')
|
||||
|
||||
expect(page).to have_content('New commit message')
|
||||
|
||||
page.within('.repo-breadcrumb') do
|
||||
expect(page).to have_content('files')
|
||||
expect(page).to have_content('doc_sample.txt')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do
|
||||
it 'uploads and commits a new text file via "upload file" button', :js do
|
||||
find('[data-testid="upload-file-button"]').click
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Every Sidekiq worker' do
|
||||
let(:workers_without_defaults) do
|
||||
Gitlab::SidekiqConfig.workers - Gitlab::SidekiqConfig::DEFAULT_WORKERS
|
||||
Gitlab::SidekiqConfig.workers - Gitlab::SidekiqConfig::DEFAULT_WORKERS.values
|
||||
end
|
||||
|
||||
it 'does not use the default queue' do
|
||||
|
|
Loading…
Reference in New Issue