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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml8
-rw-r--r--app/assets/javascripts/contextual_sidebar.js5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue27
-rw-r--r--app/assets/javascripts/invite_members/constants.js2
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js10
-rw-r--r--app/models/application_record.rb3
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/views/groups/_invite_members_side_nav_link.html.haml8
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/projects/_invite_members_side_nav_link.html.haml8
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml13
-rw-r--r--app/views/shared/nav/_sidebar_submenu.html.haml12
-rw-r--r--config/feature_flags/experiment/invite_members_in_side_nav.yml8
-rw-r--r--db/post_migrate/20210923192648_remove_foreign_keys_from_open_project_data_table.rb17
-rw-r--r--db/post_migrate/20210923192649_remove_open_project_data_table.rb22
-rw-r--r--db/schema_migrations/202109231926481
-rw-r--r--db/schema_migrations/202109231926491
-rw-r--r--db/structure.sql34
-rw-r--r--doc/user/admin_area/index.md16
-rw-r--r--lib/declarative_enum.rb19
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/sidebars/groups/menus/invite_team_members_menu.rb46
-rw-r--r--lib/sidebars/groups/panel.rb10
-rw-r--r--lib/sidebars/panel.rb1
-rw-r--r--lib/sidebars/projects/menus/invite_team_members_menu.rb47
-rw-r--r--lib/sidebars/projects/panel.rb8
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/db/schema_spec.rb1
-rw-r--r--spec/features/contextual_sidebar_spec.rb109
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js35
-rw-r--r--spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb55
-rw-r--r--spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb52
33 files changed, 502 insertions, 85 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index e6b515e16bf..183acceadf9 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -106,6 +106,9 @@
.if-dot-com-gitlab-org-and-security-merge-request: &if-dot-com-gitlab-org-and-security-merge-request
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_MERGE_REQUEST_IID'
+.if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa: &if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa
+ if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_MERGE_REQUEST_IID && $QA_MANUAL_FF_PACKAGE_AND_QA'
+
.if-dot-com-gitlab-org-and-security-tag: &if-dot-com-gitlab-org-and-security-tag
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_COMMIT_TAG'
@@ -769,10 +772,13 @@
rules:
- <<: *if-not-ee
when: never
- - <<: *if-dot-com-gitlab-org-and-security-merge-request
+ - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa
changes: *feature-flag-development-config-patterns
when: manual
allow_failure: true
+ - <<: *if-dot-com-gitlab-org-and-security-merge-request
+ changes: *feature-flag-development-config-patterns
+ allow_failure: true
###############
# Rails rules #
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 08cf0197993..08942374120 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -2,6 +2,8 @@ import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
import { debounce } from 'lodash';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { parseBoolean } from '~/lib/utils/common_utils';
export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
@@ -112,5 +114,8 @@ export default class ContextualSidebar {
const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
this.toggleCollapsedSidebar(collapse, true);
}
+
+ initInviteMembersModal();
+ initInviteMembersTrigger();
}
}
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index 3448aa72964..c5f99a1657e 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -4,7 +4,7 @@
query groupUsersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) {
id
- users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
+ users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
nodes {
user {
...User
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 05be427742c..bf3250f63a5 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '../constants';
export default {
- components: { GlButton, GlLink },
+ components: { GlButton, GlLink, GlIcon },
props: {
displayText: {
type: String,
@@ -53,13 +54,11 @@ export default {
},
},
computed: {
- isButton() {
- return this.triggerElement === 'button';
- },
componentAttributes() {
const baseAttributes = {
class: this.classes,
'data-qa-selector': 'invite_members_button',
+ 'data-test-id': 'invite-members-button',
};
if (this.event && this.label) {
@@ -77,6 +76,9 @@ export default {
this.trackExperimentOnShow();
},
methods: {
+ checkTrigger(targetTriggerElement) {
+ return this.triggerElement === targetTriggerElement;
+ },
openModal() {
eventHub.$emit('openModal', { inviteeType: 'members', source: this.triggerSource });
},
@@ -87,12 +89,14 @@ export default {
}
},
},
+ TRIGGER_ELEMENT_BUTTON,
+ TRIGGER_ELEMENT_SIDE_NAV,
};
</script>
<template>
<gl-button
- v-if="isButton"
+ v-if="checkTrigger($options.TRIGGER_ELEMENT_BUTTON)"
v-bind="componentAttributes"
:variant="variant"
:icon="icon"
@@ -100,6 +104,17 @@ export default {
>
{{ displayText }}
</gl-button>
+ <gl-link
+ v-else-if="checkTrigger($options.TRIGGER_ELEMENT_SIDE_NAV)"
+ v-bind="componentAttributes"
+ data-is-link="true"
+ @click="openModal"
+ >
+ <span class="nav-icon-container">
+ <gl-icon :name="icon" />
+ </span>
+ <span class="nav-item-name"> {{ displayText }} </span>
+ </gl-link>
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
{{ displayText }}
</gl-link>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 02f38bfd3c5..c1a1107ebe3 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -25,3 +25,5 @@ export const API_MESSAGES = {
};
export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
+export const TRIGGER_ELEMENT_BUTTON = 'button';
+export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 2aa5c571157..fc657a064dd 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -5,7 +5,15 @@ import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast);
+let initedInviteMembersModal;
+
export default function initInviteMembersModal() {
+ if (initedInviteMembersModal) {
+ // if we already loaded this in another part of the dom, we don't want to do it again
+ // else we will stack the modals
+ return false;
+ }
+
// https://gitlab.com/gitlab-org/gitlab/-/issues/344955
// bug lying in wait here for someone to put group and project invite in same screen
// once that happens we'll need to mount these differently, perhaps split
@@ -16,6 +24,8 @@ export default function initInviteMembersModal() {
return false;
}
+ initedInviteMembersModal = true;
+
return new Vue({
el,
provide: {
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 0f7b6388441..bcd8bdd6638 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -95,8 +95,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.declarative_enum(enum_mod)
- values = enum_mod.definition.transform_values { |v| v[:value] }
- enum(enum_mod.key => values)
+ enum(enum_mod.key => enum_mod.values)
end
def self.cached_column_list
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9565d1a6a69..353a896b3fe 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -16,7 +16,6 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Linear
include EachBatch
- ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
# Temporary column used for back-filling project namespaces.
# Remove it once the back-filling of all project namespaces is done.
ignore_column :tmp_project_id, remove_with: '14.7', remove_after: '2022-01-22'
diff --git a/app/views/groups/_invite_members_side_nav_link.html.haml b/app/views/groups/_invite_members_side_nav_link.html.haml
new file mode 100644
index 00000000000..bccfa9897da
--- /dev/null
+++ b/app/views/groups/_invite_members_side_nav_link.html.haml
@@ -0,0 +1,8 @@
+.js-invite-members-trigger{ data: { trigger_source: 'group-side-nav',
+ classes: 'gl-text-decoration-none! gl-shadow-none! gl-text-body!',
+ icon: 'users',
+ display_text: title,
+ trigger_element: 'side-nav'} }
+
+= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
+= render 'groups/invite_members_modal', group: group
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 2e74d983397..ed3f2b0c6db 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -32,8 +32,6 @@
= render_if_exists 'groups/group_activity_analytics', group: @group
-= render 'groups/invite_members_modal', group: @group
-
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
.top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs
diff --git a/app/views/projects/_invite_members_side_nav_link.html.haml b/app/views/projects/_invite_members_side_nav_link.html.haml
new file mode 100644
index 00000000000..ea6174d19f0
--- /dev/null
+++ b/app/views/projects/_invite_members_side_nav_link.html.haml
@@ -0,0 +1,8 @@
+.js-invite-members-trigger{ data: { trigger_source: 'project-side-nav',
+ classes: 'gl-text-decoration-none! gl-shadow-none! gl-text-body!',
+ icon: 'users',
+ display_text: title,
+ trigger_element: 'side-nav'} }
+
+= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
+= render 'projects/invite_members_modal', project: project
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
index 903d2d077ba..3f71368aff3 100644
--- a/app/views/shared/nav/_sidebar_menu.html.haml
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -16,15 +16,4 @@
%span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
= number_with_delimiter(sidebar_menu.pill_count)
- %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
- = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
- %span.fly-out-top-item-container
- %strong.fly-out-top-item-name
- = sidebar_menu.title
- - if sidebar_menu.has_pill?
- %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
- = number_with_delimiter(sidebar_menu.pill_count)
-
- - if sidebar_menu.has_renderable_items?
- %li.divider.fly-out-top-item
- = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
+ = render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
diff --git a/app/views/shared/nav/_sidebar_submenu.html.haml b/app/views/shared/nav/_sidebar_submenu.html.haml
new file mode 100644
index 00000000000..750e6c9ee57
--- /dev/null
+++ b/app/views/shared/nav/_sidebar_submenu.html.haml
@@ -0,0 +1,12 @@
+%ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
+ = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
+ %span.fly-out-top-item-container
+ %strong.fly-out-top-item-name
+ = sidebar_menu.title
+ - if sidebar_menu.has_pill?
+ %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
+ = number_with_delimiter(sidebar_menu.pill_count)
+
+ - if sidebar_menu.has_renderable_items?
+ %li.divider.fly-out-top-item
+ = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
diff --git a/config/feature_flags/experiment/invite_members_in_side_nav.yml b/config/feature_flags/experiment/invite_members_in_side_nav.yml
new file mode 100644
index 00000000000..7968a885374
--- /dev/null
+++ b/config/feature_flags/experiment/invite_members_in_side_nav.yml
@@ -0,0 +1,8 @@
+---
+name: invite_members_in_side_nav
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70451
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342951
+milestone: '14.5'
+type: experiment
+group: group::expansion
+default_enabled: false
diff --git a/db/post_migrate/20210923192648_remove_foreign_keys_from_open_project_data_table.rb b/db/post_migrate/20210923192648_remove_foreign_keys_from_open_project_data_table.rb
new file mode 100644
index 00000000000..1da5aad0fab
--- /dev/null
+++ b/db/post_migrate/20210923192648_remove_foreign_keys_from_open_project_data_table.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveForeignKeysFromOpenProjectDataTable < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_foreign_key :open_project_tracker_data, column: :service_id
+ end
+ end
+
+ def down
+ with_lock_retries do
+ add_foreign_key :open_project_tracker_data, :integrations, column: :service_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/post_migrate/20210923192649_remove_open_project_data_table.rb b/db/post_migrate/20210923192649_remove_open_project_data_table.rb
new file mode 100644
index 00000000000..252d7e07261
--- /dev/null
+++ b/db/post_migrate/20210923192649_remove_open_project_data_table.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class RemoveOpenProjectDataTable < Gitlab::Database::Migration[1.0]
+ def up
+ drop_table :open_project_tracker_data
+ end
+
+ def down
+ create_table :open_project_tracker_data do |t|
+ t.integer :service_id, index: { name: 'index_open_project_tracker_data_on_service_id' }, null: false
+ t.timestamps_with_timezone
+ t.string :encrypted_url, limit: 255
+ t.string :encrypted_url_iv, limit: 255
+ t.string :encrypted_api_url, limit: 255
+ t.string :encrypted_api_url_iv, limit: 255
+ t.string :encrypted_token, limit: 255
+ t.string :encrypted_token_iv, limit: 255
+ t.string :closed_status_id, limit: 5
+ t.string :project_identifier_code, limit: 100
+ end
+ end
+end
diff --git a/db/schema_migrations/20210923192648 b/db/schema_migrations/20210923192648
new file mode 100644
index 00000000000..5514ec65903
--- /dev/null
+++ b/db/schema_migrations/20210923192648
@@ -0,0 +1 @@
+d9cb520f198893019b24c970ba409471e3d98581eb62f746320fc6e81a16af08 \ No newline at end of file
diff --git a/db/schema_migrations/20210923192649 b/db/schema_migrations/20210923192649
new file mode 100644
index 00000000000..e91955f09fe
--- /dev/null
+++ b/db/schema_migrations/20210923192649
@@ -0,0 +1 @@
+12fb550e936ede5a8e83ab06f2fc535201e7a276295a2103564412ded32958f8 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c5d619cb2a3..e84a69c1967 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16583,30 +16583,6 @@ CREATE SEQUENCE onboarding_progresses_id_seq
ALTER SEQUENCE onboarding_progresses_id_seq OWNED BY onboarding_progresses.id;
-CREATE TABLE open_project_tracker_data (
- id bigint NOT NULL,
- service_id integer NOT NULL,
- created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL,
- encrypted_url character varying(255),
- encrypted_url_iv character varying(255),
- encrypted_api_url character varying(255),
- encrypted_api_url_iv character varying(255),
- encrypted_token character varying(255),
- encrypted_token_iv character varying(255),
- closed_status_id character varying(5),
- project_identifier_code character varying(100)
-);
-
-CREATE SEQUENCE open_project_tracker_data_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE open_project_tracker_data_id_seq OWNED BY open_project_tracker_data.id;
-
CREATE TABLE operations_feature_flag_scopes (
id bigint NOT NULL,
feature_flag_id bigint NOT NULL,
@@ -21663,8 +21639,6 @@ ALTER TABLE ONLY oauth_openid_requests ALTER COLUMN id SET DEFAULT nextval('oaut
ALTER TABLE ONLY onboarding_progresses ALTER COLUMN id SET DEFAULT nextval('onboarding_progresses_id_seq'::regclass);
-ALTER TABLE ONLY open_project_tracker_data ALTER COLUMN id SET DEFAULT nextval('open_project_tracker_data_id_seq'::regclass);
-
ALTER TABLE ONLY operations_feature_flag_scopes ALTER COLUMN id SET DEFAULT nextval('operations_feature_flag_scopes_id_seq'::regclass);
ALTER TABLE ONLY operations_feature_flags ALTER COLUMN id SET DEFAULT nextval('operations_feature_flags_id_seq'::regclass);
@@ -23419,9 +23393,6 @@ ALTER TABLE ONLY oauth_openid_requests
ALTER TABLE ONLY onboarding_progresses
ADD CONSTRAINT onboarding_progresses_pkey PRIMARY KEY (id);
-ALTER TABLE ONLY open_project_tracker_data
- ADD CONSTRAINT open_project_tracker_data_pkey PRIMARY KEY (id);
-
ALTER TABLE ONLY operations_feature_flag_scopes
ADD CONSTRAINT operations_feature_flag_scopes_pkey PRIMARY KEY (id);
@@ -26608,8 +26579,6 @@ CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_pr
CREATE INDEX index_oncall_shifts_on_rotation_id_and_starts_at_and_ends_at ON incident_management_oncall_shifts USING btree (rotation_id, starts_at, ends_at);
-CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id);
-
CREATE INDEX index_operations_feature_flags_issues_on_issue_id ON operations_feature_flags_issues USING btree (issue_id);
CREATE UNIQUE INDEX index_operations_feature_flags_on_project_id_and_iid ON operations_feature_flags USING btree (project_id, iid);
@@ -29724,9 +29693,6 @@ ALTER TABLE ONLY bulk_import_failures
ALTER TABLE ONLY group_wiki_repositories
ADD CONSTRAINT fk_rails_19755e374b FOREIGN KEY (shard_id) REFERENCES shards(id) ON DELETE RESTRICT;
-ALTER TABLE ONLY open_project_tracker_data
- ADD CONSTRAINT fk_rails_1987546e48 FOREIGN KEY (service_id) REFERENCES integrations(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY gpg_signatures
ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES gpg_key_subkeys(id) ON DELETE SET NULL;
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 6457b4df2ff..4de2397706b 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -107,6 +107,22 @@ You can combine the filter options. For example, to list only public projects wi
1. Click the **Public** tab.
1. Enter `score` in the **Filter by name...** input box.
+#### Deleted projects **(PREMIUM SELF)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37014) in GitLab 13.3.
+
+When delayed project deletion is [enabled for a group](../group/index.md#enable-delayed-project-removal),
+projects within that group are not deleted immediately, but only after a delay. To access a list of all projects that are pending deletion:
+
+1. On the top bar, select **Menu > Projects > Explore projects**.
+1. Select the **Deleted projects** tab.
+
+Listed for each project is:
+
+- The time the project was marked for deletion.
+- The time the project is scheduled for final deletion.
+- A **Restore** link to stop the project being eventually deleted.
+
### Administering Users
You can administer all users in the GitLab instance from the Admin Area's Users page:
diff --git a/lib/declarative_enum.rb b/lib/declarative_enum.rb
index 8dea9d6130b..7875e0ba4f3 100644
--- a/lib/declarative_enum.rb
+++ b/lib/declarative_enum.rb
@@ -15,9 +15,9 @@
# TEXT
#
# define do
-# acceptable_risk value: 0, description: 'The vulnerability is known but is considered to be an acceptable business risk.'
-# false_positive value: 1, description: 'An error in reporting the presence of a vulnerability in a system when the vulnerability is not present.'
-# used_in_tests value: 2, description: 'The finding is not a vulnerability because it is part of a test or is test data.'
+# acceptable_risk value: 0, description: N_('The vulnerability is known but is considered to be an acceptable business risk.')
+# false_positive value: 1, description: N_('An error in reporting the presence of a vulnerability in a system when the vulnerability is not present.')
+# used_in_tests value: 2, description: N_('The finding is not a vulnerability because it is part of a test or is test data.')
# end
#
# Then we can use this module to register enums for our Active Record models like so,
@@ -63,6 +63,19 @@ module DeclarativeEnum
@description
end
+ def values
+ definition.transform_values { |definition| definition[:value] }
+ end
+
+ # Return list of dynamically translated descriptions.
+ #
+ # It is required to define descriptions with `N_(...)`.
+ #
+ # See https://github.com/grosser/fast_gettext#n_-and-nn_-make-dynamic-translations-available-to-the-parser
+ def translated_descriptions
+ definition.transform_values { |definition| _(definition[:description]) }
+ end
+
def define(&block)
raise LocalJumpError, 'No block given' unless block
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index b1cae5e8eee..66157e998a0 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -323,7 +323,6 @@ oauth_access_tokens: :gitlab_main
oauth_applications: :gitlab_main
oauth_openid_requests: :gitlab_main
onboarding_progresses: :gitlab_main
-open_project_tracker_data: :gitlab_main
operations_feature_flags_clients: :gitlab_main
operations_feature_flag_scopes: :gitlab_main
operations_feature_flags: :gitlab_main
diff --git a/lib/sidebars/groups/menus/invite_team_members_menu.rb b/lib/sidebars/groups/menus/invite_team_members_menu.rb
new file mode 100644
index 00000000000..0779b696061
--- /dev/null
+++ b/lib/sidebars/groups/menus/invite_team_members_menu.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Groups
+ module Menus
+ class InviteTeamMembersMenu < ::Sidebars::Menu
+ override :title
+ def title
+ s_('InviteMember|Invite members')
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :admin_group_member, context.group) && all_valid_members.size <= 1
+ end
+
+ override :menu_partial
+ def menu_partial
+ 'groups/invite_members_side_nav_link'
+ end
+
+ override :menu_partial_options
+ def menu_partial_options
+ {
+ group: context.group,
+ title: title,
+ sidebar_menu: self
+ }
+ end
+
+ override :extra_nav_link_html_options
+ def extra_nav_link_html_options
+ {
+ 'data-test-id': 'side-nav-invite-members'
+ }
+ end
+
+ private
+
+ def all_valid_members
+ GroupMembersFinder.new(context.group, context.current_user).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb
index 185e49938ef..463c2571b14 100644
--- a/lib/sidebars/groups/panel.rb
+++ b/lib/sidebars/groups/panel.rb
@@ -15,12 +15,22 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::PackagesRegistriesMenu.new(context))
add_menu(Sidebars::Groups::Menus::CustomerRelationsMenu.new(context))
add_menu(Sidebars::Groups::Menus::SettingsMenu.new(context))
+ add_invite_members_menu
end
override :aria_label
def aria_label
context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
end
+
+ private
+
+ def add_invite_members_menu
+ experiment(:invite_members_in_side_nav, group: context.group) do |e|
+ e.control {}
+ e.candidate { add_menu(Sidebars::Groups::Menus::InviteTeamMembersMenu.new(context)) }
+ end
+ end
end
end
end
diff --git a/lib/sidebars/panel.rb b/lib/sidebars/panel.rb
index 75b3ba65729..e8c02a2d707 100644
--- a/lib/sidebars/panel.rb
+++ b/lib/sidebars/panel.rb
@@ -4,6 +4,7 @@ module Sidebars
class Panel
extend ::Gitlab::Utils::Override
include ::Sidebars::Concerns::PositionableList
+ include Gitlab::Experiment::Dsl
attr_reader :context, :scope_menu, :hidden_menu
diff --git a/lib/sidebars/projects/menus/invite_team_members_menu.rb b/lib/sidebars/projects/menus/invite_team_members_menu.rb
new file mode 100644
index 00000000000..0db49f1e12a
--- /dev/null
+++ b/lib/sidebars/projects/menus/invite_team_members_menu.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ class InviteTeamMembersMenu < ::Sidebars::Menu
+ override :title
+ def title
+ s_('InviteMember|Invite members')
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :admin_project_member, context.project) && all_valid_members.size <= 1
+ end
+
+ override :menu_partial
+ def menu_partial
+ 'projects/invite_members_side_nav_link'
+ end
+
+ override :menu_partial_options
+ def menu_partial_options
+ {
+ project: context.project,
+ title: title,
+ sidebar_menu: self
+ }
+ end
+
+ override :extra_nav_link_html_options
+ def extra_nav_link_html_options
+ {
+ 'data-test-id': 'side-nav-invite-members'
+ }
+ end
+
+ private
+
+ def all_valid_members
+ MembersFinder.new(context.project, context.current_user)
+ .execute(include_relations: [:inherited, :direct, :invited_groups])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb
index 374662162b5..8fbd71c1543 100644
--- a/lib/sidebars/projects/panel.rb
+++ b/lib/sidebars/projects/panel.rb
@@ -36,6 +36,14 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
add_menu(Sidebars::Projects::Menus::SnippetsMenu.new(context))
add_menu(Sidebars::Projects::Menus::SettingsMenu.new(context))
+ add_invite_members_menu
+ end
+
+ def add_invite_members_menu
+ experiment(:invite_members_in_side_nav, group: context.project.group) do |e|
+ e.control {}
+ e.candidate { add_menu(Sidebars::Projects::Menus::InviteTeamMembersMenu.new(context)) }
+ end
end
def confluence_or_wiki_menu
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cca6b77fddb..009b576ffad 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19597,6 +19597,9 @@ msgstr ""
msgid "Iterations|Title"
msgstr ""
+msgid "Iterations|Unable to find iteration cadence."
+msgstr ""
+
msgid "Iterations|Unable to find iteration."
msgstr ""
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index aebe13c39fe..521b4cd4002 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -66,7 +66,6 @@ RSpec.describe 'Database schema' do
oauth_access_grants: %w[resource_owner_id application_id],
oauth_access_tokens: %w[resource_owner_id application_id],
oauth_applications: %w[owner_id],
- open_project_tracker_data: %w[closed_status_id],
packages_build_infos: %w[pipeline_id],
packages_package_file_build_infos: %w[pipeline_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index 39881a28b11..29c7e0ddd21 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -3,35 +3,110 @@
require 'spec_helper'
RSpec.describe 'Contextual sidebar', :js do
- let_it_be(:project) { create(:project) }
+ context 'when context is a project' do
+ let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.owner }
- before do
- sign_in(user)
+ before do
+ sign_in(user)
+ end
- visit project_path(project)
- end
+ context 'when analyzing the menu' do
+ before do
+ visit project_path(project)
+ end
+
+ it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
+ expect(page).not_to have_selector('.js-sidebar-collapsed')
+
+ find('.rspec-link-pipelines').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
+
+ find('.rspec-project-link').hover
+
+ expect(page).not_to have_selector('.is-showing-fly-out')
+
+ find('.rspec-toggle-sidebar').click
+
+ find('.rspec-link-pipelines').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
- it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
- expect(page).not_to have_selector('.js-sidebar-collapsed')
+ find('.rspec-project-link').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
+ end
+ end
+
+ context 'with invite_members_in_side_nav experiment', :experiment do
+ it 'allows opening of modal for the candidate experience' do
+ stub_experiments(invite_members_in_side_nav: :candidate)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: project.group)
+ .on_next_instance
+
+ visit project_path(project)
+
+ page.within '[data-test-id="side-nav-invite-members"' do
+ find('[data-test-id="invite-members-button"').click
+ end
+
+ expect(page).to have_content("You're inviting members to the")
+ end
+
+ it 'does not have invite members link in side nav for the control experience' do
+ stub_experiments(invite_members_in_side_nav: :control)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: project.group)
+ .on_next_instance
+
+ visit project_path(project)
+
+ expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
+ end
+ end
+ end
- find('.rspec-link-pipelines').hover
+ context 'when context is a group' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) do
+ create(:group).tap do |g|
+ g.add_owner(user)
+ end
+ end
- expect(page).to have_selector('.is-showing-fly-out')
+ before do
+ sign_in(user)
+ end
- find('.rspec-project-link').hover
+ context 'with invite_members_in_side_nav experiment', :experiment do
+ it 'allows opening of modal for the candidate experience' do
+ stub_experiments(invite_members_in_side_nav: :candidate)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: group)
+ .on_next_instance
- expect(page).not_to have_selector('.is-showing-fly-out')
+ visit group_path(group)
- find('.rspec-toggle-sidebar').click
+ page.within '[data-test-id="side-nav-invite-members"' do
+ find('[data-test-id="invite-members-button"').click
+ end
- find('.rspec-link-pipelines').hover
+ expect(page).to have_content("You're inviting members to the")
+ end
- expect(page).to have_selector('.is-showing-fly-out')
+ it 'does not have invite members link in side nav for the control experience' do
+ stub_experiments(invite_members_in_side_nav: :control)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: group)
+ .on_next_instance
- find('.rspec-project-link').hover
+ visit group_path(group)
- expect(page).to have_selector('.is-showing-fly-out')
+ expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
+ end
+ end
end
end
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index b2ebb9e4a47..3fce23f854c 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '~/invite_members/constants';
jest.mock('~/experimentation/experiment_tracking');
@@ -15,6 +16,7 @@ let findButton;
const triggerComponent = {
button: GlButton,
anchor: GlLink,
+ 'side-nav': GlLink,
};
const createComponent = (props = {}) => {
@@ -27,9 +29,23 @@ const createComponent = (props = {}) => {
});
};
-describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
- triggerProps = { triggerElement, triggerSource };
- findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
+const triggerItems = [
+ {
+ triggerElement: TRIGGER_ELEMENT_BUTTON,
+ },
+ {
+ triggerElement: 'anchor',
+ },
+ {
+ triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
+ icon: 'plus',
+ },
+];
+
+describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
+ triggerProps = { ...triggerItem, triggerSource };
+
+ findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]);
afterEach(() => {
wrapper.destroy();
@@ -91,3 +107,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
});
});
});
+
+describe('side-nav with icon', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('plus');
+ });
+});
diff --git a/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
new file mode 100644
index 00000000000..a79e5182f45
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::InviteTeamMembersMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:group) do
+ build(:group).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:context) { Sidebars::Groups::Context.new(current_user: owner, container: group) }
+
+ subject(:invite_menu) { described_class.new(context) }
+
+ context 'when the group is viewed by an owner of the group' do
+ describe '#render?' do
+ it 'renders the Invite team members link' do
+ expect(invite_menu.render?).to eq(true)
+ end
+
+ context 'when the group already has at least 2 members' do
+ before do
+ group.add_guest(guest)
+ end
+
+ it 'does not render the link' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+
+ describe '#title' do
+ it 'displays the correct Invite team members text for the link in the side nav' do
+ expect(invite_menu.title).to eq('Invite members')
+ end
+ end
+ end
+
+ context 'when the group is viewed by a guest user without admin permissions' do
+ let(:context) { Sidebars::Groups::Context.new(current_user: guest, container: group) }
+
+ before do
+ group.add_guest(guest)
+ end
+
+ describe '#render?' do
+ it 'does not render the link' do
+ expect(subject.render?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
new file mode 100644
index 00000000000..df9b260d211
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::InviteTeamMembersMenu do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:context) { Sidebars::Projects::Context.new(current_user: owner, container: project) }
+
+ subject(:invite_menu) { described_class.new(context) }
+
+ context 'when the project is viewed by an owner of the group' do
+ let(:owner) { project.owner }
+
+ describe '#render?' do
+ it 'renders the Invite team members link' do
+ expect(invite_menu.render?).to eq(true)
+ end
+
+ context 'when the project already has at least 2 members' do
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'does not render the link' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+
+ describe '#title' do
+ it 'displays the correct Invite team members text for the link in the side nav' do
+ expect(invite_menu.title).to eq('Invite members')
+ end
+ end
+ end
+
+ context 'when the project is viewed by a guest user without admin permissions' do
+ let(:context) { Sidebars::Projects::Context.new(current_user: guest, container: project) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ describe '#render?' do
+ it 'does not render' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+end