Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
515f39456f
commit
498ba9dc41
49 changed files with 693 additions and 509 deletions
|
@ -2,9 +2,14 @@
|
|||
import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
|
||||
import { FIRST_DROPDOWN_INDEX, SEARCH_BOX_INDEX } from '../constants';
|
||||
import {
|
||||
FIRST_DROPDOWN_INDEX,
|
||||
SEARCH_BOX_INDEX,
|
||||
SEARCH_INPUT_DESCRIPTION,
|
||||
SEARCH_RESULTS_DESCRIPTION,
|
||||
} from '../constants';
|
||||
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
|
||||
import HeaderSearchDefaultItems from './header_search_default_items.vue';
|
||||
import HeaderSearchScopedItems from './header_search_scoped_items.vue';
|
||||
|
@ -12,7 +17,21 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue';
|
|||
export default {
|
||||
name: 'HeaderSearchApp',
|
||||
i18n: {
|
||||
searchPlaceholder: __('Search or jump to...'),
|
||||
searchPlaceholder: s__('GlobalSearch|Search or jump to...'),
|
||||
searchAria: s__('GlobalSearch|Search GitLab'),
|
||||
searchInputDescribeByNoDropdown: s__(
|
||||
'GlobalSearch|Type and press the enter key to submit search.',
|
||||
),
|
||||
searchInputDescribeByWithDropdown: s__(
|
||||
'GlobalSearch|Type for new suggestions to appear below.',
|
||||
),
|
||||
searchDescribedByDefault: s__(
|
||||
'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.',
|
||||
),
|
||||
searchDescribedByUpdated: s__(
|
||||
'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.',
|
||||
),
|
||||
searchResultsLoading: s__('GlobalSearch|Search results are loading'),
|
||||
},
|
||||
directives: { Outside },
|
||||
components: {
|
||||
|
@ -29,7 +48,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['search']),
|
||||
...mapState(['search', 'loading']),
|
||||
...mapGetters(['searchQuery', 'searchOptions']),
|
||||
searchText: {
|
||||
get() {
|
||||
|
@ -42,6 +61,9 @@ export default {
|
|||
currentFocusedOption() {
|
||||
return this.searchOptions[this.currentFocusIndex];
|
||||
},
|
||||
currentFocusedId() {
|
||||
return this.currentFocusedOption?.html_id;
|
||||
},
|
||||
isLoggedIn() {
|
||||
return gon?.current_username;
|
||||
},
|
||||
|
@ -58,6 +80,30 @@ export default {
|
|||
|
||||
return FIRST_DROPDOWN_INDEX;
|
||||
},
|
||||
searchInputDescribeBy() {
|
||||
if (this.isLoggedIn) {
|
||||
return this.$options.i18n.searchInputDescribeByWithDropdown;
|
||||
}
|
||||
|
||||
return this.$options.i18n.searchInputDescribeByNoDropdown;
|
||||
},
|
||||
dropdownResultsDescription() {
|
||||
if (!this.showSearchDropdown) {
|
||||
return ''; // This allows aria-live to see register an update when the dropdown is shown
|
||||
}
|
||||
|
||||
if (this.showDefaultItems) {
|
||||
return sprintf(this.$options.i18n.searchDescribedByDefault, {
|
||||
count: this.searchOptions.length,
|
||||
});
|
||||
}
|
||||
|
||||
return this.loading
|
||||
? this.$options.i18n.searchResultsLoading
|
||||
: sprintf(this.$options.i18n.searchDescribedByUpdated, {
|
||||
count: this.searchOptions.length,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSearch', 'fetchAutocompleteOptions']),
|
||||
|
@ -79,22 +125,44 @@ export default {
|
|||
},
|
||||
},
|
||||
SEARCH_BOX_INDEX,
|
||||
SEARCH_INPUT_DESCRIPTION,
|
||||
SEARCH_RESULTS_DESCRIPTION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-outside="closeDropdown" class="header-search gl-relative">
|
||||
<form
|
||||
v-outside="closeDropdown"
|
||||
role="search"
|
||||
:aria-label="$options.i18n.searchAria"
|
||||
class="header-search gl-relative"
|
||||
>
|
||||
<gl-search-box-by-type
|
||||
v-model="searchText"
|
||||
role="searchbox"
|
||||
class="gl-z-index-1"
|
||||
:debounce="500"
|
||||
autocomplete="off"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
:aria-activedescendant="currentFocusedId"
|
||||
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
|
||||
@focus="openDropdown"
|
||||
@click="openDropdown"
|
||||
@input="getAutocompleteOptions"
|
||||
@keydown.enter.stop.prevent="submitSearch"
|
||||
/>
|
||||
<span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
|
||||
searchInputDescribeBy
|
||||
}}</span>
|
||||
<span
|
||||
role="region"
|
||||
:data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
|
||||
class="gl-sr-only"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{{ dropdownResultsDescription }}
|
||||
</span>
|
||||
<div
|
||||
v-if="showSearchDropdown"
|
||||
data-testid="header-search-dropdown-menu"
|
||||
|
@ -118,5 +186,5 @@ export default {
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
@ -69,13 +69,16 @@ export default {
|
|||
<gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="data in option.data"
|
||||
:id="data.html_id"
|
||||
:ref="data.html_id"
|
||||
:key="data.html_id"
|
||||
:class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
|
||||
:aria-selected="isOptionFocused(data)"
|
||||
:aria-label="data.label"
|
||||
tabindex="-1"
|
||||
:href="data.url"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<div class="gl-display-flex gl-align-items-center" aria-hidden="true">
|
||||
<gl-avatar
|
||||
v-if="data.avatar_url !== undefined"
|
||||
:src="data.avatar_url"
|
||||
|
|
|
@ -43,13 +43,16 @@ export default {
|
|||
<gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="option in defaultSearchOptions"
|
||||
:id="option.html_id"
|
||||
:ref="option.html_id"
|
||||
:key="option.html_id"
|
||||
:class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
|
||||
:aria-selected="isOptionFocused(option)"
|
||||
:aria-label="option.title"
|
||||
tabindex="-1"
|
||||
:href="option.url"
|
||||
>
|
||||
{{ option.title }}
|
||||
<span aria-hidden="true">{{ option.title }}</span>
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlDropdownItem } from '@gitlab/ui';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'HeaderSearchScopedItems',
|
||||
|
@ -22,6 +23,13 @@ export default {
|
|||
isOptionFocused(option) {
|
||||
return this.currentFocusedOption?.html_id === option.html_id;
|
||||
},
|
||||
ariaLabel(option) {
|
||||
return sprintf(__('%{search} %{description} %{scope}'), {
|
||||
search: this.search,
|
||||
description: option.description,
|
||||
scope: option.scope || '',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -30,15 +38,20 @@ export default {
|
|||
<div>
|
||||
<gl-dropdown-item
|
||||
v-for="option in scopedSearchOptions"
|
||||
:id="option.html_id"
|
||||
:ref="option.html_id"
|
||||
:key="option.html_id"
|
||||
:class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
|
||||
:aria-selected="isOptionFocused(option)"
|
||||
:aria-label="ariaLabel(option)"
|
||||
tabindex="-1"
|
||||
:href="option.url"
|
||||
>
|
||||
"<span class="gl-font-weight-bold">{{ search }}</span
|
||||
>" {{ option.description }}
|
||||
<span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
|
||||
<span aria-hidden="true">
|
||||
"<span class="gl-font-weight-bold">{{ search }}</span
|
||||
>" {{ option.description }}
|
||||
<span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
|
||||
</span>
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { __ } from '~/locale';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me');
|
||||
export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
|
||||
|
||||
export const MSG_ISSUES_IVE_CREATED = __("Issues I've created");
|
||||
export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created");
|
||||
|
||||
export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me');
|
||||
export const MSG_MR_ASSIGNED_TO_ME = s__('GlobalSearch|Merge requests assigned to me');
|
||||
|
||||
export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer");
|
||||
export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a reviewer");
|
||||
|
||||
export const MSG_MR_IVE_CREATED = __("Merge requests I've created");
|
||||
export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created");
|
||||
|
||||
export const MSG_IN_ALL_GITLAB = __('in all GitLab');
|
||||
export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab');
|
||||
|
||||
export const MSG_IN_GROUP = __('in group');
|
||||
export const MSG_IN_GROUP = s__('GlobalSearch|in group');
|
||||
|
||||
export const MSG_IN_PROJECT = __('in project');
|
||||
export const MSG_IN_PROJECT = s__('GlobalSearch|in project');
|
||||
|
||||
export const GROUPS_CATEGORY = 'Groups';
|
||||
|
||||
|
@ -27,3 +27,7 @@ export const SMALL_AVATAR_PX = 16;
|
|||
export const FIRST_DROPDOWN_INDEX = 0;
|
||||
|
||||
export const SEARCH_BOX_INDEX = -1;
|
||||
|
||||
export const SEARCH_INPUT_DESCRIPTION = 'search-input-description';
|
||||
|
||||
export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description';
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
INVITE_MEMBERS_IN_COMMENT,
|
||||
GROUP_FILTERS,
|
||||
USERS_FILTER_ALL,
|
||||
MEMBER_AREAS_OF_FOCUS,
|
||||
INVITE_MEMBERS_FOR_TASK,
|
||||
MODAL_LABELS,
|
||||
LEARN_GITLAB,
|
||||
|
@ -101,14 +100,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
areasOfFocusOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
noSelectionAreasOfFocus: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tasksToBeDoneOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
@ -126,7 +117,6 @@ export default {
|
|||
inviteeType: 'members',
|
||||
newUsersToInvite: [],
|
||||
selectedDate: undefined,
|
||||
selectedAreasOfFocus: [],
|
||||
selectedTasksToBeDone: [],
|
||||
selectedTaskProject: this.projects[0],
|
||||
groupToBeSharedWith: {},
|
||||
|
@ -182,16 +172,6 @@ export default {
|
|||
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
|
||||
);
|
||||
},
|
||||
areasOfFocusEnabled() {
|
||||
return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
|
||||
},
|
||||
areasOfFocusForPost() {
|
||||
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
|
||||
return this.noSelectionAreasOfFocus;
|
||||
}
|
||||
|
||||
return this.selectedAreasOfFocus;
|
||||
},
|
||||
errorFieldDescription() {
|
||||
if (this.inviteeType === 'group') {
|
||||
return '';
|
||||
|
@ -232,8 +212,6 @@ export default {
|
|||
this.openModal(options);
|
||||
if (this.isOnLearnGitlab) {
|
||||
this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, this.source);
|
||||
} else {
|
||||
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -280,8 +258,6 @@ export default {
|
|||
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
|
||||
this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success');
|
||||
}
|
||||
|
||||
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
|
||||
},
|
||||
trackinviteMembersForTask() {
|
||||
const label = 'selected_tasks_to_be_done';
|
||||
|
@ -296,7 +272,6 @@ export default {
|
|||
this.newUsersToInvite = [];
|
||||
this.groupToBeSharedWith = {};
|
||||
this.invalidFeedbackMessage = '';
|
||||
this.selectedAreasOfFocus = [];
|
||||
this.selectedTasksToBeDone = [];
|
||||
[this.selectedTaskProject] = this.projects;
|
||||
},
|
||||
|
@ -350,7 +325,6 @@ export default {
|
|||
email: usersToInviteByEmail,
|
||||
access_level: this.selectedAccessLevel,
|
||||
invite_source: this.source,
|
||||
areas_of_focus: this.areasOfFocusForPost,
|
||||
tasks_to_be_done: this.tasksToBeDoneForPost,
|
||||
tasks_project_id: this.tasksProjectForPost,
|
||||
};
|
||||
|
@ -361,7 +335,6 @@ export default {
|
|||
user_id: usersToAddById,
|
||||
access_level: this.selectedAccessLevel,
|
||||
invite_source: this.source,
|
||||
areas_of_focus: this.areasOfFocusForPost,
|
||||
tasks_to_be_done: this.tasksToBeDoneForPost,
|
||||
tasks_project_id: this.tasksProjectForPost,
|
||||
};
|
||||
|
@ -517,16 +490,6 @@ export default {
|
|||
</template>
|
||||
</gl-datepicker>
|
||||
</div>
|
||||
<div v-if="areasOfFocusEnabled">
|
||||
<label class="gl-mt-5">
|
||||
{{ $options.labels.areasOfFocusLabel }}
|
||||
</label>
|
||||
<gl-form-checkbox-group
|
||||
v-model="selectedAreasOfFocus"
|
||||
:options="areasOfFocusOptions"
|
||||
data-testid="area-of-focus-checks"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
|
||||
<label class="gl-mt-5">
|
||||
{{ $options.labels.members.tasksToBeDone.title }}
|
||||
|
|
|
@ -3,11 +3,6 @@ import { __, s__ } from '~/locale';
|
|||
export const SEARCH_DELAY = 200;
|
||||
|
||||
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
|
||||
export const MEMBER_AREAS_OF_FOCUS = {
|
||||
name: 'member_areas_of_focus',
|
||||
view: 'view',
|
||||
submit: 'submit',
|
||||
};
|
||||
export const INVITE_MEMBERS_FOR_TASK = {
|
||||
minimum_access_level: 30,
|
||||
name: 'invite_members_for_task',
|
||||
|
@ -77,9 +72,6 @@ export const READ_MORE_TEXT = s__(
|
|||
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
|
||||
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
|
||||
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
|
||||
export const AREAS_OF_FOCUS_LABEL = s__(
|
||||
'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
|
||||
);
|
||||
|
||||
export const MODAL_LABELS = {
|
||||
members: {
|
||||
|
@ -142,7 +134,6 @@ export const MODAL_LABELS = {
|
|||
inviteButtonText: INVITE_BUTTON_TEXT,
|
||||
cancelButtonText: CANCEL_BUTTON_TEXT,
|
||||
headerCloseLabel: HEADER_CLOSE_LABEL,
|
||||
areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
|
||||
};
|
||||
|
||||
export const LEARN_GITLAB = 'learn_gitlab';
|
||||
|
|
|
@ -40,10 +40,8 @@ export default function initInviteMembersModal() {
|
|||
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
|
||||
groupSelectFilter: el.dataset.groupsFilter,
|
||||
groupSelectParentId: parseInt(el.dataset.parentId, 10),
|
||||
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
|
||||
tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
|
||||
projects: JSON.parse(el.dataset.projects || '[]'),
|
||||
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
|
||||
usersFilter: el.dataset.usersFilter,
|
||||
filterId: parseInt(el.dataset.filterId, 10),
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
|
|||
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
|
||||
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
|
||||
import FeatureCard from './feature_card.vue';
|
||||
import TrainingProviderList from './training_provider_list.vue';
|
||||
import SectionLayout from './section_layout.vue';
|
||||
import UpgradeBanner from './upgrade_banner.vue';
|
||||
|
||||
|
@ -28,8 +29,28 @@ export const i18n = {
|
|||
securityTraining: s__('SecurityConfiguration|Security training'),
|
||||
};
|
||||
|
||||
// This will be removed and replaced with GraphQL query:
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/346480
|
||||
export const TRAINING_PROVIDERS = [
|
||||
{
|
||||
id: 101,
|
||||
name: __('Kontra'),
|
||||
description: __('Interactive developer security education.'),
|
||||
url: 'https://application.security/',
|
||||
isEnabled: false,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: __('SecureCodeWarrior'),
|
||||
description: __('Security training with guide and learning pathways.'),
|
||||
url: 'https://www.securecodewarrior.com/',
|
||||
isEnabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
TRAINING_PROVIDERS,
|
||||
components: {
|
||||
AutoDevOpsAlert,
|
||||
AutoDevOpsEnabledAlert,
|
||||
|
@ -43,6 +64,7 @@ export default {
|
|||
SectionLayout,
|
||||
UpgradeBanner,
|
||||
UserCalloutDismisser,
|
||||
TrainingProviderList,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['projectPath'],
|
||||
|
@ -240,7 +262,11 @@ export default {
|
|||
data-testid="vulnerability-management-tab"
|
||||
:title="$options.i18n.vulnerabilityManagement"
|
||||
>
|
||||
<section-layout :heading="$options.i18n.securityTraining" />
|
||||
<section-layout :heading="$options.i18n.securityTraining">
|
||||
<template #features>
|
||||
<training-provider-list :providers="$options.TRAINING_PROVIDERS" />
|
||||
</template>
|
||||
</section-layout>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</article>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { GlCard, GlToggle, GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlCard,
|
||||
GlToggle,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
providers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="gl-list-style-none gl-m-0 gl-p-0">
|
||||
<li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6">
|
||||
<gl-card>
|
||||
<div class="gl-display-flex">
|
||||
<gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
|
||||
<div class="gl-ml-5">
|
||||
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
|
||||
<p>
|
||||
{{ description }}
|
||||
<gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</gl-card>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
|
@ -3,6 +3,7 @@
|
|||
class ConfirmationsController < Devise::ConfirmationsController
|
||||
include AcceptsPendingInvitations
|
||||
include GitlabRecaptcha
|
||||
include OneTrustCSP
|
||||
|
||||
prepend_before_action :check_recaptcha, only: :create
|
||||
before_action :load_recaptcha, only: :new
|
||||
|
|
|
@ -8,12 +8,9 @@ module Repositories
|
|||
|
||||
attr_reader :authentication_result, :redirected_path
|
||||
|
||||
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
|
||||
delegate :authentication_abilities, to: :authentication_result, allow_nil: true
|
||||
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
|
||||
|
||||
alias_method :user, :actor
|
||||
alias_method :authenticated_user, :actor
|
||||
|
||||
# Git clients will not know what authenticity token to send along
|
||||
skip_around_action :set_session_storage
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
@ -22,8 +19,16 @@ module Repositories
|
|||
|
||||
feature_category :source_code_management
|
||||
|
||||
def authenticated_user
|
||||
authentication_result&.user || authentication_result&.deploy_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user
|
||||
authenticated_user
|
||||
end
|
||||
|
||||
def download_request?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
|
|
@ -35,13 +35,6 @@ module InviteMembersHelper
|
|||
default_access_level: Gitlab::Access::GUEST
|
||||
}
|
||||
|
||||
experiment(:member_areas_of_focus, user: current_user) do |e|
|
||||
e.publish_to_database
|
||||
|
||||
e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) }
|
||||
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
|
||||
end
|
||||
|
||||
if show_invite_members_for_task?(source)
|
||||
dataset.merge!(
|
||||
tasks_to_be_done_options: tasks_to_be_done_options.to_json,
|
||||
|
@ -55,26 +48,6 @@ module InviteMembersHelper
|
|||
|
||||
private
|
||||
|
||||
def member_areas_of_focus_options
|
||||
[
|
||||
{
|
||||
value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase')
|
||||
},
|
||||
{
|
||||
value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests')
|
||||
},
|
||||
{
|
||||
value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD')
|
||||
},
|
||||
{
|
||||
value: 'Configure security features', text: s_('InviteMembersModal|Configure security features')
|
||||
},
|
||||
{
|
||||
value: 'Other', text: s_('InviteMembersModal|Other')
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def users_filter_data(group)
|
||||
{}
|
||||
|
|
|
@ -9,7 +9,7 @@ module Ci
|
|||
#
|
||||
if build.enqueue
|
||||
build.tap do |build|
|
||||
build.update(user: current_user, job_variables_attributes: job_variables_attributes || [])
|
||||
build.update!(user: current_user, job_variables_attributes: job_variables_attributes || [])
|
||||
|
||||
AfterRequeueJobService.new(project, current_user).execute(build)
|
||||
end
|
||||
|
|
|
@ -92,7 +92,6 @@ module Members
|
|||
super
|
||||
|
||||
track_invite_source(member)
|
||||
track_areas_of_focus(member)
|
||||
end
|
||||
|
||||
def track_invite_source(member)
|
||||
|
@ -110,12 +109,6 @@ module Members
|
|||
member.invite? ? 'net_new_user' : 'existing_user'
|
||||
end
|
||||
|
||||
def track_areas_of_focus(member)
|
||||
areas_of_focus.each do |area_of_focus|
|
||||
Gitlab::Tracking.event(self.class.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def create_tasks_to_be_done
|
||||
return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
|
||||
|
||||
|
@ -128,10 +121,6 @@ module Members
|
|||
TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id))
|
||||
end
|
||||
|
||||
def areas_of_focus
|
||||
params[:areas_of_focus] || []
|
||||
end
|
||||
|
||||
def user_limit
|
||||
limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT)
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- user_email = "(#{params[:email]})" if params[:email].present?
|
||||
- request_link_start = '<a href="%{new_user_confirmation_path}">'.html_safe % { new_user_confirmation_path: new_user_confirmation_path }
|
||||
- request_link_end = '</a>'.html_safe
|
||||
- content_for :page_specific_javascripts do
|
||||
= render "layouts/one_trust"
|
||||
|
||||
.well-confirmation.gl-text-center.gl-mb-6
|
||||
%h1.gl-mt-0
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: member_areas_of_focus
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406
|
||||
milestone: '14.2'
|
||||
type: experiment
|
||||
group: group::expansion
|
||||
default_enabled: false
|
159
config/initializers/wikicloth_patch.rb
Normal file
159
config/initializers/wikicloth_patch.rb
Normal file
|
@ -0,0 +1,159 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'wikicloth'
|
||||
require 'wikicloth/wiki_buffer/var'
|
||||
|
||||
# Adds patch for changes in this PR: https://github.com/nricciar/wikicloth/pull/112/files
|
||||
#
|
||||
# That fix has already been merged, but the maintainers are not releasing new versions, so we
|
||||
# need to patch it here.
|
||||
#
|
||||
# If they ever do release a version, then we can remove this file.
|
||||
#
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/334056#note_745336618
|
||||
|
||||
# Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth
|
||||
raise 'New version of WikiCloth detected, please remove this patch' unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1')
|
||||
|
||||
# rubocop:disable Style/ClassAndModuleChildren
|
||||
# rubocop:disable Layout/SpaceAroundEqualsInParameterDefault
|
||||
# rubocop:disable Style/HashSyntax
|
||||
# rubocop:disable Layout/SpaceAfterComma
|
||||
# rubocop:disable Style/RescueStandardError
|
||||
# rubocop:disable Rails/Output
|
||||
# rubocop:disable Style/MethodCallWithoutArgsParentheses
|
||||
# rubocop:disable Layout/EmptyLinesAroundClassBody
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
# rubocop:disable Cop/LineBreakAroundConditionalBlock
|
||||
# rubocop:disable Layout/EmptyLineAfterGuardClause
|
||||
# rubocop:disable Performance/ReverseEach
|
||||
# rubocop:disable Style/BlockDelimiters
|
||||
# rubocop:disable Cop/LineBreakAroundConditionalBlock
|
||||
# rubocop:disable Layout/MultilineBlockLayout
|
||||
# rubocop:disable Layout/BlockEndNewline
|
||||
module WikiCloth
|
||||
class WikiCloth
|
||||
def render(opt={})
|
||||
self.options = { :noedit => false, :locale => I18n.default_locale, :fast => true, :output => :html, :link_handler => self.link_handler,
|
||||
:params => self.params, :sections => self.sections }.merge(self.options).merge(opt)
|
||||
self.options[:link_handler].params = options[:params]
|
||||
|
||||
I18n.locale = self.options[:locale]
|
||||
|
||||
data = self.sections.collect { |s| s.render(self.options) }.join
|
||||
|
||||
# This is the first patched line from:
|
||||
# https://github.com/nricciar/wikicloth/pull/112/files#diff-eed3de11b953105f9181a6859d58f52af8912d28525fd2a289f8be184e66f531R69
|
||||
data.gsub!(/<!--.*?-->/m,"")
|
||||
|
||||
data << "\n" if data.last(1) != "\n"
|
||||
data << "garbage"
|
||||
|
||||
buffer = WikiBuffer.new("",options)
|
||||
|
||||
begin
|
||||
if self.options[:fast]
|
||||
until data.empty?
|
||||
case data
|
||||
when /\A\w+/
|
||||
data = $'
|
||||
@current_row += $&.length
|
||||
buffer.add_word($&)
|
||||
when /\A[^\w]+(\w|)/m
|
||||
data = $'
|
||||
$&.each_char { |c| add_current_char(buffer,c) }
|
||||
end
|
||||
end
|
||||
else
|
||||
data.each_char { |c| add_current_char(buffer,c) }
|
||||
end
|
||||
rescue => err
|
||||
debug_tree = buffer.buffers.collect { |b| b.debug }.join("-->")
|
||||
puts I18n.t("unknown error on line", :line => @current_line, :row => @current_row, :tree => debug_tree)
|
||||
raise err
|
||||
end
|
||||
|
||||
buffer.eof()
|
||||
buffer.send("to_#{self.options[:output]}")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class WikiBuffer::Var < WikiBuffer
|
||||
def to_html
|
||||
return "" if will_not_be_rendered
|
||||
|
||||
if self.is_function?
|
||||
if Extension.function_exists?(function_name)
|
||||
return Extension.functions[function_name][:klass].new(@options).instance_exec( params.collect { |p| p.strip }, &Extension.functions[function_name][:block] ).to_s
|
||||
end
|
||||
ret = default_functions(function_name,params.collect { |p| p.strip })
|
||||
ret ||= @options[:link_handler].function(function_name, params.collect { |p| p.strip })
|
||||
ret.to_s
|
||||
elsif self.is_param?
|
||||
ret = nil
|
||||
@options[:buffer].buffers.reverse.each do |b|
|
||||
ret = b.get_param(params[0],params[1]) if b.instance_of?(WikiBuffer::HTMLElement) && b.element_name == "template"
|
||||
break unless ret.nil?
|
||||
end
|
||||
ret.to_s
|
||||
else
|
||||
# put template at beginning of buffer
|
||||
template_stack = @options[:buffer].buffers.collect { |b| b.get_param("__name") if b.instance_of?(WikiBuffer::HTMLElement) &&
|
||||
b.element_name == "template" }.compact
|
||||
if template_stack.last == params[0]
|
||||
debug_tree = @options[:buffer].buffers.collect { |b| b.debug }.join("-->")
|
||||
"<span class=\"error\">#{I18n.t('template loop detected', :tree => debug_tree)}</span>"
|
||||
else
|
||||
key = params[0].to_s.strip
|
||||
key_options = params[1..-1].collect { |p| p.is_a?(Hash) ? { :name => p[:name].strip, :value => p[:value].strip } : p.strip }
|
||||
key_options ||= []
|
||||
key_digest = Digest::MD5.hexdigest(key_options.to_a.sort {|x,y| (x.is_a?(Hash) ? x[:name] : x) <=> (y.is_a?(Hash) ? y[:name] : y) }.inspect)
|
||||
|
||||
return @options[:params][key] if @options[:params].has_key?(key)
|
||||
# if we have a valid cache fragment use it
|
||||
return @options[:cache][key][key_digest] unless @options[:cache].nil? || @options[:cache][key].nil? || @options[:cache][key][key_digest].nil?
|
||||
|
||||
ret = @options[:link_handler].include_resource(key,key_options).to_s
|
||||
|
||||
# This is the second patched line from:
|
||||
# https://github.com/nricciar/wikicloth/pull/112/files#diff-f262faf4fadb222cca87185be0fb65b3f49659abc840794cc83a736d41310fb1R83
|
||||
ret.gsub!(/<!--.*?-->/m,"") unless ret.frozen?
|
||||
|
||||
count = 0
|
||||
tag_attr = key_options.collect { |p|
|
||||
if p.instance_of?(Hash)
|
||||
"#{p[:name]}=\"#{p[:value].gsub(/"/,'"')}\""
|
||||
else
|
||||
count += 1
|
||||
"#{count}=\"#{p.gsub(/"/,'"')}\""
|
||||
end
|
||||
}.join(" ")
|
||||
|
||||
self.data = ret.blank? ? "" : "<template __name=\"#{key}\" __hash=\"#{key_digest}\" #{tag_attr}>#{ret}</template>"
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/ClassAndModuleChildren
|
||||
# rubocop:enable Layout/SpaceAroundEqualsInParameterDefault
|
||||
# rubocop:enable Style/HashSyntax
|
||||
# rubocop:enable Layout/SpaceAfterComma
|
||||
# rubocop:enable Style/RescueStandardError
|
||||
# rubocop:enable Rails/Output
|
||||
# rubocop:enable Style/MethodCallWithoutArgsParentheses
|
||||
# rubocop:enable Layout/EmptyLinesAroundClassBody
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
# rubocop:enable Cop/LineBreakAroundConditionalBlock
|
||||
# rubocop:enable Layout/EmptyLineAfterGuardClause
|
||||
# rubocop:enable Performance/ReverseEach
|
||||
# rubocop:enable Style/BlockDelimiters
|
||||
# rubocop:enable Cop/LineBreakAroundConditionalBlock
|
||||
# rubocop:enable Layout/MultilineBlockLayout
|
||||
# rubocop:enable Layout/BlockEndNewline
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMigratedToNewStructureColumnToVulnerabilityOccurrences < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
add_column :vulnerability_occurrences, :migrated_to_new_structure, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnVulnerabilityOccurrencesMigratedToNewStructureColumn < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_vulnerability_occurrences_on_migrated_to_new_structure'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :vulnerability_occurrences, [:migrated_to_new_structure, :id], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropClustersApplicationsRunnersCiRunnersFk < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36')
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36', column: :runner_id, target_column: :id, on_delete: 'set null')
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211129151155
Normal file
1
db/schema_migrations/20211129151155
Normal file
|
@ -0,0 +1 @@
|
|||
c1ba97f01fca6330628090010abb54220c0d057514386c6bb867c1b6f13f252c
|
1
db/schema_migrations/20211129151832
Normal file
1
db/schema_migrations/20211129151832
Normal file
|
@ -0,0 +1 @@
|
|||
c6d257f635049f88cd6efba903c9384a0a1af23b3c8fe6fa7f0842dcdf9f7e39
|
1
db/schema_migrations/20211201101541
Normal file
1
db/schema_migrations/20211201101541
Normal file
|
@ -0,0 +1 @@
|
|||
277cfcd1002e32c6cd664d6c0b6a7cbdf2ed7e5242e46dbddc4f99b0e8422361
|
|
@ -20828,6 +20828,7 @@ CREATE TABLE vulnerability_occurrences (
|
|||
cve text,
|
||||
location jsonb,
|
||||
detection_method smallint DEFAULT 0 NOT NULL,
|
||||
migrated_to_new_structure boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_4a3a60f2ba CHECK ((char_length(solution) <= 7000)),
|
||||
CONSTRAINT check_ade261da6b CHECK ((char_length(description) <= 15000)),
|
||||
CONSTRAINT check_df6dd20219 CHECK ((char_length(message) <= 3000)),
|
||||
|
@ -27780,6 +27781,8 @@ CREATE INDEX index_vulnerability_occurrences_on_location_cluster_id ON vulnerabi
|
|||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_location_image ON vulnerability_occurrences USING gin (((location -> 'image'::text))) WHERE (report_type = ANY (ARRAY[2, 7]));
|
||||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_migrated_to_new_structure ON vulnerability_occurrences USING btree (migrated_to_new_structure, id);
|
||||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON vulnerability_occurrences USING btree (primary_identifier_id);
|
||||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_project_fingerprint ON vulnerability_occurrences USING btree (project_fingerprint);
|
||||
|
@ -28891,9 +28894,6 @@ ALTER TABLE ONLY deployments
|
|||
ALTER TABLE ONLY epics
|
||||
ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_runners
|
||||
ADD CONSTRAINT fk_02de2ded36 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY incident_management_escalation_rules
|
||||
ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Geo Nodes API **(PREMIUM SELF)**
|
||||
|
||||
To interact with Geo node endpoints, you need to authenticate yourself as an
|
||||
To interact with Geo node endpoints, you must authenticate yourself as an
|
||||
administrator.
|
||||
|
||||
## Create a new Geo node
|
||||
|
@ -26,7 +26,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------------------| ------- | -------- | -----------------------------------------------------------------|
|
||||
| `primary` | boolean | no | Specifying whether this node will be primary. Defaults to false. |
|
||||
| `primary` | boolean | no | Specifying whether this node should be primary. Defaults to false. |
|
||||
| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. Defaults to true. |
|
||||
| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url` |
|
||||
| `url` | string | yes | The user-facing URL for the Geo node. |
|
||||
|
@ -35,11 +35,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/
|
|||
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. Defaults to 25. |
|
||||
| `verification_max_capacity` | integer | no | Control the maximum concurrency of repository verification for this node. Defaults to 100. |
|
||||
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. Defaults to 10. |
|
||||
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. Defaults to false. |
|
||||
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. Defaults to false. |
|
||||
| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
|
||||
| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
|
||||
| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
|
||||
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. |
|
||||
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
|
||||
|
||||
Example response:
|
||||
|
||||
|
@ -199,11 +199,11 @@ PUT /geo_nodes/:id
|
|||
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
|
||||
| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
|
||||
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
|
||||
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. |
|
||||
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. |
|
||||
| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
|
||||
| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
|
||||
| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
|
||||
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. |
|
||||
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
|
||||
|
||||
Example response:
|
||||
|
||||
|
@ -241,7 +241,7 @@ Example response:
|
|||
Removes the Geo node.
|
||||
|
||||
NOTE:
|
||||
Only a Geo primary node will accept this request.
|
||||
Only a Geo primary node accepts this request.
|
||||
|
||||
```plaintext
|
||||
DELETE /geo_nodes/:id
|
||||
|
|
|
@ -42,7 +42,6 @@ POST /projects/:id/invitations
|
|||
| `access_level` | integer | yes | A valid access level |
|
||||
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
|
||||
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
|
||||
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
|
||||
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 |
|
||||
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 |
|
||||
|
||||
|
|
|
@ -429,7 +429,6 @@ POST /projects/:id/members
|
|||
| `access_level` | integer | yes | A valid access level |
|
||||
| `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` |
|
||||
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
|
||||
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
|
||||
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
|
||||
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
|
||||
|
||||
|
|
|
@ -313,12 +313,8 @@ As in other list types, click the trash icon to remove a list.
|
|||
|
||||
### Iteration lists **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
|
||||
administrator to [disable the `iteration_board_lists` flag](../../administration/feature_flags.md).
|
||||
On GitLab.com, this feature is available.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75404) in GitLab 14.6. Feature flag `iteration_board_lists` removed.
|
||||
|
||||
You're also able to create lists of an iteration.
|
||||
These lists filter issues by the assigned iteration.
|
||||
|
|
|
@ -24,7 +24,6 @@ module API
|
|||
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
|
||||
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
|
||||
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
|
||||
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
|
||||
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
|
||||
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
|
||||
end
|
||||
|
|
|
@ -95,7 +95,6 @@ module API
|
|||
requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.'
|
||||
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
|
||||
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api'
|
||||
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
|
||||
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
|
||||
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
|
||||
end
|
||||
|
|
|
@ -873,6 +873,9 @@ msgstr ""
|
|||
msgid "%{scope} results for term '%{term}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{search} %{description} %{scope}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{seconds}s"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16077,6 +16080,51 @@ msgstr ""
|
|||
msgid "Global notification settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Issues I've created"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Issues assigned to me"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Merge requests I've created"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Merge requests assigned to me"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Merge requests that I'm a reviewer"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Search GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Search or jump to..."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Search results are loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Type and press the enter key to submit search."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Type for new suggestions to appear below."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|in all GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|in group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|in project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go Back"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18843,6 +18891,9 @@ msgstr ""
|
|||
msgid "Integrations|can't exceed %{recipients_limit}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Interactive developer security education."
|
||||
msgstr ""
|
||||
|
||||
msgid "Interactive mode"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19071,21 +19122,9 @@ msgstr ""
|
|||
msgid "InviteMembersModal|Close invite team members"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Collaborate on open issues and merge requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Configure CI/CD"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Configure security features"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Congratulations on creating your project, you're almost there!"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Contribute to the codebase"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19110,9 +19149,6 @@ msgstr ""
|
|||
msgid "InviteMembersModal|Members were successfully added"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Other"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Search for a group to invite"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19131,9 +19167,6 @@ msgstr ""
|
|||
msgid "InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
|
||||
msgstr ""
|
||||
|
||||
|
@ -19455,9 +19488,6 @@ msgstr ""
|
|||
msgid "Issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues I've created"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues Rate Limits"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19467,9 +19497,6 @@ msgstr ""
|
|||
msgid "Issues are being rebalanced at the moment, so manual reordering is disabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues assigned to me"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
|
||||
msgstr ""
|
||||
|
||||
|
@ -20196,6 +20223,9 @@ msgstr ""
|
|||
msgid "Ki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kontra"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kroki"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21859,18 +21889,9 @@ msgstr ""
|
|||
msgid "Merge requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge requests I've created"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge requests assigned to me"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge requests that I'm a reviewer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge the branch and fix any conflicts that come up"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30671,6 +30692,9 @@ msgstr ""
|
|||
msgid "Secure token that identifies an external storage request."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecureCodeWarrior"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30695,6 +30719,9 @@ msgstr ""
|
|||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security training with guide and learning pathways."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
|
||||
msgstr ""
|
||||
|
||||
|
@ -36606,6 +36633,9 @@ msgstr ""
|
|||
msgid "Track your GitLab projects with GitLab for Slack."
|
||||
msgstr ""
|
||||
|
||||
msgid "Training mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
|
@ -41231,18 +41261,9 @@ msgstr ""
|
|||
msgid "in"
|
||||
msgstr ""
|
||||
|
||||
msgid "in all GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "in group"
|
||||
msgstr ""
|
||||
|
||||
msgid "in group %{link_to_group}"
|
||||
msgstr ""
|
||||
|
||||
msgid "in project"
|
||||
msgstr ""
|
||||
|
||||
msgid "in project %{link_to_project}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -90,6 +90,14 @@ RSpec.describe Repositories::GitHttpController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is a deploy token' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
let(:container) { project }
|
||||
let(:user) { create(:deploy_token, :project, projects: [project]) }
|
||||
let(:access_checker_class) { Gitlab::GitAccess }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository container is a project wiki' do
|
||||
|
|
|
@ -85,33 +85,6 @@ RSpec.describe 'Groups > Members > Manage members' do
|
|||
property: 'existing_user',
|
||||
user: user1
|
||||
)
|
||||
expect_no_snowplow_event(
|
||||
category: 'Members::CreateService',
|
||||
action: 'area_of_focus'
|
||||
)
|
||||
end
|
||||
|
||||
it 'adds a user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
|
||||
stub_experiments(member_areas_of_focus: :candidate)
|
||||
group.add_owner(user1)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
||||
invite_member(user2.name, role: 'Reporter', area_of_focus: true)
|
||||
wait_for_requests
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Members::CreateService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Contribute to the codebase',
|
||||
property: group.members.last.id.to_s
|
||||
)
|
||||
expect_snowplow_event(
|
||||
category: 'Members::CreateService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Collaborate on open issues and merge requests',
|
||||
property: group.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'do not disclose email addresses', :js do
|
||||
|
@ -221,36 +194,9 @@ RSpec.describe 'Groups > Members > Manage members' do
|
|||
property: 'net_new_user',
|
||||
user: user1
|
||||
)
|
||||
expect_no_snowplow_event(
|
||||
category: 'Members::CreateService',
|
||||
action: 'area_of_focus'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'invite user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
|
||||
stub_experiments(member_areas_of_focus: :candidate)
|
||||
group.add_owner(user1)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
||||
invite_member('test@example.com', role: 'Reporter', area_of_focus: true)
|
||||
wait_for_requests
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Members::InviteService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Contribute to the codebase',
|
||||
property: group.members.last.id.to_s
|
||||
)
|
||||
expect_snowplow_event(
|
||||
category: 'Members::InviteService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Collaborate on open issues and merge requests',
|
||||
property: group.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
|
||||
context 'when user is a guest' do
|
||||
before do
|
||||
group.add_guest(user1)
|
||||
|
|
23
spec/features/one_trust_spec.rb
Normal file
23
spec/features/one_trust_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'OneTrust' do
|
||||
context 'almost there page' do
|
||||
context 'when OneTrust is enabled' do
|
||||
let_it_be(:onetrust_url) { 'https://*.onetrust.com' }
|
||||
let_it_be(:one_trust_id) { SecureRandom.uuid }
|
||||
|
||||
before do
|
||||
stub_config(extra: { one_trust_id: one_trust_id })
|
||||
stub_feature_flags(ecomm_instrumentation: true)
|
||||
visit users_almost_there_path
|
||||
end
|
||||
|
||||
it 'has the OneTrust CSP settings', :aggregate_failures do
|
||||
expect(response_headers['Content-Security-Policy']).to include("#{onetrust_url}")
|
||||
expect(page.html).to include("https://cdn.cookielaw.org/consent/#{one_trust_id}/OtAutoBlock.js")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ import HeaderSearchApp from '~/header_search/components/app.vue';
|
|||
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
|
||||
import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue';
|
||||
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
|
||||
import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants';
|
||||
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
|
||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
@ -14,6 +15,7 @@ import {
|
|||
MOCK_SEARCH_QUERY,
|
||||
MOCK_USERNAME,
|
||||
MOCK_DEFAULT_SEARCH_OPTIONS,
|
||||
MOCK_SCOPED_SEARCH_OPTIONS,
|
||||
} from '../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -59,11 +61,26 @@ describe('HeaderSearchApp', () => {
|
|||
const findHeaderSearchAutocompleteItems = () =>
|
||||
wrapper.findComponent(HeaderSearchAutocompleteItems);
|
||||
const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation);
|
||||
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
|
||||
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
|
||||
|
||||
describe('template', () => {
|
||||
it('always renders Header Search Input', () => {
|
||||
createComponent();
|
||||
expect(findHeaderSearchInput().exists()).toBe(true);
|
||||
describe('always renders', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('Header Search Input', () => {
|
||||
expect(findHeaderSearchInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Search Input Description', () => {
|
||||
expect(findSearchInputDescription().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Search Results Description', () => {
|
||||
expect(findSearchResultsDescription().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
@ -77,7 +94,7 @@ describe('HeaderSearchApp', () => {
|
|||
beforeEach(() => {
|
||||
window.gon.current_username = username;
|
||||
createComponent();
|
||||
wrapper.setData({ showDropdown });
|
||||
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
|
||||
});
|
||||
|
||||
it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
|
||||
|
@ -123,6 +140,53 @@ describe('HeaderSearchApp', () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe.each`
|
||||
username | showDropdown | expectedDesc
|
||||
${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
|
||||
${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
|
||||
${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
|
||||
${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
|
||||
`('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
|
||||
describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
|
||||
beforeEach(() => {
|
||||
window.gon.current_username = username;
|
||||
createComponent();
|
||||
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
|
||||
});
|
||||
|
||||
it(`sets description to ${expectedDesc}`, () => {
|
||||
expect(findSearchInputDescription().text()).toBe(expectedDesc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
username | showDropdown | search | loading | searchOptions | expectedDesc
|
||||
${null} | ${true} | ${''} | ${false} | ${[]} | ${''}
|
||||
${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''}
|
||||
${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
|
||||
${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
|
||||
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
|
||||
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading}
|
||||
`(
|
||||
'Search Results Description',
|
||||
({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
|
||||
describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${
|
||||
Boolean(username) && showDropdown
|
||||
}`, () => {
|
||||
beforeEach(() => {
|
||||
window.gon.current_username = username;
|
||||
createComponent({ search, loading }, { searchOptions: () => searchOptions });
|
||||
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
|
||||
});
|
||||
|
||||
it(`sets description to ${expectedDesc}`, () => {
|
||||
expect(findSearchResultsDescription().text()).toBe(expectedDesc);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
|
|
|
@ -110,11 +110,11 @@ describe('HeaderSearchAutocompleteItems', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
currentFocusedOption | isFocused
|
||||
${null} | ${false}
|
||||
${{ html_id: 'not-a-match' }} | ${false}
|
||||
${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
|
||||
currentFocusedOption | isFocused | ariaSelected
|
||||
${null} | ${false} | ${undefined}
|
||||
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
|
||||
${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
|
||||
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, {}, { currentFocusedOption });
|
||||
|
@ -123,6 +123,10 @@ describe('HeaderSearchAutocompleteItems', () => {
|
|||
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
|
||||
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
|
||||
});
|
||||
|
||||
it(`sets "aria-selected to ${ariaSelected}`, () => {
|
||||
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,11 +83,11 @@ describe('HeaderSearchDefaultItems', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
currentFocusedOption | isFocused
|
||||
${null} | ${false}
|
||||
${{ html_id: 'not-a-match' }} | ${false}
|
||||
${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
|
||||
currentFocusedOption | isFocused | ariaSelected
|
||||
${null} | ${false} | ${undefined}
|
||||
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
|
||||
${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
|
||||
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, { currentFocusedOption });
|
||||
|
@ -96,6 +96,10 @@ describe('HeaderSearchDefaultItems', () => {
|
|||
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
|
||||
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
|
||||
});
|
||||
|
||||
it(`sets "aria-selected to ${ariaSelected}`, () => {
|
||||
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,6 +37,8 @@ describe('HeaderSearchScopedItems', () => {
|
|||
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findFirstDropdownItem = () => findDropdownItems().at(0);
|
||||
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
|
||||
const findDropdownItemAriaLabels = () =>
|
||||
findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label')));
|
||||
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
|
||||
|
||||
describe('template', () => {
|
||||
|
@ -56,6 +58,13 @@ describe('HeaderSearchScopedItems', () => {
|
|||
expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
|
||||
});
|
||||
|
||||
it('renders aria-labels correctly', () => {
|
||||
const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
|
||||
trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`),
|
||||
);
|
||||
expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels);
|
||||
});
|
||||
|
||||
it('renders links correctly', () => {
|
||||
const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
|
||||
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
|
||||
|
@ -63,11 +72,11 @@ describe('HeaderSearchScopedItems', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
currentFocusedOption | isFocused
|
||||
${null} | ${false}
|
||||
${{ html_id: 'not-a-match' }} | ${false}
|
||||
${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
|
||||
currentFocusedOption | isFocused | ariaSelected
|
||||
${null} | ${false} | ${undefined}
|
||||
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
|
||||
${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
|
||||
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
|
||||
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, { currentFocusedOption });
|
||||
|
@ -76,6 +85,10 @@ describe('HeaderSearchScopedItems', () => {
|
|||
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
|
||||
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
|
||||
});
|
||||
|
||||
it(`sets "aria-selected to ${ariaSelected}`, () => {
|
||||
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
GlSprintf,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlFormCheckboxGroup,
|
||||
} from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
|
@ -19,7 +18,6 @@ import ModalConfetti from '~/invite_members/components/confetti.vue';
|
|||
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
|
||||
import {
|
||||
INVITE_MEMBERS_IN_COMMENT,
|
||||
MEMBER_AREAS_OF_FOCUS,
|
||||
INVITE_MEMBERS_FOR_TASK,
|
||||
CANCEL_BUTTON_TEXT,
|
||||
INVITE_BUTTON_TEXT,
|
||||
|
@ -52,12 +50,7 @@ const inviteeType = 'members';
|
|||
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
|
||||
const defaultAccessLevel = 10;
|
||||
const inviteSource = 'unknown';
|
||||
const noSelectionAreasOfFocus = ['no_selection'];
|
||||
const helpLink = 'https://example.com';
|
||||
const areasOfFocusOptions = [
|
||||
{ text: 'area1', value: 'area1' },
|
||||
{ text: 'area2', value: 'area2' },
|
||||
];
|
||||
const tasksToBeDoneOptions = [
|
||||
{ text: 'First task', value: 'first' },
|
||||
{ text: 'Second task', value: 'second' },
|
||||
|
@ -96,9 +89,7 @@ const createComponent = (data = {}, props = {}) => {
|
|||
isProject,
|
||||
inviteeType,
|
||||
accessLevels,
|
||||
areasOfFocusOptions,
|
||||
defaultAccessLevel,
|
||||
noSelectionAreasOfFocus,
|
||||
tasksToBeDoneOptions,
|
||||
projects,
|
||||
helpLink,
|
||||
|
@ -164,7 +155,6 @@ describe('InviteMembersModal', () => {
|
|||
const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
|
||||
const membersFormGroupDescription = () => findMembersFormGroup().props('description');
|
||||
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
|
||||
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
|
||||
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
|
||||
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
|
||||
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
|
||||
|
@ -215,21 +205,6 @@ describe('InviteMembersModal', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('rendering the areas_of_focus', () => {
|
||||
it('renders the areas_of_focus checkboxes', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAreaofFocusCheckBoxGroup().props('options')).toBe(areasOfFocusOptions);
|
||||
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render the areas_of_focus checkboxes', () => {
|
||||
createComponent({}, { areasOfFocusOptions: [] });
|
||||
|
||||
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering the tasks to be done', () => {
|
||||
const setupComponent = (
|
||||
extraData = {},
|
||||
|
@ -442,20 +417,6 @@ describe('InviteMembersModal', () => {
|
|||
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
|
||||
const expectedSyntaxError = 'email contains an invalid email address';
|
||||
|
||||
it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => {
|
||||
const spy = jest.spyOn(Api, 'addGroupMembersByUserId');
|
||||
const expectedFocus = [areasOfFocusOptions[0].value];
|
||||
createComponent({ newUsersToInvite: [user1] });
|
||||
|
||||
findAreaofFocusCheckBoxGroup().vm.$emit('input', expectedFocus);
|
||||
clickInviteButton();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
user1.id.toString(),
|
||||
expect.objectContaining({ areas_of_focus: expectedFocus }),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when inviting an existing user to group by user ID', () => {
|
||||
const postData = {
|
||||
user_id: '1,2',
|
||||
|
@ -463,7 +424,6 @@ describe('InviteMembersModal', () => {
|
|||
expires_at: undefined,
|
||||
invite_source: inviteSource,
|
||||
format: 'json',
|
||||
areas_of_focus: noSelectionAreasOfFocus,
|
||||
tasks_to_be_done: [],
|
||||
tasks_project_id: '',
|
||||
};
|
||||
|
@ -476,16 +436,6 @@ describe('InviteMembersModal', () => {
|
|||
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
|
||||
});
|
||||
|
||||
it('includes the non-default selected areas of focus', () => {
|
||||
const focus = ['abc'];
|
||||
const updatedPostData = { ...postData, areas_of_focus: focus };
|
||||
wrapper.setData({ selectedAreasOfFocus: focus });
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, updatedPostData);
|
||||
});
|
||||
|
||||
describe('when triggered from regular mounting', () => {
|
||||
beforeEach(() => {
|
||||
clickInviteButton();
|
||||
|
@ -661,7 +611,6 @@ describe('InviteMembersModal', () => {
|
|||
expires_at: undefined,
|
||||
email: 'email@example.com',
|
||||
invite_source: inviteSource,
|
||||
areas_of_focus: noSelectionAreasOfFocus,
|
||||
tasks_to_be_done: [],
|
||||
tasks_project_id: '',
|
||||
format: 'json',
|
||||
|
@ -675,16 +624,6 @@ describe('InviteMembersModal', () => {
|
|||
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
|
||||
});
|
||||
|
||||
it('includes the non-default selected areas of focus', () => {
|
||||
const focus = ['abc'];
|
||||
const updatedPostData = { ...postData, areas_of_focus: focus };
|
||||
wrapper.setData({ selectedAreasOfFocus: focus });
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, updatedPostData);
|
||||
});
|
||||
|
||||
describe('when triggered from regular mounting', () => {
|
||||
beforeEach(() => {
|
||||
clickInviteButton();
|
||||
|
@ -792,7 +731,6 @@ describe('InviteMembersModal', () => {
|
|||
access_level: defaultAccessLevel,
|
||||
expires_at: undefined,
|
||||
invite_source: inviteSource,
|
||||
areas_of_focus: noSelectionAreasOfFocus,
|
||||
format: 'json',
|
||||
tasks_to_be_done: [],
|
||||
tasks_project_id: '',
|
||||
|
@ -951,30 +889,12 @@ describe('InviteMembersModal', () => {
|
|||
expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT);
|
||||
});
|
||||
|
||||
it('tracks the view for areas_of_focus', () => {
|
||||
eventHub.$emit('openModal', { inviteeType: 'members' });
|
||||
|
||||
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
|
||||
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view);
|
||||
});
|
||||
|
||||
it('tracks the view for learn_gitlab source', () => {
|
||||
eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
|
||||
|
||||
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
|
||||
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
|
||||
});
|
||||
|
||||
it('tracks the invite for areas_of_focus', () => {
|
||||
eventHub.$emit('openModal', { inviteeType: 'members' });
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
|
||||
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
|
||||
MEMBER_AREAS_OF_FOCUS.submit,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
|||
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
|
||||
import stubChildren from 'helpers/stub_children';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
|
||||
import SecurityConfigurationApp, {
|
||||
i18n,
|
||||
TRAINING_PROVIDERS,
|
||||
} from '~/security_configuration/components/app.vue';
|
||||
import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
|
||||
import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
|
||||
import {
|
||||
|
@ -20,6 +23,7 @@ import {
|
|||
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
|
||||
} from '~/security_configuration/components/constants';
|
||||
import FeatureCard from '~/security_configuration/components/feature_card.vue';
|
||||
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
||||
|
||||
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
|
||||
import {
|
||||
|
@ -78,6 +82,7 @@ describe('App component', () => {
|
|||
const findTabs = () => wrapper.findAllComponents(GlTab);
|
||||
const findByTestId = (id) => wrapper.findByTestId(id);
|
||||
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
|
||||
const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
|
||||
const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
|
||||
const findLink = ({ href, text, container = wrapper }) => {
|
||||
const selector = `a[href="${href}"]`;
|
||||
|
@ -180,6 +185,10 @@ describe('App component', () => {
|
|||
expect(findComplianceViewHistoryLink().exists()).toBe(false);
|
||||
expect(findSecurityViewHistoryLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders training provider list with correct props', () => {
|
||||
expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manage via MR Error Alert', () => {
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { GlLink, GlToggle, GlCard } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
||||
import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
providers: TRAINING_PROVIDERS,
|
||||
};
|
||||
|
||||
describe('TrainingProviderList component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(TrainingProviderList, {
|
||||
propsData: {
|
||||
...DEFAULT_PROPS,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findCards = () => wrapper.findAllComponents(GlCard);
|
||||
const findLinks = () => wrapper.findAllComponents(GlLink);
|
||||
const findToggles = () => wrapper.findAllComponents(GlToggle);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('basic structure', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders correct amount of cards', () => {
|
||||
expect(findCards()).toHaveLength(DEFAULT_PROPS.providers.length);
|
||||
});
|
||||
|
||||
DEFAULT_PROPS.providers.forEach(({ name, description, url, isEnabled }, index) => {
|
||||
it(`shows the name for card ${index}`, () => {
|
||||
expect(findCards().at(index).text()).toContain(name);
|
||||
});
|
||||
|
||||
it(`shows the description for card ${index}`, () => {
|
||||
expect(findCards().at(index).text()).toContain(description);
|
||||
});
|
||||
|
||||
it(`shows the learn more link for card ${index}`, () => {
|
||||
expect(findLinks().at(index).attributes()).toEqual({
|
||||
target: '_blank',
|
||||
href: url,
|
||||
});
|
||||
});
|
||||
|
||||
it(`shows the toggle with the correct value for card ${index}`, () => {
|
||||
expect(findToggles().at(index).props('value')).toEqual(isEnabled);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,52 +16,14 @@ RSpec.describe InviteMembersHelper do
|
|||
end
|
||||
|
||||
describe '#common_invite_modal_dataset' do
|
||||
context 'when member_areas_of_focus is enabled', :experiment do
|
||||
context 'with control experience' do
|
||||
before do
|
||||
stub_experiments(member_areas_of_focus: :control)
|
||||
end
|
||||
it 'has expected common attributes' do
|
||||
attributes = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
default_access_level: Gitlab::Access::GUEST
|
||||
}
|
||||
|
||||
it 'has expected attributes' do
|
||||
attributes = {
|
||||
areas_of_focus_options: [],
|
||||
no_selection_areas_of_focus: []
|
||||
}
|
||||
|
||||
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with candidate experience' do
|
||||
before do
|
||||
stub_experiments(member_areas_of_focus: :candidate)
|
||||
end
|
||||
|
||||
it 'has expected attributes', :aggregate_failures do
|
||||
output = helper.common_invite_modal_dataset(project)
|
||||
|
||||
expect(output[:no_selection_areas_of_focus]).to eq ['no_selection']
|
||||
expect(Gitlab::Json.parse(output[:areas_of_focus_options]).first['value']).to eq 'Contribute to the codebase'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when member_areas_of_focus is disabled' do
|
||||
before do
|
||||
stub_feature_flags(member_areas_of_focus: false)
|
||||
end
|
||||
|
||||
it 'has expected attributes' do
|
||||
attributes = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
default_access_level: Gitlab::Access::GUEST,
|
||||
areas_of_focus_options: [],
|
||||
no_selection_areas_of_focus: []
|
||||
}
|
||||
|
||||
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
|
||||
end
|
||||
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
|
||||
end
|
||||
|
||||
context 'tasks_to_be_done' do
|
||||
|
|
|
@ -152,20 +152,6 @@ RSpec.describe API::Invitations do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with areas_of_focus', :snowplow do
|
||||
it 'tracks the areas_of_focus from params' do
|
||||
post invitations_url(source, maintainer),
|
||||
params: { email: email, access_level: Member::DEVELOPER, areas_of_focus: 'Other' }
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Members::InviteService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Other',
|
||||
property: source.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tasks_to_be_done and tasks_project_id in the params' do
|
||||
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
|
||||
|
||||
|
|
|
@ -387,33 +387,6 @@ RSpec.describe API::Members do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with areas_of_focus considerations', :snowplow do
|
||||
let(:user_id) { stranger.id }
|
||||
|
||||
context 'when areas_of_focus is present in params' do
|
||||
it 'tracks the areas_of_focus' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||
params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' }
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Members::CreateService',
|
||||
action: 'area_of_focus',
|
||||
label: 'Other',
|
||||
property: source.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when areas_of_focus is not present in params' do
|
||||
it 'does not track the areas_of_focus' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||
params: { user_id: user_id, access_level: Member::DEVELOPER }
|
||||
|
||||
expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tasks_to_be_done and tasks_project_id in the params' do
|
||||
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
|
||||
|
||||
|
|
|
@ -79,12 +79,22 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
|
|||
{ key: 'second', secret_value: 'second' }]
|
||||
end
|
||||
|
||||
subject { service.execute(build, job_variables) }
|
||||
|
||||
it 'assigns the variables to the build' do
|
||||
service.execute(build, job_variables)
|
||||
subject
|
||||
|
||||
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
|
||||
end
|
||||
|
||||
context 'when variables are invalid' do
|
||||
let(:job_variables) { [{}] }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user defined variables are restricted' do
|
||||
before do
|
||||
project.update!(restrict_user_defined_variables: true)
|
||||
|
@ -96,7 +106,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
|
|||
end
|
||||
|
||||
it 'assigns the variables to the build' do
|
||||
service.execute(build, job_variables)
|
||||
subject
|
||||
|
||||
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
|
||||
end
|
||||
|
@ -104,8 +114,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
|
|||
|
||||
context 'when user is developer' do
|
||||
it 'raises an error' do
|
||||
expect { service.execute(build, job_variables) }
|
||||
.to raise_error Gitlab::Access::AccessDeniedError
|
||||
expect { subject }.to raise_error Gitlab::Access::AccessDeniedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -127,76 +127,6 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
|
|||
end
|
||||
end
|
||||
|
||||
context 'when tracking the areas of focus', :snowplow do
|
||||
context 'when areas_of_focus is not passed' do
|
||||
it 'does not track' do
|
||||
execute_service
|
||||
|
||||
expect_no_snowplow_event(category: described_class.name, action: 'area_of_focus')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when 1 areas_of_focus is passed' do
|
||||
let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: ['no_selection'] } }
|
||||
|
||||
it 'tracks the areas_of_focus from params' do
|
||||
execute_service
|
||||
|
||||
expect_snowplow_event(
|
||||
category: described_class.name,
|
||||
action: 'area_of_focus',
|
||||
label: 'no_selection',
|
||||
property: source.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
|
||||
context 'when passing many user ids' do
|
||||
let(:another_user) { create(:user) }
|
||||
let(:user_ids) { [member.id, another_user.id].join(',') }
|
||||
|
||||
it 'tracks the areas_of_focus from params' do
|
||||
execute_service
|
||||
|
||||
members = source.members.last(2)
|
||||
|
||||
expect_snowplow_event(
|
||||
category: described_class.name,
|
||||
action: 'area_of_focus',
|
||||
label: 'no_selection',
|
||||
property: members.first.id.to_s
|
||||
)
|
||||
expect_snowplow_event(
|
||||
category: described_class.name,
|
||||
action: 'area_of_focus',
|
||||
label: 'no_selection',
|
||||
property: members.last.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple areas_of_focus are passed' do
|
||||
let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: %w[no_selection Other] } }
|
||||
|
||||
it 'tracks the areas_of_focus from params' do
|
||||
execute_service
|
||||
|
||||
expect_snowplow_event(
|
||||
category: described_class.name,
|
||||
action: 'area_of_focus',
|
||||
label: 'no_selection',
|
||||
property: source.members.last.id.to_s
|
||||
)
|
||||
expect_snowplow_event(
|
||||
category: described_class.name,
|
||||
action: 'area_of_focus',
|
||||
label: 'Other',
|
||||
property: source.members.last.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assigning tasks to be done' do
|
||||
let(:additional_params) do
|
||||
{ invite_source: '_invite_source_', tasks_to_be_done: %w(ci code), tasks_project_id: source.id }
|
||||
|
|
|
@ -5,7 +5,7 @@ module Spec
|
|||
module Helpers
|
||||
module Features
|
||||
module InviteMembersModalHelper
|
||||
def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false)
|
||||
def invite_member(name, role: 'Guest', expires_at: nil)
|
||||
click_on 'Invite members'
|
||||
|
||||
page.within '[data-testid="invite-members-modal"]' do
|
||||
|
@ -14,7 +14,6 @@ module Spec
|
|||
wait_for_requests
|
||||
click_button name
|
||||
choose_options(role, expires_at)
|
||||
choose_area_of_focus if area_of_focus
|
||||
|
||||
click_button 'Invite'
|
||||
|
||||
|
@ -44,13 +43,6 @@ module Spec
|
|||
|
||||
fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at
|
||||
end
|
||||
|
||||
def choose_area_of_focus
|
||||
page.within '[data-testid="area-of-focus-checks"]' do
|
||||
check 'Contribute to the codebase'
|
||||
check 'Collaborate on open issues and merge requests'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,7 +50,8 @@ RSpec.shared_examples Repositories::GitHttpController do
|
|||
|
||||
context 'with authorized user' do
|
||||
before do
|
||||
request.headers.merge! auth_env(user.username, user.password, nil)
|
||||
password = user.try(:password) || user.try(:token)
|
||||
request.headers.merge! auth_env(user.username, password, nil)
|
||||
end
|
||||
|
||||
it 'returns 200' do
|
||||
|
@ -71,9 +72,10 @@ RSpec.shared_examples Repositories::GitHttpController do
|
|||
it 'adds user info to the logs' do
|
||||
get :info_refs, params: params
|
||||
|
||||
expect(log_data).to include('username' => user.username,
|
||||
'user_id' => user.id,
|
||||
'meta.user' => user.username)
|
||||
user_log_data = { 'username' => user.username, 'user_id' => user.id }
|
||||
user_log_data['meta.user'] = user.username if user.is_a?(User)
|
||||
|
||||
expect(log_data).to include(user_log_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue