Add latest changes from gitlab-org/gitlab@master
13
.rubocop.yml
|
@ -644,3 +644,16 @@ Cop/UserAdmin:
|
|||
Performance/OpenStruct:
|
||||
Exclude:
|
||||
- 'ee/spec/**/*.rb'
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
|
||||
Style/RegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Style/RegexpLiteralMixedPreserve:
|
||||
Enabled: true
|
||||
SupportedStyles:
|
||||
- slashes
|
||||
- percent_r
|
||||
- mixed
|
||||
- mixed_preserve
|
||||
EnforcedStyle: mixed_preserve
|
||||
|
|
|
@ -176,8 +176,6 @@ Rails/SaveBang:
|
|||
- 'spec/lib/gitlab/database/custom_structure_spec.rb'
|
||||
- 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
|
||||
- 'spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb'
|
||||
- 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
|
||||
- 'spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb'
|
||||
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
|
||||
- 'spec/lib/gitlab/git_access_spec.rb'
|
||||
- 'spec/lib/gitlab/import_export/avatar_saver_spec.rb'
|
||||
|
@ -3332,3 +3330,60 @@ Gitlab/FeatureAvailableUsage:
|
|||
- 'ee/spec/models/project_spec.rb'
|
||||
- 'lib/api/helpers/related_resources_helpers.rb'
|
||||
- 'spec/models/concerns/featurable_spec.rb'
|
||||
|
||||
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
|
||||
Style/RegexpLiteralMixedPreserve:
|
||||
Exclude:
|
||||
- 'app/controllers/projects/repositories_controller.rb'
|
||||
- 'app/helpers/ci/variables_helper.rb'
|
||||
- 'app/models/alert_management/alert.rb'
|
||||
- 'app/models/application_setting.rb'
|
||||
- 'app/models/blob_viewer/go_mod.rb'
|
||||
- 'app/models/concerns/ci/maskable.rb'
|
||||
- 'app/models/operations/feature_flag.rb'
|
||||
- 'app/models/packages/go/module.rb'
|
||||
- 'app/models/project_services/chat_message/base_message.rb'
|
||||
- 'app/services/packages/conan/search_service.rb'
|
||||
- 'app/services/projects/update_remote_mirror_service.rb'
|
||||
- 'config/initializers/rspec_profiling.rb'
|
||||
- 'ee/app/models/status_page/project_setting.rb'
|
||||
- 'ee/app/presenters/vulnerability_presenter.rb'
|
||||
- 'ee/lib/api/geo_nodes.rb'
|
||||
- 'ee/lib/gitlab/vulnerabilities/standard_vulnerability.rb'
|
||||
- 'ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
|
||||
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'
|
||||
- 'ee/spec/controllers/groups/groups_controller_spec.rb'
|
||||
- 'ee/spec/features/groups/saml_enforcement_spec.rb'
|
||||
- 'ee/spec/features/markdown/metrics_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
|
||||
- 'ee/spec/models/project_services/jira_service_spec.rb'
|
||||
- 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
|
||||
- 'lib/api/invitations.rb'
|
||||
- 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
|
||||
- 'lib/gitlab/metrics/requests_rack_middleware.rb'
|
||||
- 'lib/gitlab/metrics/subscribers/active_record.rb'
|
||||
- 'lib/gitlab/regex.rb'
|
||||
- 'lib/gitlab/utils.rb'
|
||||
- 'lib/product_analytics/tracker.rb'
|
||||
- 'qa/qa/page/project/settings/advanced.rb'
|
||||
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
|
||||
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
|
||||
- 'spec/features/clusters/cluster_health_dashboard_spec.rb'
|
||||
- 'spec/features/markdown/metrics_spec.rb'
|
||||
- 'spec/features/search/user_searches_for_code_spec.rb'
|
||||
- 'spec/features/snippets/embedded_snippet_spec.rb'
|
||||
- 'spec/helpers/diff_helper_spec.rb'
|
||||
- 'spec/helpers/releases_helper_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/reports/test_case_spec.rb'
|
||||
- 'spec/lib/gitlab/consul/internal_spec.rb'
|
||||
- 'spec/lib/gitlab/import_export/shared_spec.rb'
|
||||
- 'spec/lib/gitlab/utils/usage_data_spec.rb'
|
||||
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
|
||||
- 'spec/requests/api/projects_spec.rb'
|
||||
- 'spec/services/jira/requests/projects/list_service_spec.rb'
|
||||
- 'spec/support/capybara.rb'
|
||||
- 'spec/support/helpers/grafana_api_helpers.rb'
|
||||
- 'spec/support/helpers/query_recorder.rb'
|
||||
- 'spec/support/helpers/require_migration.rb'
|
||||
- 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb'
|
||||
- 'spec/views/layouts/_head.html.haml_spec.rb'
|
||||
|
|
|
@ -922,13 +922,6 @@ Style/RedundantRegexpEscape:
|
|||
Style/RedundantSelf:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 213
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
|
||||
# SupportedStyles: slashes, percent_r, mixed
|
||||
Style/RegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 53
|
||||
# Cop supports --auto-correct.
|
||||
Style/RescueModifier:
|
||||
|
|
|
@ -87,7 +87,7 @@ export default {
|
|||
@input="searchMergeRequests"
|
||||
@removeToken="setSearchType(null)"
|
||||
/>
|
||||
<gl-icon :size="18" name="search" class="ml-3 input-icon" use-deprecated-sizes />
|
||||
<gl-icon :size="16" name="search" class="ml-3 input-icon" />
|
||||
</label>
|
||||
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
|
||||
<gl-loading-icon
|
||||
|
@ -105,7 +105,7 @@ export default {
|
|||
@click.stop="setSearchType(searchType)"
|
||||
>
|
||||
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
|
||||
<gl-icon :size="18" name="search" use-deprecated-sizes />
|
||||
<gl-icon :size="16" name="search" />
|
||||
</span>
|
||||
<span>{{ searchType.label }}</span>
|
||||
</button>
|
||||
|
|
|
@ -12,6 +12,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
memberId: {
|
||||
type: Number,
|
||||
|
@ -19,7 +20,11 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['memberPath']),
|
||||
...mapState({
|
||||
memberPath(state) {
|
||||
return state[this.namespace].memberPath;
|
||||
},
|
||||
}),
|
||||
approvePath() {
|
||||
return this.memberPath.replace(/:id$/, `${this.memberId}/approve_access_request`);
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
groupLink: {
|
||||
type: Object,
|
||||
|
@ -19,7 +20,11 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['showRemoveGroupLinkModal']),
|
||||
...mapActions({
|
||||
showRemoveGroupLinkModal(dispatch, payload) {
|
||||
return dispatch(`${this.namespace}/showRemoveGroupLinkModal`, payload);
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -8,6 +8,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
memberId: {
|
||||
type: Number,
|
||||
|
@ -43,7 +44,11 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['memberPath']),
|
||||
...mapState({
|
||||
memberPath(state) {
|
||||
return state[this.namespace].memberPath;
|
||||
},
|
||||
}),
|
||||
computedMemberPath() {
|
||||
return this.memberPath.replace(':id', this.memberId);
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
memberId: {
|
||||
type: Number,
|
||||
|
@ -19,7 +20,11 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['memberPath']),
|
||||
...mapState({
|
||||
memberPath(state) {
|
||||
return state[this.namespace].memberPath;
|
||||
},
|
||||
}),
|
||||
resendPath() {
|
||||
return this.memberPath.replace(/:id$/, `${this.memberId}/resend_invite`);
|
||||
},
|
||||
|
|
|
@ -9,8 +9,16 @@ import MembersTable from './table/members_table.vue';
|
|||
export default {
|
||||
name: 'MembersApp',
|
||||
components: { MembersTable, FilterSortContainer, GlAlert },
|
||||
inject: ['namespace'],
|
||||
computed: {
|
||||
...mapState(['showError', 'errorMessage']),
|
||||
...mapState({
|
||||
showError(state) {
|
||||
return state[this.namespace].showError;
|
||||
},
|
||||
errorMessage(state) {
|
||||
return state[this.namespace].errorMessage;
|
||||
},
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
showError(value) {
|
||||
|
@ -23,7 +31,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
hideError: HIDE_ERROR,
|
||||
hideError(commit) {
|
||||
return commit(`${this.namespace}/${HIDE_ERROR}`);
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,8 +6,16 @@ import SortDropdown from './sort_dropdown.vue';
|
|||
export default {
|
||||
name: 'FilterSortContainer',
|
||||
components: { MembersFilteredSearchBar, SortDropdown },
|
||||
inject: ['namespace'],
|
||||
computed: {
|
||||
...mapState(['filteredSearchBar', 'tableSortableFields']),
|
||||
...mapState({
|
||||
filteredSearchBar(state) {
|
||||
return state[this.namespace].filteredSearchBar;
|
||||
},
|
||||
tableSortableFields(state) {
|
||||
return state[this.namespace].tableSortableFields;
|
||||
},
|
||||
}),
|
||||
showContainer() {
|
||||
return this.filteredSearchBar.show || this.showSortDropdown;
|
||||
},
|
||||
|
|
|
@ -37,14 +37,18 @@ export default {
|
|||
],
|
||||
},
|
||||
],
|
||||
inject: ['sourceId', 'canManageMembers'],
|
||||
inject: ['namespace', 'sourceId', 'canManageMembers'],
|
||||
data() {
|
||||
return {
|
||||
initialFilterValue: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['filteredSearchBar']),
|
||||
...mapState({
|
||||
filteredSearchBar(state) {
|
||||
return state[this.namespace].filteredSearchBar;
|
||||
},
|
||||
}),
|
||||
tokens() {
|
||||
return this.$options.availableTokens.filter((token) => {
|
||||
if (
|
||||
|
|
|
@ -8,8 +8,16 @@ import { parseSortParam, buildSortHref } from '~/members/utils';
|
|||
export default {
|
||||
name: 'SortDropdown',
|
||||
components: { GlSorting, GlSortingItem },
|
||||
inject: ['namespace'],
|
||||
computed: {
|
||||
...mapState(['tableSortableFields', 'filteredSearchBar']),
|
||||
...mapState({
|
||||
tableSortableFields(state) {
|
||||
return state[this.namespace].tableSortableFields;
|
||||
},
|
||||
filteredSearchBar(state) {
|
||||
return state[this.namespace].filteredSearchBar;
|
||||
},
|
||||
}),
|
||||
sort() {
|
||||
return parseSortParam(this.tableSortableFields);
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
|
@ -30,7 +31,11 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['memberPath']),
|
||||
...mapState({
|
||||
memberPath(state) {
|
||||
return state[this.namespace].memberPath;
|
||||
},
|
||||
}),
|
||||
leavePath() {
|
||||
return this.memberPath.replace(/:id$/, 'leave');
|
||||
},
|
||||
|
|
|
@ -22,8 +22,19 @@ export default {
|
|||
},
|
||||
modalId: REMOVE_GROUP_LINK_MODAL_ID,
|
||||
components: { GlModal, GlSprintf, GlForm },
|
||||
inject: ['namespace'],
|
||||
computed: {
|
||||
...mapState(['memberPath', 'groupLinkToRemove', 'removeGroupLinkModalVisible']),
|
||||
...mapState({
|
||||
memberPath(state) {
|
||||
return state[this.namespace].memberPath;
|
||||
},
|
||||
groupLinkToRemove(state) {
|
||||
return state[this.namespace].groupLinkToRemove;
|
||||
},
|
||||
removeGroupLinkModalVisible(state) {
|
||||
return state[this.namespace].removeGroupLinkModalVisible;
|
||||
},
|
||||
}),
|
||||
groupLinkPath() {
|
||||
return this.memberPath.replace(/:id$/, this.groupLinkToRemove?.id);
|
||||
},
|
||||
|
@ -35,7 +46,11 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['hideRemoveGroupLinkModal']),
|
||||
...mapActions({
|
||||
hideRemoveGroupLinkModal(dispatch) {
|
||||
return dispatch(`${this.namespace}/hideRemoveGroupLinkModal`);
|
||||
},
|
||||
}),
|
||||
handlePrimary() {
|
||||
this.$refs.form.$el.submit();
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ import { s__ } from '~/locale';
|
|||
export default {
|
||||
name: 'ExpirationDatepicker',
|
||||
components: { GlDatepicker },
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
|
@ -46,7 +47,11 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateMemberExpiration']),
|
||||
...mapActions({
|
||||
updateMemberExpiration(dispatch, payload) {
|
||||
return dispatch(`${this.namespace}/updateMemberExpiration`, payload);
|
||||
},
|
||||
}),
|
||||
handleInput(date) {
|
||||
this.busy = true;
|
||||
this.updateMemberExpiration({
|
||||
|
|
|
@ -31,9 +31,19 @@ export default {
|
|||
LdapOverrideConfirmationModal: () =>
|
||||
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
|
||||
},
|
||||
inject: ['currentUserId'],
|
||||
inject: ['namespace', 'currentUserId'],
|
||||
computed: {
|
||||
...mapState(['members', 'tableFields', 'tableAttrs']),
|
||||
...mapState({
|
||||
members(state) {
|
||||
return state[this.namespace].members;
|
||||
},
|
||||
tableFields(state) {
|
||||
return state[this.namespace].tableFields;
|
||||
},
|
||||
tableAttrs(state) {
|
||||
return state[this.namespace].tableAttrs;
|
||||
},
|
||||
}),
|
||||
filteredFields() {
|
||||
return FIELDS.filter(
|
||||
(field) => this.tableFields.includes(field.key) && this.showField(field),
|
||||
|
|
|
@ -11,6 +11,7 @@ export default {
|
|||
GlDropdownItem,
|
||||
LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
|
||||
},
|
||||
inject: ['namespace'],
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
|
@ -44,7 +45,11 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateMemberRole']),
|
||||
...mapActions({
|
||||
updateMemberRole(dispatch, payload) {
|
||||
return dispatch(`${this.namespace}/updateMemberRole`, payload);
|
||||
},
|
||||
}),
|
||||
handleSelect(value, name) {
|
||||
if (value === this.member.accessLevel.integerValue) {
|
||||
return;
|
||||
|
|
|
@ -8,6 +8,7 @@ import membersStore from './store';
|
|||
export const initMembersApp = (
|
||||
el,
|
||||
{
|
||||
namespace,
|
||||
tableFields = [],
|
||||
tableAttrs = {},
|
||||
tableSortableFields = [],
|
||||
|
@ -24,22 +25,25 @@ export const initMembersApp = (
|
|||
|
||||
const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el);
|
||||
|
||||
const store = new Vuex.Store(
|
||||
membersStore({
|
||||
...vuexStoreAttributes,
|
||||
tableFields,
|
||||
tableAttrs,
|
||||
tableSortableFields,
|
||||
requestFormatter,
|
||||
filteredSearchBar,
|
||||
}),
|
||||
);
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
[namespace]: membersStore({
|
||||
...vuexStoreAttributes,
|
||||
tableFields,
|
||||
tableAttrs,
|
||||
tableSortableFields,
|
||||
requestFormatter,
|
||||
filteredSearchBar,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
components: { App },
|
||||
store,
|
||||
provide: {
|
||||
namespace,
|
||||
currentUserId: gon.current_user_id || null,
|
||||
sourceId,
|
||||
canManageMembers,
|
||||
|
|
|
@ -3,6 +3,7 @@ import mutations from 'ee_else_ce/members/store/mutations';
|
|||
import createState from 'ee_else_ce/members/store/state';
|
||||
|
||||
export default (initialState) => ({
|
||||
namespaced: true,
|
||||
state: createState(initialState),
|
||||
actions,
|
||||
mutations,
|
||||
|
|
|
@ -8,6 +8,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
|
|||
import { s__ } from '~/locale';
|
||||
import memberExpirationDate from '~/member_expiration_date';
|
||||
import { initMembersApp } from '~/members';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { groupLinkRequestFormatter } from '~/members/utils';
|
||||
import UsersSelect from '~/users_select';
|
||||
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
|
||||
|
@ -29,6 +30,7 @@ function mountRemoveMemberModal() {
|
|||
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
|
||||
|
||||
initMembersApp(document.querySelector('.js-group-members-list'), {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
|
||||
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
|
||||
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
|
||||
|
@ -43,6 +45,7 @@ initMembersApp(document.querySelector('.js-group-members-list'), {
|
|||
});
|
||||
|
||||
initMembersApp(document.querySelector('.js-group-group-links-list'), {
|
||||
namespace: MEMBER_TYPES.group,
|
||||
tableFields: SHARED_FIELDS.concat('granted'),
|
||||
tableAttrs: {
|
||||
table: { 'data-qa-selector': 'groups_list' },
|
||||
|
@ -51,6 +54,7 @@ initMembersApp(document.querySelector('.js-group-group-links-list'), {
|
|||
requestFormatter: groupLinkRequestFormatter,
|
||||
});
|
||||
initMembersApp(document.querySelector('.js-group-invited-members-list'), {
|
||||
namespace: MEMBER_TYPES.invite,
|
||||
tableFields: SHARED_FIELDS.concat('invited'),
|
||||
requestFormatter: groupMemberRequestFormatter,
|
||||
filteredSearchBar: {
|
||||
|
@ -62,6 +66,7 @@ initMembersApp(document.querySelector('.js-group-invited-members-list'), {
|
|||
},
|
||||
});
|
||||
initMembersApp(document.querySelector('.js-group-access-requests-list'), {
|
||||
namespace: MEMBER_TYPES.accessRequest,
|
||||
tableFields: SHARED_FIELDS.concat('requested'),
|
||||
requestFormatter: groupMemberRequestFormatter,
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
|
|||
import { s__ } from '~/locale';
|
||||
import memberExpirationDate from '~/member_expiration_date';
|
||||
import { initMembersApp } from '~/members';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { groupLinkRequestFormatter } from '~/members/utils';
|
||||
import { projectMemberRequestFormatter } from '~/projects/members/utils';
|
||||
import UsersSelect from '~/users_select';
|
||||
|
@ -42,6 +43,7 @@ new UsersSelect(); // eslint-disable-line no-new
|
|||
|
||||
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
|
||||
initMembersApp(document.querySelector('.js-project-members-list'), {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
|
||||
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
|
||||
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
|
||||
|
@ -56,6 +58,7 @@ initMembersApp(document.querySelector('.js-project-members-list'), {
|
|||
});
|
||||
|
||||
initMembersApp(document.querySelector('.js-project-group-links-list'), {
|
||||
namespace: MEMBER_TYPES.group,
|
||||
tableFields: SHARED_FIELDS.concat('granted'),
|
||||
tableAttrs: {
|
||||
table: { 'data-qa-selector': 'groups_list' },
|
||||
|
@ -72,11 +75,13 @@ initMembersApp(document.querySelector('.js-project-group-links-list'), {
|
|||
});
|
||||
|
||||
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
|
||||
namespace: MEMBER_TYPES.invite,
|
||||
tableFields: SHARED_FIELDS.concat('invited'),
|
||||
requestFormatter: projectMemberRequestFormatter,
|
||||
});
|
||||
|
||||
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
|
||||
namespace: MEMBER_TYPES.accessRequest,
|
||||
tableFields: SHARED_FIELDS.concat('requested'),
|
||||
requestFormatter: projectMemberRequestFormatter,
|
||||
});
|
||||
|
|
|
@ -47,10 +47,15 @@ export default class PerformanceBarStore {
|
|||
}
|
||||
|
||||
canTrackRequest(requestUrl) {
|
||||
return (
|
||||
requestUrl.endsWith('/api/graphql') ||
|
||||
this.requests.filter((request) => request.url === requestUrl).length < 2
|
||||
);
|
||||
// We want to store at most 2 unique requests per URL, as additional
|
||||
// requests to the same URL probably aren't very interesting.
|
||||
//
|
||||
// GraphQL requests are the exception: because all GraphQL requests
|
||||
// go to the same URL, we set a higher limit of 10 to allow
|
||||
// capturing different queries a page may make.
|
||||
const requestsLimit = requestUrl.endsWith('/api/graphql') ? 10 : 2;
|
||||
|
||||
return this.requests.filter((request) => request.url === requestUrl).length < requestsLimit;
|
||||
}
|
||||
|
||||
static truncateUrl(requestUrl) {
|
||||
|
|
|
@ -15,4 +15,7 @@ export const STAGE_VIEW = 'stage';
|
|||
export const LAYER_VIEW = 'layer';
|
||||
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
|
||||
|
||||
export const SINGLE_JOB = 'single_job';
|
||||
export const JOB_DROPDOWN = 'job_dropdown';
|
||||
|
||||
export const IID_FAILURE = 'missing_iid';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
|
||||
import JobItem from './job_item.vue';
|
||||
|
||||
/**
|
||||
|
@ -28,6 +29,10 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
jobItemTypes: {
|
||||
jobDropdown: JOB_DROPDOWN,
|
||||
singleJob: SINGLE_JOB,
|
||||
},
|
||||
computed: {
|
||||
computedJobId() {
|
||||
return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
|
||||
|
@ -57,11 +62,10 @@ export default {
|
|||
>
|
||||
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
|
||||
<job-item
|
||||
:dropdown-length="group.size"
|
||||
:type="$options.jobItemTypes.jobDropdown"
|
||||
:group-tooltip="tooltipText"
|
||||
:job="group"
|
||||
:stage-name="stageName"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
|
||||
|
@ -75,6 +79,7 @@ export default {
|
|||
<job-item
|
||||
:dropdown-length="group.size"
|
||||
:job="job"
|
||||
:type="$options.jobItemTypes.singleJob"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { reportToSentry } from '../../utils';
|
|||
import ActionComponent from '../jobs_shared/action_component.vue';
|
||||
import JobNameComponent from '../jobs_shared/job_name_component.vue';
|
||||
import { accessValue } from './accessors';
|
||||
import { REST } from './constants';
|
||||
import { REST, SINGLE_JOB } from './constants';
|
||||
|
||||
/**
|
||||
* Renders the badge for the pipeline graph and the job's dropdown.
|
||||
|
@ -97,6 +97,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: SINGLE_JOB,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
boundary() {
|
||||
|
@ -111,6 +116,9 @@ export default {
|
|||
hasDetails() {
|
||||
return accessValue(this.dataMethod, 'hasDetails', this.status);
|
||||
},
|
||||
isSingleItem() {
|
||||
return this.type === SINGLE_JOB;
|
||||
},
|
||||
nameComponent() {
|
||||
return this.hasDetails ? 'gl-link' : 'div';
|
||||
},
|
||||
|
@ -177,6 +185,17 @@ export default {
|
|||
hideTooltips() {
|
||||
this.$root.$emit(BV_HIDE_TOOLTIP);
|
||||
},
|
||||
jobItemClick(evt) {
|
||||
if (this.isSingleItem) {
|
||||
/*
|
||||
This is so the jobDropdown still toggles. Issue to refactor:
|
||||
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
|
||||
*/
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
this.hideTooltips();
|
||||
},
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('pipelineActionRequestComplete');
|
||||
},
|
||||
|
@ -201,7 +220,7 @@ export default {
|
|||
:href="detailsPath"
|
||||
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
|
||||
:data-testid="testId"
|
||||
@click.stop="hideTooltips"
|
||||
@click="jobItemClick"
|
||||
@mouseout="hideTooltips"
|
||||
>
|
||||
<div class="ci-job-name-component gl-display-flex gl-align-items-center">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%p.text-center
|
||||
%span.light
|
||||
Already have login and password?
|
||||
= _('Already have login and password?')
|
||||
- path_params = { redirect_to_referer: 'yes' }
|
||||
- path_params[:invite_email] = @invite_email if @invite_email.present?
|
||||
= link_to "Sign in", new_session_path(:user, path_params)
|
||||
= link_to _('Sign in'), new_session_path(:user, path_params)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
|
||||
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
|
||||
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- @skip_current_level_breadcrumb = true
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
|
||||
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
|
||||
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
|
||||
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
|
||||
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- classes = local_assigns.fetch(:classes, '')
|
||||
|
||||
%span.js-filepicker
|
||||
%button.gl-button.btn.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
|
||||
%button.gl-button.btn.btn-default.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
|
||||
%span.file_name.js-filepicker-filename= _("No file chosen.")
|
||||
= f.file_field field, class: "js-filepicker-input hidden"
|
||||
- if help_text.present?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a migration to insert trail plans within SAAS for Ultimate and Premium plans
|
||||
merge_request: 57814
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in shared/_sign_in_link.html.haml
|
||||
merge_request: 58283
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add btn-default class for file picker button
|
||||
merge_request: 58238
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang Rubocop offenses for email handlers
|
||||
merge_request: 58095
|
||||
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Limit number of GraphQL requests tracked in performance bar to 10
|
||||
merge_request: 59158
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNewTrailPlans < ActiveRecord::Migration[6.0]
|
||||
class Plan < ActiveRecord::Base
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
has_one :limits, class_name: 'PlanLimits'
|
||||
|
||||
def actual_limits
|
||||
self.limits || self.build_limits
|
||||
end
|
||||
end
|
||||
|
||||
class PlanLimits < ActiveRecord::Base
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
belongs_to :plan
|
||||
end
|
||||
|
||||
def create_plan_limits(plan_limit_name, plan)
|
||||
plan_limit = Plan.find_or_initialize_by(name: plan_limit_name).actual_limits.dup
|
||||
plan_limit.plan = plan
|
||||
plan_limit.save!
|
||||
end
|
||||
|
||||
def up
|
||||
return unless Gitlab.dev_env_or_com?
|
||||
|
||||
ultimate_trial = Plan.create!(name: 'ultimate_trial', title: 'Ultimate Trial')
|
||||
premium_trial = Plan.create!(name: 'premium_trial', title: 'Premium Trial')
|
||||
|
||||
create_plan_limits('gold', ultimate_trial)
|
||||
create_plan_limits('silver', premium_trial)
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.dev_env_or_com?
|
||||
|
||||
Plan.where(name: %w(ultimate_trial premium_trial)).delete_all
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
b40c702ea6b2120da6fe11b213064a7a124dbc86bfb2d6785bfd2274c44f1e22
|
|
@ -99,7 +99,7 @@ GitLab CI/CD uses a number of concepts to describe and run your build and deploy
|
|||
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
|
||||
| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own runners to execute your scripts. |
|
||||
| [Pipeline efficiency](pipelines/pipeline_efficiency.md) | Configure your pipelines to run quickly and efficiently. |
|
||||
| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. |
|
||||
| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. <!--- this seems to be a duplicate description ---> |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ There are 3 ways to resolve an abuse report, with a button for each method:
|
|||
|
||||
The following is an example of the **Abuse Reports** page:
|
||||
|
||||
![abuse-reports-page-image](img/abuse_reports_page.png)
|
||||
![abuse-reports-page-image](img/abuse_reports_page_v13_11.png)
|
||||
|
||||
### Blocking users
|
||||
|
||||
|
|
Before Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 76 KiB |
|
@ -24,11 +24,11 @@ Initially, no data appears. Data is populated as users comment on open merge req
|
|||
|
||||
Code Review Analytics is available to users with Reporter access and above, and displays a table of open merge requests that have at least one non-author comment. The review time is measured from the time the first non-author comment was submitted.
|
||||
|
||||
To access Code Review Analytics, from your project's menu, go to **Project Analytics > Code Review**.
|
||||
To access Code Review Analytics, from your project's menu, go to **Analytics > Code Review**.
|
||||
|
||||
You can filter the list of merge requests by milestone and label.
|
||||
|
||||
![Code Review Analytics](img/code_review_analytics_v12_8.png "List of code reviews; oldest review first.")
|
||||
![Code Review Analytics](img/code_review_analytics_v13_11.png "List of code reviews; oldest review first.")
|
||||
|
||||
The table is sorted by:
|
||||
|
||||
|
|
Before Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 56 KiB |
|
@ -13,7 +13,7 @@ Issue Analytics is a bar graph which illustrates the number of issues created ea
|
|||
The default time span is 13 months, which includes the current month, and the 12 months
|
||||
prior.
|
||||
|
||||
To access the chart, navigate to your project sidebar and select **{chart}** **Analytics > Issue Analytics**.
|
||||
To access the chart, navigate to your project sidebar and select **Analytics > Issue**.
|
||||
|
||||
Hover over each bar to see the total number of issues.
|
||||
|
||||
|
@ -31,7 +31,7 @@ You can change the total number of months displayed by setting a URL parameter.
|
|||
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
|
||||
shows a total of 15 months for the chart in the GitLab.org group.
|
||||
|
||||
![Issues created per month](img/issues_created_per_month_v12_8.png)
|
||||
![Issues created per month](img/issues_created_per_month_v13_11.png)
|
||||
|
||||
## Drill into the information
|
||||
|
||||
|
|
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 63 KiB |
|
@ -17,7 +17,7 @@ for merging into production.
|
|||
|
||||
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
|
||||
|
||||
![Compliance Dashboard](img/compliance_dashboard_v13_6.png)
|
||||
![Compliance Dashboard](img/compliance_dashboard_v13_11.png)
|
||||
|
||||
NOTE:
|
||||
The Compliance Dashboard shows only the latest MR on each project.
|
||||
|
|
Before Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 7.6 KiB |
|
@ -13,14 +13,12 @@ Configure the Insights that matter for your groups to explore data such as
|
|||
triage hygiene, issues created/closed per a given period, average time for merge
|
||||
requests to be merged and much more.
|
||||
|
||||
![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png)
|
||||
![Insights example stacked bar chart](img/insights_example_stacked_bar_chart_v13_11.png)
|
||||
|
||||
## View your group's Insights
|
||||
|
||||
You can access your group's Insights by clicking the **Analytics > Insights**
|
||||
link in the left sidebar:
|
||||
|
||||
![Insights sidebar link](img/insights_sidebar_link_v12_8.png)
|
||||
link in the left sidebar.
|
||||
|
||||
## Configure your Insights
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ group: Package
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# GitLab Container Registry
|
||||
# GitLab Container Registry **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4040) in GitLab 8.8.
|
||||
> - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
|
||||
|
|
|
@ -4,7 +4,7 @@ group: Package
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Packages & Registries
|
||||
# Packages and Registries **(FREE)**
|
||||
|
||||
The GitLab [Package Registry](package_registry/index.md) acts as a private or public registry
|
||||
for a variety of common package managers. You can publish and share
|
||||
|
|
|
@ -4,7 +4,7 @@ group: Package
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Ruby gems in the Package Registry
|
||||
# Ruby gems in the Package Registry **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/803) in [GitLab Free](https://about.gitlab.com/pricing/) 13.10.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ group: Package
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Store all of your packages in one GitLab project
|
||||
# Store all of your packages in one GitLab project **(FREE)**
|
||||
|
||||
You can store all of your packages in one project's Package Registry. Rather than using
|
||||
a GitLab repository to store code, you can use the repository to store all your packages.
|
||||
|
|
|
@ -333,7 +333,7 @@ project and should only have access to that project.
|
|||
|
||||
External users:
|
||||
|
||||
- Cannot create projects (including forks), groups, or snippets.
|
||||
- Can only create projects (including forks), subgroups, and snippets within the top-level group to which they belong.
|
||||
- Can only access public projects and projects to which they are explicitly granted access,
|
||||
thus hiding all other internal or private ones from them (like being
|
||||
logged out).
|
||||
|
|
|
@ -3266,6 +3266,9 @@ msgstr ""
|
|||
msgid "Already blocked"
|
||||
msgstr ""
|
||||
|
||||
msgid "Already have login and password?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Style
|
||||
# This cop is based on `Style/RegexpLiteral` but adds a new
|
||||
# `EnforcedStyle` option `mixed_preserve`.
|
||||
#
|
||||
# This cop will be removed once the upstream PR is merged and RuboCop upgraded.
|
||||
#
|
||||
# See https://github.com/rubocop/rubocop/pull/9688
|
||||
class RegexpLiteralMixedPreserve < RuboCop::Cop::Style::RegexpLiteral
|
||||
module Patch
|
||||
private
|
||||
|
||||
def allowed_slash_literal?(node)
|
||||
super || allowed_mixed_preserve?(node)
|
||||
end
|
||||
|
||||
def allowed_percent_r_literal?(node)
|
||||
super || allowed_mixed_preserve?(node)
|
||||
end
|
||||
|
||||
def allowed_mixed_preserve?(node)
|
||||
style == :mixed_preserve && !contains_disallowed_slash?(node)
|
||||
end
|
||||
end
|
||||
|
||||
prepend Patch
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
|
@ -14,9 +15,14 @@ describe('ApproveAccessRequestButton', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.accessRequest]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -25,6 +31,9 @@ describe('ApproveAccessRequestButton', () => {
|
|||
wrapper = shallowMount(ApproveAccessRequestButton, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.accessRequest,
|
||||
},
|
||||
propsData: {
|
||||
memberId: 1,
|
||||
...propsData,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { group } from '../../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -17,7 +18,12 @@ describe('RemoveGroupLinkButton', () => {
|
|||
|
||||
const createStore = () => {
|
||||
return new Vuex.Store({
|
||||
actions,
|
||||
modules: {
|
||||
[MEMBER_TYPES.group]: {
|
||||
namespaced: true,
|
||||
actions,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -25,6 +31,9 @@ describe('RemoveGroupLinkButton', () => {
|
|||
wrapper = mount(RemoveGroupLinkButton, {
|
||||
localVue,
|
||||
store: createStore(),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.group,
|
||||
},
|
||||
propsData: {
|
||||
groupLink: group,
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -11,9 +12,14 @@ describe('RemoveMemberButton', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -22,6 +28,9 @@ describe('RemoveMemberButton', () => {
|
|||
wrapper = shallowMount(RemoveMemberButton, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
propsData: {
|
||||
memberId: 1,
|
||||
memberType: 'GroupMember',
|
||||
|
|
|
@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
|
@ -14,9 +15,14 @@ describe('ResendInviteButton', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.invite]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -25,6 +31,9 @@ describe('ResendInviteButton', () => {
|
|||
wrapper = shallowMount(ResendInviteButton, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.invite,
|
||||
},
|
||||
propsData: {
|
||||
memberId: 1,
|
||||
...propsData,
|
||||
|
|
|
@ -5,6 +5,7 @@ import Vuex from 'vuex';
|
|||
import * as commonUtils from '~/lib/utils/common_utils';
|
||||
import MembersApp from '~/members/components/app.vue';
|
||||
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
|
||||
import mutations from '~/members/store/mutations';
|
||||
|
||||
|
@ -17,16 +18,24 @@ describe('MembersApp', () => {
|
|||
|
||||
const createComponent = (state = {}, options = {}) => {
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
showError: true,
|
||||
errorMessage: 'Something went wrong, please try again.',
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
showError: true,
|
||||
errorMessage: 'Something went wrong, please try again.',
|
||||
...state,
|
||||
},
|
||||
mutations,
|
||||
},
|
||||
},
|
||||
mutations,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(MembersApp, {
|
||||
localVue,
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
store,
|
||||
...options,
|
||||
});
|
||||
|
@ -48,7 +57,9 @@ describe('MembersApp', () => {
|
|||
it('renders and scrolls to error alert', async () => {
|
||||
createComponent({ showError: false, errorMessage: '' });
|
||||
|
||||
store.commit(RECEIVE_MEMBER_ROLE_ERROR, { error: new Error('Network Error') });
|
||||
store.commit(`${MEMBER_TYPES.user}/${RECEIVE_MEMBER_ROLE_ERROR}`, {
|
||||
error: new Error('Network Error'),
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
@ -66,7 +77,7 @@ describe('MembersApp', () => {
|
|||
it('does not render and scroll to error alert', async () => {
|
||||
createComponent();
|
||||
|
||||
store.commit(HIDE_ERROR);
|
||||
store.commit(`${MEMBER_TYPES.user}/${HIDE_ERROR}`);
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import Vuex from 'vuex';
|
|||
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
|
||||
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
|
||||
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -12,22 +13,30 @@ describe('FilterSortContainer', () => {
|
|||
|
||||
const createComponent = (state) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
},
|
||||
tableSortableFields: ['account'],
|
||||
...state,
|
||||
},
|
||||
},
|
||||
tableSortableFields: ['account'],
|
||||
...state,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMount(FilterSortContainer, {
|
||||
localVue,
|
||||
store,
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -12,15 +13,20 @@ describe('MembersFilteredSearchBar', () => {
|
|||
|
||||
const createComponent = ({ state = {}, provide = {} } = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
},
|
||||
...state,
|
||||
},
|
||||
},
|
||||
...state,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -29,6 +35,7 @@ describe('MembersFilteredSearchBar', () => {
|
|||
provide: {
|
||||
sourceId: 1,
|
||||
canManageMembers: true,
|
||||
namespace: MEMBER_TYPES.user,
|
||||
...provide,
|
||||
},
|
||||
store,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import * as urlUtilities from '~/lib/utils/url_utility';
|
||||
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -14,16 +15,21 @@ describe('SortDropdown', () => {
|
|||
|
||||
const createComponent = (state) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['two_factor'],
|
||||
searchParam: 'search',
|
||||
placeholder: 'Filter members',
|
||||
recentSearchesStorageKey: 'group_members',
|
||||
},
|
||||
...state,
|
||||
},
|
||||
},
|
||||
...state,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -31,6 +37,7 @@ describe('SortDropdown', () => {
|
|||
localVue,
|
||||
provide: {
|
||||
sourceId: 1,
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
store,
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
|
|||
import { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import LeaveModal from '~/members/components/modals/leave_modal.vue';
|
||||
import { LEAVE_MODAL_ID } from '~/members/constants';
|
||||
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
|
||||
import { member } from '../../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
@ -17,9 +17,14 @@ describe('LeaveModal', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -28,6 +33,9 @@ describe('LeaveModal', () => {
|
|||
wrapper = mount(LeaveModal, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
propsData: {
|
||||
member,
|
||||
...propsData,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
|
|||
import { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
|
||||
import { REMOVE_GROUP_LINK_MODAL_ID } from '~/members/constants';
|
||||
import { REMOVE_GROUP_LINK_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
|
||||
import { group } from '../../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
@ -21,13 +21,18 @@ describe('RemoveGroupLinkModal', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_links/:id',
|
||||
groupLinkToRemove: group,
|
||||
removeGroupLinkModalVisible: true,
|
||||
...state,
|
||||
modules: {
|
||||
[MEMBER_TYPES.group]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_links/:id',
|
||||
groupLinkToRemove: group,
|
||||
removeGroupLinkModalVisible: true,
|
||||
...state,
|
||||
},
|
||||
actions,
|
||||
},
|
||||
},
|
||||
actions,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -35,6 +40,9 @@ describe('RemoveGroupLinkModal', () => {
|
|||
wrapper = mount(RemoveGroupLinkModal, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.group,
|
||||
},
|
||||
attrs: {
|
||||
static: true,
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import Vuex from 'vuex';
|
|||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { member } from '../../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -31,7 +32,11 @@ describe('ExpirationDatepicker', () => {
|
|||
),
|
||||
};
|
||||
|
||||
return new Vuex.Store({ actions });
|
||||
return new Vuex.Store({
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: { namespaced: true, actions },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
|
@ -41,6 +46,9 @@ describe('ExpirationDatepicker', () => {
|
|||
permissions: { canUpdate: true },
|
||||
...propsData,
|
||||
},
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
localVue,
|
||||
store: createStore(),
|
||||
mocks: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
|
|||
import MemberSource from '~/members/components/table/member_source.vue';
|
||||
import MembersTable from '~/members/components/table/members_table.vue';
|
||||
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import * as initUserPopovers from '~/user_popovers';
|
||||
import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
|
||||
|
||||
|
@ -25,14 +26,19 @@ describe('MembersTable', () => {
|
|||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
members: [],
|
||||
tableFields: [],
|
||||
tableAttrs: {
|
||||
table: { 'data-qa-selector': 'members_list' },
|
||||
tr: { 'data-qa-selector': 'member_row' },
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
members: [],
|
||||
tableFields: [],
|
||||
tableAttrs: {
|
||||
table: { 'data-qa-selector': 'members_list' },
|
||||
tr: { 'data-qa-selector': 'member_row' },
|
||||
},
|
||||
...state,
|
||||
},
|
||||
},
|
||||
...state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -44,6 +50,7 @@ describe('MembersTable', () => {
|
|||
provide: {
|
||||
sourceId: 1,
|
||||
currentUserId: 1,
|
||||
namespace: MEMBER_TYPES.user,
|
||||
...provide,
|
||||
},
|
||||
stubs: [
|
||||
|
|
|
@ -7,6 +7,7 @@ import Vuex from 'vuex';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
|
||||
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { member } from '../../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -24,11 +25,18 @@ describe('RoleDropdown', () => {
|
|||
updateMemberRole: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
return new Vuex.Store({ actions });
|
||||
return new Vuex.Store({
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: { namespaced: true, actions },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = mount(RoleDropdown, {
|
||||
provide: {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
},
|
||||
propsData: {
|
||||
member,
|
||||
permissions: {},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createWrapper } from '@vue/test-utils';
|
||||
import MembersApp from '~/members/components/app.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { initMembersApp } from '~/members/index';
|
||||
import { membersJsonString, members } from './mock_data';
|
||||
|
||||
|
@ -10,6 +11,7 @@ describe('initMembersApp', () => {
|
|||
|
||||
const setup = () => {
|
||||
vm = initMembersApp(el, {
|
||||
namespace: MEMBER_TYPES.user,
|
||||
tableFields: ['account'],
|
||||
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
|
||||
tableSortableFields: ['account'],
|
||||
|
@ -45,42 +47,46 @@ describe('initMembersApp', () => {
|
|||
it('parses and sets `members` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.members).toEqual(members);
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].members).toEqual(members);
|
||||
});
|
||||
|
||||
it('sets `tableFields` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.tableFields).toEqual(['account']);
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].tableFields).toEqual(['account']);
|
||||
});
|
||||
|
||||
it('sets `tableAttrs` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.tableAttrs).toEqual({ table: { 'data-qa-selector': 'members_list' } });
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].tableAttrs).toEqual({
|
||||
table: { 'data-qa-selector': 'members_list' },
|
||||
});
|
||||
});
|
||||
|
||||
it('sets `tableSortableFields` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.tableSortableFields).toEqual(['account']);
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].tableSortableFields).toEqual(['account']);
|
||||
});
|
||||
|
||||
it('sets `requestFormatter` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.requestFormatter()).toEqual({});
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].requestFormatter()).toEqual({});
|
||||
});
|
||||
|
||||
it('sets `filteredSearchBar` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.filteredSearchBar).toEqual({ show: false });
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].filteredSearchBar).toEqual({ show: false });
|
||||
});
|
||||
|
||||
it('sets `memberPath` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.memberPath).toBe('/groups/foo-bar/-/group_members/:id');
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].memberPath).toBe(
|
||||
'/groups/foo-bar/-/group_members/:id',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -59,4 +59,44 @@ describe('PerformanceBarStore', () => {
|
|||
expect(store.findRequest('id').details.test.calls).toEqual(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canTrackRequest', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new PerformanceBarStore();
|
||||
});
|
||||
|
||||
it('limits to 10 requests for GraphQL', () => {
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
|
||||
|
||||
store.addRequest('0', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('1', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('2', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('3', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('4', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('5', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('6', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('7', 'https://gitlab.com/api/graphql');
|
||||
store.addRequest('8', 'https://gitlab.com/api/graphql');
|
||||
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
|
||||
|
||||
store.addRequest('9', 'https://gitlab.com/api/graphql');
|
||||
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(false);
|
||||
});
|
||||
|
||||
it('limits to 2 requests for all other URLs', () => {
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
|
||||
|
||||
store.addRequest('a', 'https://gitlab.com/api/v4/users/1');
|
||||
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
|
||||
|
||||
store.addRequest('b', 'https://gitlab.com/api/v4/users/1');
|
||||
|
||||
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
|
|||
|
||||
context 'when the noteable could not be found' do
|
||||
before do
|
||||
noteable.destroy
|
||||
noteable.destroy!
|
||||
end
|
||||
|
||||
it 'raises a NoteableNotFoundError' do
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe AddNewTrailPlans, :migration do
|
||||
describe '#up' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return true
|
||||
end
|
||||
|
||||
it 'creates 2 entries within the plans table' do
|
||||
expect { migrate! }.to change { AddNewTrailPlans::Plan.count }.by 2
|
||||
expect(AddNewTrailPlans::Plan.last(2).pluck(:name)).to match_array(%w(ultimate_trial premium_trial))
|
||||
end
|
||||
|
||||
it 'creates 2 entries for plan limits' do
|
||||
expect { migrate! }.to change { AddNewTrailPlans::PlanLimits.count }.by 2
|
||||
end
|
||||
|
||||
context 'when the plan limits for gold and silver exists' do
|
||||
before do
|
||||
table(:plans).create!(id: 1, name: 'gold', title: 'Gold')
|
||||
table(:plan_limits).create!(id: 1, plan_id: 1, storage_size_limit: 2000)
|
||||
table(:plans).create!(id: 2, name: 'silver', title: 'Silver')
|
||||
table(:plan_limits).create!(id: 2, plan_id: 2, storage_size_limit: 1000)
|
||||
end
|
||||
|
||||
it 'duplicates the gold and silvers plan limits entries' do
|
||||
migrate!
|
||||
|
||||
ultimate_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial').limits
|
||||
expect(ultimate_plan_limits.storage_size_limit).to be 2000
|
||||
|
||||
premium_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'premium_trial').limits
|
||||
expect(premium_plan_limits.storage_size_limit).to be 1000
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the instance is not SaaS' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return false
|
||||
end
|
||||
|
||||
it 'does not create plans and plan limits and returns' do
|
||||
expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
|
||||
expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
before do
|
||||
table(:plans).create!(id: 3, name: 'other')
|
||||
table(:plan_limits).create!(plan_id: 3)
|
||||
end
|
||||
|
||||
context 'when the instance is SaaS' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return true
|
||||
end
|
||||
|
||||
it 'removes the newly added ultimate and premium trial entries' do
|
||||
migrate!
|
||||
|
||||
expect { described_class.new.down }.to change { AddNewTrailPlans::Plan.count }.by(-2)
|
||||
expect(AddNewTrailPlans::Plan.find_by(name: 'premium_trial')).to be_nil
|
||||
expect(AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial')).to be_nil
|
||||
|
||||
other_plan = AddNewTrailPlans::Plan.find_by(name: 'other')
|
||||
expect(other_plan).to be_persisted
|
||||
expect(AddNewTrailPlans::PlanLimits.count).to eq(1)
|
||||
expect(AddNewTrailPlans::PlanLimits.first.plan_id).to eq(other_plan.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the instance is not SaaS' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return false
|
||||
table(:plans).create!(id: 1, name: 'ultimate_trial', title: 'Ultimate Trial')
|
||||
table(:plans).create!(id: 2, name: 'premium_trial', title: 'Premium Trial')
|
||||
table(:plan_limits).create!(id: 1, plan_id: 1)
|
||||
table(:plan_limits).create!(id: 2, plan_id: 2)
|
||||
end
|
||||
|
||||
it 'does not delete plans and plan limits and returns' do
|
||||
migrate!
|
||||
|
||||
expect { described_class.new.down }.not_to change { AddNewTrailPlans::Plan.count }
|
||||
expect(AddNewTrailPlans::PlanLimits.count).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require_relative '../../../../rubocop/cop/style/regexp_literal_mixed_preserve'
|
||||
|
||||
# This spec contains only relevant examples.
|
||||
#
|
||||
# See also https://github.com/rubocop/rubocop/pull/9688
|
||||
RSpec.describe RuboCop::Cop::Style::RegexpLiteralMixedPreserve, :config do
|
||||
let(:config) do
|
||||
supported_styles = { 'SupportedStyles' => %w[slashes percent_r mixed mixed_preserve] }
|
||||
RuboCop::Config.new('Style/PercentLiteralDelimiters' =>
|
||||
percent_literal_delimiters_config,
|
||||
'Style/RegexpLiteralMixedPreserve' =>
|
||||
cop_config.merge(supported_styles))
|
||||
end
|
||||
|
||||
let(:percent_literal_delimiters_config) { { 'PreferredDelimiters' => { '%r' => '{}' } } }
|
||||
|
||||
context 'when EnforcedStyle is set to mixed_preserve' do
|
||||
let(:cop_config) { { 'EnforcedStyle' => 'mixed_preserve' } }
|
||||
|
||||
describe 'a single-line `//` regex without slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses('foo = /a/')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a single-line `//` regex with slashes' do
|
||||
it 'registers an offense and corrects' do
|
||||
expect_offense(<<~'RUBY')
|
||||
foo = /home\//
|
||||
^^^^^^^^ Use `%r` around regular expression.
|
||||
RUBY
|
||||
|
||||
expect_correction(<<~'RUBY')
|
||||
foo = %r{home/}
|
||||
RUBY
|
||||
end
|
||||
|
||||
describe 'when configured to allow inner slashes' do
|
||||
before do
|
||||
cop_config['AllowInnerSlashes'] = true
|
||||
end
|
||||
|
||||
it 'is accepted' do
|
||||
expect_no_offenses('foo = /home\\//')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a multi-line `//` regex without slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses(<<~'RUBY')
|
||||
foo = /
|
||||
foo
|
||||
bar
|
||||
/x
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a multi-line `//` regex with slashes' do
|
||||
it 'registers an offense and corrects' do
|
||||
expect_offense(<<~'RUBY')
|
||||
foo = /
|
||||
^ Use `%r` around regular expression.
|
||||
https?:\/\/
|
||||
example\.com
|
||||
/x
|
||||
RUBY
|
||||
|
||||
expect_correction(<<~'RUBY')
|
||||
foo = %r{
|
||||
https?://
|
||||
example\.com
|
||||
}x
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a single-line %r regex without slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
foo = %r{a}
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a single-line %r regex with slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses('foo = %r{home/}')
|
||||
end
|
||||
|
||||
describe 'when configured to allow inner slashes' do
|
||||
before do
|
||||
cop_config['AllowInnerSlashes'] = true
|
||||
end
|
||||
|
||||
it 'is accepted' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
foo = %r{home/}
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a multi-line %r regex without slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
foo = %r{
|
||||
foo
|
||||
bar
|
||||
}x
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a multi-line %r regex with slashes' do
|
||||
it 'is accepted' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
foo = %r{
|
||||
https?://
|
||||
example\.com
|
||||
}x
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|