From 01a8b31afeac9ac1eaf64d781de5ee8f15eb1897 Mon Sep 17 00:00:00 2001
From: GitLab Bot
Date: Mon, 16 Nov 2020 15:09:23 +0000
Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master
---
.eslintrc.yml | 1 +
.scss-lint.yml | 1 +
GITALY_SERVER_VERSION | 2 +-
.../clusters/components/applications.vue | 11 +
.../javascripts/helpers/startup_css_helper.js | 12 +-
.../edit/components/confirmation_modal.vue | 4 +-
.../edit/components/integration_form.vue | 32 +-
.../components/reset_confirmation_modal.vue | 61 ++
.../javascripts/integrations/edit/index.js | 2 +
.../integrations/edit/store/actions.js | 2 +
.../integrations/edit/store/getters.js | 2 +-
.../integrations/edit/store/mutation_types.js | 1 +
.../integrations/edit/store/mutations.js | 3 +
.../integrations/edit/store/state.js | 1 +
app/assets/javascripts/lib/utils/css_utils.js | 2 +
.../javascripts/lib/utils/text_utility.js | 12 +
.../stylesheets/lazy_bundles/select2.scss | 654 ++++++++++++++++++
.../lazy_bundles/select2_overrides.scss | 359 ++++++++++
app/assets/stylesheets/pages/commits.scss | 4 +-
app/helpers/services_helper.rb | 3 +-
.../application_setting_implementation.rb | 2 +-
app/models/member.rb | 4 +
app/views/projects/blob/_upload.html.haml | 2 +-
app/views/projects/commits/_commit.html.haml | 2 +-
.../shared/issuable/_label_dropdown.html.haml | 2 +-
app/views/shared/issuable/_sidebar.html.haml | 2 +-
.../shared/issuable/_sort_dropdown.html.haml | 2 +-
.../issuable/form/_type_selector.html.haml | 2 +-
.../224509-chevron-down-svg-issuable.yml | 5 +
...min-approval-default-for-new-instances.yml | 5 +
...-get-invitations-for-group-and-project.yml | 5 +
config/application.rb | 1 +
...ter_user_signup_to_application_settings.rb | 9 +
db/schema_migrations/20201107032257 | 1 +
db/structure.sql | 2 +-
doc/administration/sidekiq.md | 1 -
doc/api/invitations.md | 43 +-
.../documentation/restful_api_styleguide.md | 7 +
doc/development/fe_guide/performance.md | 30 +-
doc/development/fe_guide/vue.md | 24 +
.../settings/sign_up_restrictions.md | 5 +-
lib/api/entities/invitation.rb | 3 +-
lib/api/helpers/members_helpers.rb | 11 +
lib/api/invitations.rb | 19 +
lib/gitlab/current_settings.rb | 4 +-
lib/gitlab/gon_helper.rb | 1 +
locale/gitlab.pot | 27 +
qa/qa/page/main/sign_up.rb | 14 +-
qa/qa/runtime/application_settings.rb | 2 +-
qa/qa/runtime/env.rb | 7 +-
.../1_manage/login/register_spec.rb | 41 +-
.../registrations_controller_spec.rb | 5 +-
spec/features/invites_spec.rb | 16 +
.../projects/snippets/create_snippet_spec.rb | 2 +-
.../uploads/user_uploads_file_to_note_spec.rb | 2 +-
spec/features/users/signup_spec.rb | 20 +
.../edit/components/integration_form_spec.js | 59 ++
.../integrations/edit/store/actions_spec.js | 27 +-
.../integrations/edit/store/getters_spec.js | 32 +-
.../integrations/edit/store/mutations_spec.js | 24 +
.../integrations/edit/store/state_spec.js | 1 +
spec/frontend/lib/utils/text_utility_spec.js | 15 +
spec/requests/api/invitations_spec.rb | 98 ++-
spec/requests/api/settings_spec.rb | 2 +-
tooling/README.md | 4 +
.../eslint-config/conditionally_ignore_ee.js | 5 +
66 files changed, 1698 insertions(+), 68 deletions(-)
create mode 100644 app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
create mode 100644 app/assets/stylesheets/lazy_bundles/select2.scss
create mode 100644 app/assets/stylesheets/lazy_bundles/select2_overrides.scss
create mode 100644 changelogs/unreleased/224509-chevron-down-svg-issuable.yml
create mode 100644 changelogs/unreleased/267568-user-admin-approval-default-for-new-instances.yml
create mode 100644 changelogs/unreleased/add-get-invitations-for-group-and-project.yml
create mode 100644 db/migrate/20201107032257_add_default_true_require_admin_approval_after_user_signup_to_application_settings.rb
create mode 100644 db/schema_migrations/20201107032257
create mode 100644 tooling/README.md
create mode 100644 tooling/eslint-config/conditionally_ignore_ee.js
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b8683ba803f..5529d82ccf5 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -3,6 +3,7 @@ extends:
- plugin:@gitlab/i18n
- plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4
+ - ./tooling/eslint-config/conditionally_ignore_ee.js
globals:
__webpack_public_path__: true
gl: false
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 420b15274bb..ec193e2a3db 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -8,6 +8,7 @@ scss_files:
exclude:
- 'app/assets/stylesheets/pages/emojis.scss'
- 'app/assets/stylesheets/startup/startup-*.scss'
+ - 'app/assets/stylesheets/lazy_bundles/select2.scss'
linters:
# Reports when you use improper spacing around ! (the "bang") in !default,
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index c0359917f4f..4862ceb7a20 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-506c44cc07dcb804ce970ec1c02bb6e0d52320d8
+40d58655a42f71b6180a3cbaf369cc20b60e695a
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 912568c8870..271d862afab 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -467,6 +467,17 @@ export default {
notebooks to a class of students, a corporate data science group,
or a scientific research group.`)
}}
+
+
+ {{ content }}
+
+
diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js
index 8351cbf04bf..d41a6209898 100644
--- a/app/assets/javascripts/helpers/startup_css_helper.js
+++ b/app/assets/javascripts/helpers/startup_css_helper.js
@@ -20,17 +20,7 @@ const handleStartupEvents = () => {
}
};
-/* Wait for.... The methods can be used:
- - with a callback (preferred),
- waitFor(action)
-
- - with then (discouraged),
- await waitFor().then(action);
-
- - with await,
- await waitFor;
- action();
--*/
+/* For `waitForCSSLoaded` methods, see docs.gitlab.com/ee/development/fe_guide/performance.html#important-considerations */
export const waitForCSSLoaded = (action = () => {}) => {
if (!gon?.features?.startupCss || allLinksLoaded()) {
return new Promise(resolve => {
diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
index d4489bbf693..93ea1f4f636 100644
--- a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
@@ -8,14 +8,14 @@ export default {
GlModal,
},
computed: {
- ...mapGetters(['isSavingOrTesting']),
+ ...mapGetters(['isDisabled']),
primaryProps() {
return {
text: __('Save'),
attributes: [
{ variant: 'success' },
{ category: 'primary' },
- { disabled: this.isSavingOrTesting },
+ { disabled: this.isDisabled },
],
};
},
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index e42a8b7de7c..bbfa865905a 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -12,6 +12,7 @@ import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue';
import DynamicField from './dynamic_field.vue';
import ConfirmationModal from './confirmation_modal.vue';
+import ResetConfirmationModal from './reset_confirmation_modal.vue';
export default {
name: 'IntegrationForm',
@@ -23,6 +24,7 @@ export default {
TriggerFields,
DynamicField,
ConfirmationModal,
+ ResetConfirmationModal,
GlButton,
},
directives: {
@@ -30,8 +32,8 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
- ...mapState(['defaultState', 'override', 'isSaving', 'isTesting']),
+ ...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
+ ...mapState(['defaultState', 'override', 'isSaving', 'isTesting', 'isResetting']),
isEditable() {
return this.propsSource.editable;
},
@@ -47,9 +49,12 @@ export default {
showJiraIssuesFields() {
return this.isJira && this.glFeatures.jiraIssuesIntegration;
},
+ showReset() {
+ return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
+ },
},
methods: {
- ...mapActions(['setOverride', 'setIsSaving', 'setIsTesting']),
+ ...mapActions(['setOverride', 'setIsSaving', 'setIsTesting', 'setIsResetting']),
onSaveClick() {
this.setIsSaving(true);
eventHub.$emit('saveIntegration');
@@ -58,6 +63,7 @@ export default {
this.setIsTesting(true);
eventHub.$emit('testIntegration');
},
+ onResetClick() {},
},
};
@@ -100,7 +106,7 @@ export default {
category="primary"
variant="success"
:loading="isSaving"
- :disabled="isSavingOrTesting"
+ :disabled="isDisabled"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
@@ -113,7 +119,7 @@ export default {
variant="success"
type="submit"
:loading="isSaving"
- :disabled="isSavingOrTesting"
+ :disabled="isDisabled"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
>
@@ -123,13 +129,27 @@ export default {
{{ __('Test settings') }}
+
+
+ {{ __('Reset') }}
+
+
+
+
{{ __('Cancel') }}
diff --git a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
new file mode 100644
index 00000000000..d8503910566
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+ {{
+ s__(
+ 'Integrations|Resetting this integration will clear the settings and deactivate this integration.',
+ )
+ }}
+
+
+ {{ s__('Integrations|All projects inheriting these settings will also be reset.') }}
+
+
+
+ {{ s__('Integrations|Projects using custom settings will not be affected.') }}
+
+
+
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 248ee62d43a..95a53f1beab 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -26,6 +26,7 @@ function parseDatasetToProps(data) {
integrationLevel,
cancelPath,
testPath,
+ resetPath,
...booleanAttributes
} = data;
const {
@@ -49,6 +50,7 @@ function parseDatasetToProps(data) {
editable,
canTest,
testPath,
+ resetPath,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 199c9074ead..097304be242 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -3,3 +3,5 @@ import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving);
export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
+export const setIsResetting = ({ commit }, isResetting) =>
+ commit(types.SET_IS_RESETTING, isResetting);
diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js
index 4ee5f11855c..310d970c73e 100644
--- a/app/assets/javascripts/integrations/edit/store/getters.js
+++ b/app/assets/javascripts/integrations/edit/store/getters.js
@@ -1,6 +1,6 @@
export const isInheriting = state => (state.defaultState === null ? false : !state.override);
-export const isSavingOrTesting = state => state.isSaving || state.isTesting;
+export const isDisabled = state => state.isSaving || state.isTesting || state.isResetting;
export const propsSource = (state, getters) =>
getters.isInheriting ? state.defaultState : state.customState;
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index 0dae8ea079e..2a84408f658 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -1,3 +1,4 @@
export const SET_OVERRIDE = 'SET_OVERRIDE';
export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
+export const SET_IS_RESETTING = 'SET_IS_RESETTING';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 8ac3c476f9e..07e3e25ccf0 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -10,4 +10,7 @@ export default {
[types.SET_IS_TESTING](state, isTesting) {
state.isTesting = isTesting;
},
+ [types.SET_IS_RESETTING](state, isResetting) {
+ state.isResetting = isResetting;
+ },
};
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index a9ecee6c539..aae3db1583f 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -7,5 +7,6 @@ export default ({ defaultState = null, customState = {} } = {}) => {
customState,
isSaving: false,
isTesting: false,
+ isResetting: false,
};
};
diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js
index 90213221443..02f092e73e1 100644
--- a/app/assets/javascripts/lib/utils/css_utils.js
+++ b/app/assets/javascripts/lib/utils/css_utils.js
@@ -1,5 +1,7 @@
export function loadCSSFile(path) {
return new Promise(resolve => {
+ if (!path) resolve();
+
if (document.querySelector(`link[href="${path}"]`)) {
resolve();
} else {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 8ac6a44cba9..a81ca3f211f 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -399,3 +399,15 @@ export const truncateNamespace = (string = '') => {
* @returns {Boolean}
*/
export const hasContent = obj => isString(obj) && obj.trim() !== '';
+
+/**
+ * A utility function that validates if a
+ * string is valid SHA1 hash format.
+ *
+ * @param {String} hash to validate
+ *
+ * @return {Boolean} true if valid
+ */
+export const isValidSha1Hash = str => {
+ return /^[0-9a-f]{5,40}$/.test(str);
+};
diff --git a/app/assets/stylesheets/lazy_bundles/select2.scss b/app/assets/stylesheets/lazy_bundles/select2.scss
new file mode 100644
index 00000000000..f2c372020ef
--- /dev/null
+++ b/app/assets/stylesheets/lazy_bundles/select2.scss
@@ -0,0 +1,654 @@
+/*
+Version: 3.5.2 Timestamp: Sat Nov 1 14:43:36 EDT 2014
+Updated 2020-10-05 by TimZ
+*/
+.select2-container {
+ margin: 0;
+ position: relative;
+ display: inline-block;
+}
+
+.select2-container,
+.select2-drop,
+.select2-search,
+.select2-search input {
+ box-sizing: border-box;
+}
+
+.select2-container .select2-choice {
+ display: block;
+ height: 26px;
+ padding: 0 0 0 8px;
+ overflow: hidden;
+ position: relative;
+
+ border: 1px solid #aaa;
+ white-space: nowrap;
+ line-height: 26px;
+ color: #444;
+ text-decoration: none;
+
+ border-radius: 4px;
+
+ background-clip: padding-box;
+
+ user-select: none;
+
+ background-color: #fff;
+ background-image: linear-gradient(to top, #eee 0%, #fff 50%);
+}
+
+html[dir='rtl'] .select2-container .select2-choice {
+ padding: 0 8px 0 0;
+}
+
+.select2-container.select2-drop-above .select2-choice {
+ border-bottom-color: #aaa;
+
+ border-radius: 0 0 4px 4px;
+
+ background-image: linear-gradient(to bottom, #eee 0%, #fff 90%);
+}
+
+.select2-container.select2-allowclear .select2-choice .select2-chosen {
+ margin-right: 42px;
+}
+
+.select2-container .select2-choice > .select2-chosen {
+ margin-right: 26px;
+ display: block;
+ overflow: hidden;
+
+ white-space: nowrap;
+
+ text-overflow: ellipsis;
+ float: none;
+ width: auto;
+}
+
+html[dir='rtl'] .select2-container .select2-choice > .select2-chosen {
+ margin-left: 26px;
+ margin-right: 0;
+}
+
+.select2-container .select2-choice abbr {
+ display: none;
+ width: 12px;
+ height: 12px;
+ position: absolute;
+ right: 24px;
+ top: 8px;
+
+ font-size: 1px;
+ text-decoration: none;
+
+ border: 0;
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path('select2.png')) right top no-repeat;
+ cursor: pointer;
+ outline: 0;
+}
+
+.select2-container.select2-allowclear .select2-choice abbr {
+ display: inline-block;
+}
+
+.select2-container .select2-choice abbr:hover {
+ background-position: right -11px;
+ cursor: pointer;
+}
+
+.select2-drop-mask {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ position: fixed;
+ left: 0;
+ top: 0;
+ min-height: 100%;
+ min-width: 100%;
+ height: auto;
+ width: auto;
+ opacity: 0;
+ z-index: 9998;
+ /* styles required for IE to work */
+ background-color: #fff;
+ filter: alpha(opacity=0);
+}
+
+.select2-drop {
+ width: 100%;
+ margin-top: -1px;
+ position: absolute;
+ z-index: 9999;
+ top: 100%;
+
+ background: #fff;
+ color: #000;
+ border: 1px solid #aaa;
+ border-top: 0;
+
+ border-radius: 0 0 4px 4px;
+
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
+}
+
+.select2-drop.select2-drop-above {
+ margin-top: 1px;
+ border-top: 1px solid #aaa;
+ border-bottom: 0;
+
+ border-radius: 4px 4px 0 0;
+
+ box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15);
+}
+
+.select2-drop-active {
+ border: 1px solid #5897fb;
+ border-top: 0;
+}
+
+.select2-drop.select2-drop-above.select2-drop-active {
+ border-top: 1px solid #5897fb;
+}
+
+.select2-drop-auto-width {
+ border-top: 1px solid #aaa;
+ width: auto;
+}
+
+.select2-drop-auto-width .select2-search {
+ padding-top: 4px;
+}
+
+.select2-container .select2-choice .select2-arrow {
+ display: inline-block;
+ width: 18px;
+ height: 100%;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ border-left: 1px solid #aaa;
+ border-radius: 0 4px 4px 0;
+
+ background-clip: padding-box;
+
+ background: #ccc;
+ background-image: linear-gradient(to top, #ccc 0%, #eee 60%);
+}
+
+html[dir='rtl'] .select2-container .select2-choice .select2-arrow {
+ left: 0;
+ right: auto;
+
+ border-left: 0;
+ border-right: 1px solid #aaa;
+ border-radius: 4px 0 0 4px;
+}
+
+.select2-container .select2-choice .select2-arrow b {
+ display: block;
+ width: 100%;
+ height: 100%;
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path("select2.png")) no-repeat 0 1px;
+}
+
+html[dir='rtl'] .select2-container .select2-choice .select2-arrow b {
+ background-position: 2px 1px;
+}
+
+.select2-search {
+ display: inline-block;
+ width: 100%;
+ min-height: 26px;
+ margin: 0;
+ padding-left: 4px;
+ padding-right: 4px;
+
+ position: relative;
+ z-index: 10000;
+
+ white-space: nowrap;
+}
+
+.select2-search input {
+ width: 100%;
+ height: auto !important;
+ min-height: 26px;
+ padding: 4px 20px 4px 5px;
+ margin: 0;
+
+ outline: 0;
+ font-family: sans-serif;
+ font-size: 1em;
+
+ border: 1px solid #aaa;
+ border-radius: 0;
+
+ box-shadow: none;
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path('select2.png')) no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
+}
+
+html[dir='rtl'] .select2-search input {
+ padding: 4px 5px 4px 20px;
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path('select2.png')) no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
+}
+
+.select2-drop.select2-drop-above .select2-search input {
+ margin-top: 4px;
+}
+
+.select2-search input.select2-active {
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path('select2-spinner.gif')) no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
+}
+
+.select2-container-active .select2-choice,
+.select2-container-active .select2-choices {
+ border: 1px solid #5897fb;
+ outline: none;
+
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+}
+
+.select2-dropdown-open .select2-choice {
+ border-bottom-color: transparent;
+ box-shadow: 0 1px 0 #fff inset;
+
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+
+ background-color: #eee;
+ background-image: linear-gradient(to top, #fff 0%, #eee 50%);
+}
+
+.select2-dropdown-open.select2-drop-above .select2-choice,
+.select2-dropdown-open.select2-drop-above .select2-choices {
+ border: 1px solid #5897fb;
+ border-top-color: transparent;
+
+ background-image: linear-gradient(to bottom, #fff 0%, #eee 50%);
+}
+
+.select2-dropdown-open .select2-choice .select2-arrow {
+ background: transparent;
+ border-left: 0;
+ filter: none;
+}
+
+html[dir='rtl'] .select2-dropdown-open .select2-choice .select2-arrow {
+ border-right: 0;
+}
+
+.select2-dropdown-open .select2-choice .select2-arrow b {
+ background-position: -18px 1px;
+}
+
+html[dir='rtl'] .select2-dropdown-open .select2-choice .select2-arrow b {
+ background-position: -16px 1px;
+}
+
+.select2-hidden-accessible {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
+/* results */
+.select2-results {
+ max-height: 200px;
+ padding: 0 0 0 4px;
+ margin: 4px 4px 4px 0;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+html[dir='rtl'] .select2-results {
+ padding: 0 4px 0 0;
+ margin: 4px 0 4px 4px;
+}
+
+.select2-results ul.select2-result-sub {
+ margin: 0;
+ padding-left: 0;
+}
+
+.select2-results li {
+ list-style: none;
+ display: list-item;
+ background-image: none;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+ padding: 3px 7px 4px;
+ margin: 0;
+ cursor: pointer;
+
+ min-height: 1em;
+
+ user-select: none;
+}
+
+.select2-results-dept-1 .select2-result-label { padding-left: 20px; }
+.select2-results-dept-2 .select2-result-label { padding-left: 40px; }
+.select2-results-dept-3 .select2-result-label { padding-left: 60px; }
+.select2-results-dept-4 .select2-result-label { padding-left: 80px; }
+.select2-results-dept-5 .select2-result-label { padding-left: 100px; }
+.select2-results-dept-6 .select2-result-label { padding-left: 110px; }
+.select2-results-dept-7 .select2-result-label { padding-left: 120px; }
+
+.select2-results .select2-highlighted {
+ background: #3875d7;
+ color: #fff;
+}
+
+.select2-results li em {
+ background: #feffde;
+ font-style: normal;
+}
+
+.select2-results .select2-highlighted em {
+ background: transparent;
+}
+
+.select2-results .select2-highlighted ul {
+ background: #fff;
+ color: #000;
+}
+
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: #f4f4f4;
+ display: list-item;
+ padding-left: 5px;
+}
+
+/*
+disabled look for disabled choices in the results dropdown
+*/
+.select2-results .select2-disabled.select2-highlighted {
+ color: #666;
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+
+.select2-results .select2-disabled {
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+
+.select2-results .select2-selected {
+ display: none;
+}
+
+.select2-more-results.select2-active {
+ /* stylelint-disable-next-line function-url-quotes */
+ background: #f4f4f4 url(image-path('select2-spinner.gif')) no-repeat 100%;
+}
+
+.select2-results .select2-ajax-error {
+ background: rgba(255, 50, 50, 0.2);
+}
+
+.select2-more-results {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/* disabled styles */
+
+.select2-container.select2-container-disabled .select2-choice {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container.select2-container-disabled .select2-choice .select2-arrow {
+ background-color: #f4f4f4;
+ background-image: none;
+ border-left: 0;
+}
+
+.select2-container.select2-container-disabled .select2-choice abbr {
+ display: none;
+}
+
+
+/* multiselect */
+
+.select2-container-multi .select2-choices {
+ height: auto !important;
+ height: 1%;
+ margin: 0;
+ padding: 0 5px 0 0;
+ position: relative;
+
+ border: 1px solid #aaa;
+ cursor: text;
+ overflow: hidden;
+
+ background-color: #fff;
+ background-image: linear-gradient(to bottom, #eee 1%, #fff 15%);
+}
+
+html[dir='rtl'] .select2-container-multi .select2-choices {
+ padding: 0 0 0 5px;
+}
+
+.select2-locked {
+ padding: 3px 5px !important;
+}
+
+.select2-container-multi .select2-choices {
+ min-height: 26px;
+}
+
+.select2-container-multi.select2-container-active .select2-choices {
+ border: 1px solid #5897fb;
+ outline: none;
+
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+}
+
+.select2-container-multi .select2-choices li {
+ float: left;
+ list-style: none;
+}
+
+html[dir='rtl'] .select2-container-multi .select2-choices li {
+ float: right;
+}
+
+.select2-container-multi .select2-choices .select2-search-field {
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input {
+ padding: 5px;
+ margin: 1px 0;
+
+ font-family: sans-serif;
+ font-size: 100%;
+ color: #666;
+ outline: 0;
+ border: 0;
+
+ box-shadow: none;
+ background: transparent !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+ /* stylelint-disable-next-line function-url-quotes */
+ background: #fff url(image-path('select2-spinner.gif')) no-repeat 100% !important;
+}
+
+.select2-default {
+ color: #999 !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
+ padding: 3px 5px 3px 18px;
+ margin: 3px 0 3px 5px;
+ position: relative;
+
+ line-height: 13px;
+ color: #333;
+ cursor: default;
+ border: 1px solid #aaa;
+
+ border-radius: 3px;
+
+ box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
+
+ background-clip: padding-box;
+
+ user-select: none;
+
+ background-color: #e4e4e4;
+ background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+}
+
+html[dir='rtl'] .select2-container-multi .select2-choices .select2-search-choice {
+ margin: 3px 5px 3px 0;
+ padding: 3px 18px 3px 5px;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
+ cursor: default;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice-focus {
+ background: #d4d4d4;
+}
+
+.select2-search-choice-close {
+ display: block;
+ width: 12px;
+ height: 13px;
+ position: absolute;
+ right: 3px;
+ top: 4px;
+
+ font-size: 1px;
+ outline: none;
+ /* stylelint-disable-next-line function-url-quotes */
+ background: url(image-path('select2.png')) right top no-repeat;
+}
+
+html[dir='rtl'] .select2-search-choice-close {
+ right: auto;
+ left: 3px;
+}
+
+.select2-container-multi .select2-search-choice-close {
+ left: 3px;
+}
+
+html[dir='rtl'] .select2-container-multi .select2-search-choice-close {
+ left: auto;
+ right: 2px;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
+ background-position: right -11px;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
+ background-position: right -11px;
+}
+
+/* disabled styles */
+.select2-container-multi.select2-container-disabled .select2-choices {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
+ padding: 3px 5px;
+ border: 1px solid #ddd;
+ background-image: none;
+ background-color: #f4f4f4;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {
+ display: none;
+ background: none;
+}
+/* end multiselect */
+
+
+.select2-result-selectable .select2-match,
+.select2-result-unselectable .select2-match {
+ text-decoration: underline;
+}
+
+.select2-offscreen,
+.select2-offscreen:focus {
+ clip: rect(0 0 0 0) !important;
+ width: 1px !important;
+ height: 1px !important;
+ border: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ position: absolute !important;
+ outline: 0 !important;
+ left: 0 !important;
+ top: 0 !important;
+}
+
+.select2-display-none {
+ display: none;
+}
+
+.select2-measure-scrollbar {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+}
+
+@media only screen and (min-resolution: 120dpi) {
+ .select2-search input,
+ .select2-search-choice-close,
+ .select2-container .select2-choice abbr,
+ .select2-container .select2-choice .select2-arrow b {
+ /* stylelint-disable-next-line function-url-quotes */
+ background-image: url(image-path("select2x2.png")) !important;
+ background-repeat: no-repeat !important;
+ background-size: 60px 40px !important;
+ }
+
+ .select2-search input {
+ background-position: 100% -21px !important;
+ }
+}
+
+/* End of select2.css */
+
+@import './select2_overrides';
diff --git a/app/assets/stylesheets/lazy_bundles/select2_overrides.scss b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
new file mode 100644
index 00000000000..6c51c4b0ec3
--- /dev/null
+++ b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
@@ -0,0 +1,359 @@
+@import 'page_bundles/mixins_and_variables_and_functions';
+/** Select2 selectbox style override **/
+.select2-container {
+ width: 100% !important;
+
+ &.input-md,
+ &.input-lg {
+ display: block;
+ }
+}
+
+.select2-container,
+.select2-container.select2-drop-above {
+ .select2-choice {
+ background: $white;
+ color: $gl-text-color;
+ border-color: $border-color;
+ height: 34px;
+ padding: $gl-vert-padding $gl-input-padding;
+ font-size: $gl-font-size;
+ line-height: 1.42857143;
+ border-radius: $gl-border-radius-base;
+
+ .select2-arrow {
+ background-image: none;
+ background-color: transparent;
+ border: 0;
+ padding-top: 12px;
+ padding-right: 20px;
+ font-size: 10px;
+
+ b {
+ display: none;
+ }
+
+ &::after {
+ content: '\f078';
+ position: absolute;
+ z-index: 1;
+ text-align: center;
+ pointer-events: none;
+ box-sizing: border-box;
+ color: $gray-darkest;
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+
+ .select2-chosen {
+ margin-right: 15px;
+ }
+
+ &:hover {
+ border-color: $gray-darkest;
+ color: $gl-text-color;
+ }
+ }
+
+ // Essentially we’re doing @include form-control-focus here (from
+ // bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
+ // `&:focus` selector and we’re never actually focusing the .select2-choice
+ // link nor the .select2-container, the Select2 library focuses an off-screen
+ // .select2-focusser element instead.
+ &.select2-container-active:not(.select2-dropdown-open) {
+ .select2-choice {
+ color: $input-focus-color;
+ background-color: $input-focus-bg;
+ border-color: $input-focus-border-color;
+ outline: 0;
+ }
+
+ // Reusable focus “glow” box-shadow
+ @mixin form-control-focus-glow {
+ @if $enable-shadows {
+ box-shadow: $input-box-shadow, $input-focus-box-shadow;
+ } @else {
+ box-shadow: $input-focus-box-shadow;
+ }
+ }
+
+ // Apply the focus “glow” shadow to the .select2-container if it also has
+ // the .block-truncated class as that applies an overflow: hidden, thereby
+ // hiding the glow of the nested .select2-choice element.
+ &.block-truncated {
+ @include form-control-focus-glow;
+ }
+
+ // Apply the glow directly to the .select2-choice link if we’re not
+ // block-truncating the container.
+ &:not(.block-truncated) .select2-choice {
+ @include form-control-focus-glow;
+ }
+ }
+
+ &.is-invalid {
+ ~ .invalid-feedback {
+ display: block;
+ }
+
+ .select2-choices,
+ .select2-choice {
+ border-color: $red-500;
+ }
+ }
+}
+
+.select2-drop,
+.select2-drop.select2-drop-above {
+ background: $white;
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+ border-radius: $gl-border-radius-base;
+ border: 1px solid $border-color;
+ min-width: 175px;
+ color: $gl-text-color;
+ z-index: 999;
+
+ .modal-open & {
+ z-index: $zindex-modal + 200;
+ }
+}
+
+.select2-drop-mask {
+ z-index: 998;
+
+ .modal-open & {
+ z-index: $zindex-modal + 100;
+ }
+}
+
+.select2-drop.select2-drop-above.select2-drop-active {
+ border-top: 1px solid $border-color;
+ margin-top: -6px;
+}
+
+.select2-container-active {
+ .select2-choice,
+ .select2-choices {
+ box-shadow: none;
+ }
+}
+
+.select2-dropdown-open,
+.select2-dropdown-open.select2-drop-above {
+ .select2-choice {
+ border-color: $gray-darkest;
+ outline: 0;
+ }
+}
+
+.select2-container-multi {
+ .select2-choices {
+ border-radius: $border-radius-default;
+ border-color: $border-color;
+ background: none;
+
+ .select2-search-field input {
+ padding: 5px $gl-input-padding;
+ height: auto;
+ font-family: inherit;
+ font-size: inherit;
+ }
+
+ .select2-search-choice {
+ margin: 5px 0 0 8px;
+ box-shadow: none;
+ border-color: $border-color;
+ color: $gl-text-color;
+ line-height: 15px;
+ background-color: $gray-light;
+ background-image: none;
+ padding: 3px 18px 3px 5px;
+
+ .select2-search-choice-close {
+ top: 5px;
+ left: initial;
+ right: 3px;
+ }
+
+ &.select2-search-choice-focus {
+ border-color: $gl-text-color;
+ }
+ }
+ }
+}
+
+.select2-drop-active {
+ margin-top: $dropdown-vertical-offset;
+ font-size: 14px;
+
+ .select2-results {
+ max-height: 350px;
+ }
+}
+
+.select2-search {
+ padding: $grid-size;
+
+ .select2-drop-auto-width & {
+ padding: $grid-size;
+ }
+
+ input {
+ padding: $grid-size;
+ background: transparent image-url('select2.png');
+ color: $gl-text-color;
+ background-clip: content-box;
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 0 !important;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ line-height: 16px;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+
+ &:focus {
+ border-color: $blue-300;
+ }
+
+ &.select2-active {
+ background-color: $white;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ background-position: right 6px center !important;
+ background-size: 16px 16px !important;
+ }
+ }
+
+ + .select2-results {
+ padding-top: 0;
+ }
+}
+
+.select2-results {
+ margin: 0;
+ padding: #{$gl-padding / 2} 0;
+
+ .select2-no-results,
+ .select2-searching,
+ .select2-ajax-error,
+ .select2-selection-limit {
+ background: transparent;
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-result-label,
+ .select2-more-results {
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-highlighted {
+ background: transparent;
+ color: $gl-text-color;
+
+ .select2-result-label {
+ background: $gray-darker;
+ }
+ }
+
+ .select2-result {
+ padding: 0 1px;
+ }
+
+ li.select2-result-with-children > .select2-result-label {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ }
+}
+
+.select2-highlighted {
+ .group-result {
+ .group-path {
+ color: $gray-700;
+ }
+ }
+}
+
+.select2-result-selectable,
+.select2-result-unselectable {
+ .select2-match {
+ font-weight: $gl-font-weight-bold;
+ text-decoration: none;
+ }
+}
+
+.input-group {
+ .select2-container {
+ display: table-cell;
+ max-width: 180px;
+ }
+}
+
+.file-editor {
+ .select2 {
+ float: right;
+ }
+}
+
+.import-namespace-select {
+ > .select2-choice {
+ border-radius: $border-radius-default 0 0 $border-radius-default;
+ position: relative;
+ left: 1px;
+ }
+}
+
+.issue-form {
+ .select2-container {
+ width: 250px !important;
+ }
+}
+
+.new_project,
+.edit-project,
+.import-project {
+ .input-group {
+ .select2-container {
+ display: unset;
+ max-width: unset;
+ flex-grow: 1;
+ }
+ }
+
+ .input-group-prepend,
+ .input-group-append {
+ + .select2 a {
+ border-radius: 0 $gl-border-radius-base $gl-border-radius-base 0;
+ }
+ }
+}
+
+.project-path {
+ .select2-choice {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
+
+.transfer-project .select2-container {
+ min-width: 200px;
+}
+
+.right-sidebar {
+ .block {
+ .select2-container span {
+ margin-top: 0;
+ }
+ }
+}
+
+.block-truncated {
+ > div:not(.block):not(.select2-display-none) {
+ display: inline;
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 1ad77a52b1d..17474b95e50 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -208,9 +208,9 @@
display: inline-flex;
.label,
- .btn {
+ .btn:not(.gl-button) {
padding: $gl-vert-padding $gl-btn-padding;
- border: 1px $border-color solid;
+ border: 1px $gray-200 solid;
font-size: $gl-font-size;
line-height: $line-height-base;
border-radius: 0;
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 8e41b1562ec..96eb14be4b4 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -93,7 +93,8 @@ module ServicesHelper
editable: integration.editable?.to_s,
cancel_path: scoped_integrations_path,
can_test: integration.can_test?.to_s,
- test_path: scoped_test_integration_path(integration)
+ test_path: scoped_test_integration_path(integration),
+ reset_path: ''
}
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 63ed65e6725..5c7abbccd63 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -120,7 +120,7 @@ module ApplicationSettingImplementation
repository_checks_enabled: true,
repository_storages_weighted: { default: 100 },
repository_storages: ['default'],
- require_admin_approval_after_user_signup: false,
+ require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: 0,
diff --git a/app/models/member.rb b/app/models/member.rb
index 07a3bc1dc1d..687830f5267 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -419,6 +419,10 @@ class Member < ApplicationRecord
invite? && user_id.nil?
end
+ def created_by_name
+ created_by&.name
+ end
+
private
def send_invite
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 4dbfa2b1e3c..f400c7de5eb 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -21,7 +21,7 @@
.form-actions
= button_tag class: 'btn gl-button btn-success btn-upload-file', id: 'submit-all', type: 'button' do
- = icon('spin spinner', class: 'js-loading-icon hidden' )
+ .spinner.spinner-sm.gl-mr-2.js-loading-icon.hidden
= button_title
= link_to _("Cancel"), '#', class: "btn gl-button btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9a572817646..179b0c5efbd 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -57,7 +57,7 @@
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
- .commit-sha-group.d-none.d-sm-flex
+ .commit-sha-group.btn-group.d-none.d-sm-flex
.label.label-monospace.monospace
= commit.short_id
= clipboard_button(text: commit.id, title: _("Copy commit SHA"), class: "gl-button btn btn-default", container: "body")
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 535af522c1a..08883bb3372 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -26,7 +26,7 @@
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 1b3fea93b8e..1f20c1a30aa 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -96,7 +96,7 @@
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
- = icon('chevron-down', 'aria-hidden': 'true')
+ = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-menu-due-date
= dropdown_title(_('Due date'))
= dropdown_content do
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index 81dbecb430b..f60be3f3e4a 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -7,7 +7,7 @@
.btn-group{ role: 'group' }
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index 3347966f39a..5d64c15d9f9 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -8,7 +8,7 @@
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-toggle-text.is-default
= issuable.issue_type.capitalize || _("Select type")
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-menu-selectable.dropdown-select
.dropdown-title.gl-display-flex
%span.gl-ml-auto
diff --git a/changelogs/unreleased/224509-chevron-down-svg-issuable.yml b/changelogs/unreleased/224509-chevron-down-svg-issuable.yml
new file mode 100644
index 00000000000..accc855aac4
--- /dev/null
+++ b/changelogs/unreleased/224509-chevron-down-svg-issuable.yml
@@ -0,0 +1,5 @@
+---
+title: Update icons to svg for issuable pages
+merge_request: 47596
+author:
+type: other
diff --git a/changelogs/unreleased/267568-user-admin-approval-default-for-new-instances.yml b/changelogs/unreleased/267568-user-admin-approval-default-for-new-instances.yml
new file mode 100644
index 00000000000..b6512ee661f
--- /dev/null
+++ b/changelogs/unreleased/267568-user-admin-approval-default-for-new-instances.yml
@@ -0,0 +1,5 @@
+---
+title: Admin approval required on user registration by default
+merge_request: 46937
+author:
+type: changed
diff --git a/changelogs/unreleased/add-get-invitations-for-group-and-project.yml b/changelogs/unreleased/add-get-invitations-for-group-and-project.yml
new file mode 100644
index 00000000000..6a8d3939d69
--- /dev/null
+++ b/changelogs/unreleased/add-get-invitations-for-group-and-project.yml
@@ -0,0 +1,5 @@
+---
+title: Add API get /invitations for project and group
+merge_request: 46046
+author:
+type: added
diff --git a/config/application.rb b/config/application.rb
index 22259b95cdc..3493c76899a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -202,6 +202,7 @@ module Gitlab
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "lazy_bundles/cropper.css"
+ config.assets.precompile << "lazy_bundles/select2.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "disable_animations.css"
config.assets.precompile << "snippets.css"
diff --git a/db/migrate/20201107032257_add_default_true_require_admin_approval_after_user_signup_to_application_settings.rb b/db/migrate/20201107032257_add_default_true_require_admin_approval_after_user_signup_to_application_settings.rb
new file mode 100644
index 00000000000..196f7bd8359
--- /dev/null
+++ b/db/migrate/20201107032257_add_default_true_require_admin_approval_after_user_signup_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDefaultTrueRequireAdminApprovalAfterUserSignupToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ change_column_default :application_settings, :require_admin_approval_after_user_signup, from: false, to: true
+ end
+end
diff --git a/db/schema_migrations/20201107032257 b/db/schema_migrations/20201107032257
new file mode 100644
index 00000000000..4ea5f763dc0
--- /dev/null
+++ b/db/schema_migrations/20201107032257
@@ -0,0 +1 @@
+07160ee3c92e68273042df979640c3927abbb187f79e1a4645471e28061e1c2c
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 44a00c8f7aa..81c84155bd3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9324,7 +9324,7 @@ CREATE TABLE application_settings (
gitpod_enabled boolean DEFAULT false NOT NULL,
gitpod_url text DEFAULT 'https://gitpod.io/'::text,
abuse_notification_email character varying,
- require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL,
+ require_admin_approval_after_user_signup boolean DEFAULT true NOT NULL,
help_page_documentation_base_url text,
automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL,
encrypted_ci_jwt_signing_key text,
diff --git a/doc/administration/sidekiq.md b/doc/administration/sidekiq.md
index a0cedf37e84..2cc6acc8c87 100644
--- a/doc/administration/sidekiq.md
+++ b/doc/administration/sidekiq.md
@@ -113,7 +113,6 @@ prometheus['enable'] = false
gitlab_rails['auto_migrate'] = false
alertmanager['enable'] = false
gitaly['enable'] = false
-gitlab_monitor['enable'] = false
gitlab_workhorse['enable'] = false
nginx['enable'] = false
postgres_exporter['enable'] = false
diff --git a/doc/api/invitations.md b/doc/api/invitations.md
index ecdc58f3e03..6fd2b26d80f 100644
--- a/doc/api/invitations.md
+++ b/doc/api/invitations.md
@@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Invitations API
-Use the Invitations API to send email to users you want to join a group or project.
+Use the Invitations API to send email to users you want to join a group or project, and to list pending
+invitations.
## Valid access levels
@@ -64,3 +65,43 @@ When there was any error sending the email:
}
}
```
+
+## List all invitations pending for a group or project
+
+Gets a list of invited group or project members viewable by the authenticated user.
+Returns invitations to direct members only, and not through inherited ancestors' groups.
+
+This function takes pagination parameters `page` and `per_page` to restrict the list of users.
+
+```plaintext
+GET /groups/:id/invitations
+GET /projects/:id/invitations
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `page` | integer | no | Page to retrieve |
+| `per_page`| integer | no | Number of member invitations to return per page |
+| `query` | string | no | A query string to search for invited members by invite email. Query text must match email address exactly. When empty, returns all invitations. |
+
+```shell
+curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/:id/invitations?query=member@example.org"
+curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/:id/invitations?query=member@example.org"
+```
+
+Example response:
+
+```json
+ [
+ {
+ "id": 1,
+ "invite_email": "member@example.org",
+ "invited_at": "2020-10-22T14:13:35Z",
+ "access_level": 30,
+ "expires_at": "2020-11-22T14:13:35Z",
+ "user_name": "Raymond Smith",
+ "created_by_name": "Administrator"
+ },
+]
+```
diff --git a/doc/development/documentation/restful_api_styleguide.md b/doc/development/documentation/restful_api_styleguide.md
index 3aa63abe937..13c6140114f 100644
--- a/doc/development/documentation/restful_api_styleguide.md
+++ b/doc/development/documentation/restful_api_styleguide.md
@@ -38,12 +38,16 @@ The following can be used as a template to get started:
````markdown
## Descriptive title
+> Version history note.
+
One or two sentence description of what endpoint does.
```plaintext
METHOD /endpoint
```
+Supported attributes:
+
| Attribute | Type | Required | Description |
|:------------|:---------|:---------|:----------------------|
| `attribute` | datatype | yes/no | Detailed description. |
@@ -65,6 +69,9 @@ Example response:
```
````
+Adjust the [version history note accordingly](styleguide/index.md#version-text-in-the-version-history)
+to describe the GitLab release that introduced the API call.
+
## Method description
Use the following table headers to describe the methods. Attributes should
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 128b81d4e33..6bc498636df 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -115,8 +115,36 @@ browser's developer console while on any page within GitLab.
import initMyWidget from './my_widget';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
+ waitForCSSLoaded(initMyWidget);
+ ```
+
+ Note that `waitForCSSLoaded()` methods supports receiving the action in different ways:
+
+ - With a callback:
+
+ ```javascript
+ waitForCSSLoaded(action)
+ ```
+
+ - With `then()`:
+
+ ```javascript
+ waitForCSSLoaded().then(action);
+ ```
+
+ - With `await` followed by `action`:
+
+ ```javascript
+ await waitForCSSLoaded;
+ action();
+ ```
+
+ For example, see how we use this in [app/assets/javascripts/pages/projects/graphs/charts/index.js](https://gitlab.com/gitlab-org/gitlab/-/commit/5e90885d6afd4497002df55bf015b338efcfc3c5#02e81de37f5b1716a3ef3222fa7f7edf22c40969_9_8):
+
+ ```javascript
waitForCSSLoaded(() => {
- initMyWidget();
+ const languagesContainer = document.getElementById('js-languages-chart');
+ //...
});
```
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 578c364052e..bf6833e1ad6 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -188,6 +188,30 @@ Check this [page](vuex.md) for more details.
- It is acceptable for Vue to listen to existing jQuery events using jQuery event listeners.
- It is not recommended to add new jQuery events for Vue to interact with jQuery.
+### Mixing Vue and JavaScript classes (in the data function)
+
+In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows:
+
+> The data object for the Vue instance. Vue will recursively convert its properties into getter/setters to make it “reactive”. The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior.
+
+Based on the Vue guidance:
+
+- **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data), such as `user: new User()`.
+- **Do not** add new JavaScript class implementations.
+- **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if cannot use simple primitives or objects.
+- **Do** maintain existing implementations using such approaches.
+- **Do** Migrate components to a pure object model when there are substantial changes to it.
+- **Do** add business logic to helpers or utils, so you can test them separately from your component.
+
+#### Why
+
+There are additional reasons why having a JavaScript class presents maintainability issues on a huge codebase:
+
+- Once a class is created, it is easy to extend it in a way that can infringe Vue reactivity and best practices.
+- A class adds a layer of abstraction, which makes the component API and its inner workings less clear.
+- It makes it harder to test. Since the class is instantiated by the component data function, it is harder to 'manage' component and class separately.
+- Adding OOP to a functional codebase adds yet another way of writing code, reducing consistency and clarity.
+
## Style guide
Please refer to the Vue section of our [style guide](style/vue.md)
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 7bf05f00552..5ea034ff4d2 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -27,9 +27,10 @@ To disable sign ups:
## Require administrator approval for new sign ups
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4491) in GitLab 13.5.
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4491) in GitLab 13.5.
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/267568) in GitLab 13.6.
-When this setting is enabled, any user visiting your GitLab domain and signing up for a new account must be explicitly [approved](../approving_users.md#approving-a-user) by an administrator before they can start using their account. This setting is only applicable if sign ups are enabled.
+When this setting is enabled, any user visiting your GitLab domain and signing up for a new account must be explicitly [approved](../approving_users.md#approving-a-user) by an administrator before they can start using their account. This setting is enabled by default for newly created instances. This setting is only applicable if sign ups are enabled.
To require administrator approval for new sign ups:
diff --git a/lib/api/entities/invitation.rb b/lib/api/entities/invitation.rb
index 6698326bd6c..342f4804cf3 100644
--- a/lib/api/entities/invitation.rb
+++ b/lib/api/entities/invitation.rb
@@ -8,7 +8,8 @@ module API
expose :expires_at
expose :invite_email
expose :invite_token
- expose :user_id
+ expose :user_name, if: -> (member, _) { member.user.present? }
+ expose :created_by_name
end
end
end
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index cdc0893a2e7..431001c227d 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -27,6 +27,13 @@ module API
members
end
+ def retrieve_member_invitations(source, query = nil)
+ members = source_members(source).where.not(invite_token: nil)
+ members = members.includes(:user)
+ members = members.where(invite_email: query) if query.present?
+ members
+ end
+
def source_members(source)
source.members
end
@@ -52,6 +59,10 @@ module API
def present_members(members)
present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info]
end
+
+ def present_member_invitations(invitations)
+ present invitations, with: Entities::Invitation, current_user: current_user
+ end
end
end
end
diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb
index 4485278286b..be8147908e9 100644
--- a/lib/api/invitations.rb
+++ b/lib/api/invitations.rb
@@ -2,6 +2,8 @@
module API
class Invitations < ::API::Base
+ include PaginationParams
+
feature_category :users
before { authenticate! }
@@ -29,6 +31,23 @@ module API
::Members::InviteService.new(current_user, params).execute(source)
end
+
+ desc 'Get a list of group or project invitations viewable by the authenticated user' do
+ detail 'This feature was introduced in GitLab 13.6'
+ success Entities::Invitation
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ use :pagination
+ end
+ get ":id/invitations" do
+ source = find_source(source_type, params[:id])
+ query = params[:query]
+
+ invitations = paginate(retrieve_member_invitations(source, query))
+
+ present_member_invitations invitations
+ end
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 2b08d3c63bb..d0579a44219 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -16,8 +16,8 @@ module Gitlab
@in_memory_application_settings = nil
end
- def method_missing(name, *args, &block)
- current_application_settings.send(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(name, *args, **kwargs, &block)
+ current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(name, include_private = false)
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 308491c36d0..2d41ad76618 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -28,6 +28,7 @@ module Gitlab
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
+ gon.select2_css_path = ActionController::Base.helpers.stylesheet_path('lazy_bundles/select2.css')
gon.test_env = Rails.env.test?
gon.disable_animations = Gitlab.config.gitlab['disable_animations']
gon.suggested_label_colors = LabelsHelper.suggested_colors
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cf92cee430c..b156ae50d96 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5725,6 +5725,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed."
+msgstr ""
+
msgid "ClusterIntegration|%{externalIp}.nip.io"
msgstr ""
@@ -11172,6 +11175,9 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export commit custody report"
+msgstr ""
+
msgid "Export group"
msgstr ""
@@ -14621,6 +14627,9 @@ msgstr ""
msgid "Integrations|All details"
msgstr ""
+msgid "Integrations|All projects inheriting these settings will also be reset."
+msgstr ""
+
msgid "Integrations|Comment detail:"
msgstr ""
@@ -14654,9 +14663,18 @@ msgstr ""
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
msgstr ""
+msgid "Integrations|Projects using custom settings will not be affected."
+msgstr ""
+
msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use parent level defaults."
msgstr ""
+msgid "Integrations|Reset integration?"
+msgstr ""
+
+msgid "Integrations|Resetting this integration will clear the settings and deactivate this integration."
+msgstr ""
+
msgid "Integrations|Return to GitLab for Jira"
msgstr ""
@@ -14768,6 +14786,9 @@ msgstr ""
msgid "Invalid file."
msgstr ""
+msgid "Invalid hash"
+msgstr ""
+
msgid "Invalid import params"
msgstr ""
@@ -16842,6 +16863,9 @@ msgstr ""
msgid "Merge automatically (%{strategy})"
msgstr ""
+msgid "Merge commit SHA"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -23062,6 +23086,9 @@ msgstr ""
msgid "Resend it"
msgstr ""
+msgid "Reset"
+msgstr ""
+
msgid "Reset authorization key"
msgstr ""
diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb
index b394cd485cb..f8e85798012 100644
--- a/qa/qa/page/main/sign_up.rb
+++ b/qa/qa/page/main/sign_up.rb
@@ -18,15 +18,13 @@ module QA
end
def sign_up!(user)
- fill_element :new_user_first_name_field, user.first_name
- fill_element :new_user_last_name_field, user.last_name
- fill_element :new_user_username_field, user.username
- fill_element :new_user_email_field, user.email
- fill_element :new_user_password_field, user.password
-
- signed_in = retry_until do
+ signed_in = retry_until(raise_on_failure: false) do
+ fill_element :new_user_first_name_field, user.first_name
+ fill_element :new_user_last_name_field, user.last_name
+ fill_element :new_user_username_field, user.username
+ fill_element :new_user_email_field, user.email
+ fill_element :new_user_password_field, user.password
click_element :new_user_register_button if has_element?(:new_user_register_button)
-
click_element :get_started_button if has_element?(:get_started_button)
Page::Main::Menu.perform(&:has_personal_area?)
diff --git a/qa/qa/runtime/application_settings.rb b/qa/qa/runtime/application_settings.rb
index c78f4258721..428ed20c83f 100644
--- a/qa/qa/runtime/application_settings.rb
+++ b/qa/qa/runtime/application_settings.rb
@@ -33,7 +33,7 @@ module QA
def api_client
@api_client ||= Runtime::API::Client.as_admin
- rescue AuthorizationError => e
+ rescue API::Client::AuthorizationError => e
raise "Administrator access is required to set application settings. #{e.message}"
end
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index ddaf35a2d65..4c4dd416093 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -8,7 +8,7 @@ module QA
module Env
extend self
- attr_writer :personal_access_token, :ldap_username, :ldap_password
+ attr_writer :personal_access_token
ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES
@@ -293,6 +293,11 @@ module QA
@ldap_username ||= ENV['GITLAB_LDAP_USERNAME']
end
+ def ldap_username=(ldap_username)
+ @ldap_username = ldap_username # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ ENV['GITLAB_LDAP_USERNAME'] = ldap_username
+ end
+
def ldap_password
@ldap_password ||= ENV['GITLAB_LDAP_PASSWORD']
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 11a6bf6fdfa..2bb03b6154f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -13,11 +13,39 @@ module QA
end
end
- RSpec.describe 'Manage', :skip_signup_disabled do
- describe 'standard', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/936' do
+ RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin do
+ describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/935' do
+ before do
+ # When LDAP is enabled, a previous test might have created a token for the LDAP 'tanuki' user who is not an admin
+ # So we need to set it to nil in order to create a new token for admin user so that we are able to set_application_settings
+ # Also, when GITLAB_LDAP_USERNAME is provided, it is used to create a token. This also needs to be set to nil temporarily
+ # for the same reason as above.
+
+ @personal_access_token = Runtime::Env.personal_access_token
+ Runtime::Env.personal_access_token = nil
+ ldap_username = Runtime::Env.ldap_username
+ Runtime::Env.ldap_username = nil
+
+ disable_require_admin_approval_after_user_signup
+
+ Runtime::Env.ldap_username = ldap_username
+ end
+
it_behaves_like 'registration and login'
- context 'when user account is deleted', :requires_admin do
+ after do
+ Runtime::Env.personal_access_token = @personal_access_token
+ end
+ end
+
+ describe 'standard', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/936' do
+ before(:all) do
+ disable_require_admin_approval_after_user_signup
+ end
+
+ it_behaves_like 'registration and login'
+
+ context 'when user account is deleted' do
let(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = admin_api_client
@@ -61,11 +89,10 @@ module QA
end
end
end
- end
- RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do
- describe 'while LDAP is enabled', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/935' do
- it_behaves_like 'registration and login'
+ def disable_require_admin_approval_after_user_signup
+ Runtime::ApplicationSettings.set_application_settings(require_admin_approval_after_user_signup: false)
+ sleep 10 # It takes a moment for the setting to come into effect
end
end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index fdcea33b0be..2fb17e56f37 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe RegistrationsController do
before do
stub_feature_flags(invisible_captcha: false)
+ stub_application_setting(require_admin_approval_after_user_signup: false)
end
describe '#new' do
@@ -76,10 +77,6 @@ RSpec.describe RegistrationsController do
end
context 'when the `require_admin_approval_after_user_signup` setting is turned off' do
- before do
- stub_application_setting(require_admin_approval_after_user_signup: false)
- end
-
it 'signs up the user in `active` state' do
subject
created_user = User.find_by(email: 'new@user.com')
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index d1520a5a53a..2ceffa896eb 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
let(:group_invite) { group.group_members.invite.last }
before do
+ stub_application_setting(require_admin_approval_after_user_signup: false)
project.add_maintainer(owner)
group.add_owner(owner)
group.add_developer('user@example.com', owner)
@@ -97,6 +98,21 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
click_link 'Register now'
end
+ context 'with admin appoval required enabled' do
+ before do
+ stub_application_setting(require_admin_approval_after_user_signup: true)
+ end
+
+ let(:send_email_confirmation) { true }
+
+ it 'does not sign the user in' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(new_user_session_path)
+ expect(page).to have_content('You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator')
+ end
+ end
+
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index 28fe0a0b7e1..0ed9e23c7f8 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe 'Projects > Snippets > Create Snippet', :js do
click_button('Create snippet')
wait_for_requests
- link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
+ link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 1eb3b016152..589cc9f9b02 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe 'User uploads file to note' do
click_button 'Comment'
wait_for_requests
- expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+ expect(find('a.no-attachment-icon img.js-lazy-loaded[alt="dk"]')['src'])
.to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index bb2b465a69a..bfdd1e1bdb7 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -43,6 +43,10 @@ end
RSpec.describe 'Signup' do
include TermsHelper
+ before do
+ stub_application_setting(require_admin_approval_after_user_signup: false)
+ end
+
let(:new_user) { build_stubbed(:user) }
def fill_in_signup_form
@@ -228,6 +232,22 @@ RSpec.describe 'Signup' do
expect(current_path).to eq users_sign_up_welcome_path
end
end
+
+ context 'with required admin approval enabled' do
+ before do
+ stub_application_setting(require_admin_approval_after_user_signup: true)
+ end
+
+ it 'creates the user but does not sign them in' do
+ visit new_user_registration_path
+
+ fill_in_signup_form
+
+ expect { click_button 'Register' }.to change { User.count }.by(1)
+ expect(current_path).to eq new_user_session_path
+ expect(page).to have_content("You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator")
+ end
+ end
end
context 'with errors' do
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 4dd38c959f8..97e77ac87ab 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -5,6 +5,7 @@ import IntegrationForm from '~/integrations/edit/components/integration_form.vue
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
+import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -44,6 +45,8 @@ describe('IntegrationForm', () => {
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
const findConfirmationModal = () => wrapper.find(ConfirmationModal);
+ const findResetConfirmationModal = () => wrapper.find(ResetConfirmationModal);
+ const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
const findTriggerFields = () => wrapper.find(TriggerFields);
@@ -75,6 +78,29 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(true);
});
+
+ describe('resetPath is empty', () => {
+ it('does not render ResetConfirmationModal and button', () => {
+ createComponent({
+ integrationLevel: integrationLevels.INSTANCE,
+ });
+
+ expect(findResetButton().exists()).toBe(false);
+ expect(findResetConfirmationModal().exists()).toBe(false);
+ });
+ });
+
+ describe('resetPath is present', () => {
+ it('renders ResetConfirmationModal and button', () => {
+ createComponent({
+ integrationLevel: integrationLevels.INSTANCE,
+ resetPath: 'resetPath',
+ });
+
+ expect(findResetButton().exists()).toBe(true);
+ expect(findResetConfirmationModal().exists()).toBe(true);
+ });
+ });
});
describe('integrationLevel is group', () => {
@@ -85,6 +111,29 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(true);
});
+
+ describe('resetPath is empty', () => {
+ it('does not render ResetConfirmationModal and button', () => {
+ createComponent({
+ integrationLevel: integrationLevels.GROUP,
+ });
+
+ expect(findResetButton().exists()).toBe(false);
+ expect(findResetConfirmationModal().exists()).toBe(false);
+ });
+ });
+
+ describe('resetPath is present', () => {
+ it('renders ResetConfirmationModal and button', () => {
+ createComponent({
+ integrationLevel: integrationLevels.GROUP,
+ resetPath: 'resetPath',
+ });
+
+ expect(findResetButton().exists()).toBe(true);
+ expect(findResetConfirmationModal().exists()).toBe(true);
+ });
+ });
});
describe('integrationLevel is project', () => {
@@ -95,6 +144,16 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(false);
});
+
+ it('does not render ResetConfirmationModal and button', () => {
+ createComponent({
+ integrationLevel: 'project',
+ resetPath: 'resetPath',
+ });
+
+ expect(findResetButton().exists()).toBe(false);
+ expect(findResetConfirmationModal().exists()).toBe(false);
+ });
});
describe('type is "slack"', () => {
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index 5356c0a411b..5b5c8d6f76e 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -1,6 +1,11 @@
import testAction from 'helpers/vuex_action_helper';
import createState from '~/integrations/edit/store/state';
-import { setOverride } from '~/integrations/edit/store/actions';
+import {
+ setOverride,
+ setIsSaving,
+ setIsTesting,
+ setIsResetting,
+} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
describe('Integration form store actions', () => {
@@ -15,4 +20,24 @@ describe('Integration form store actions', () => {
return testAction(setOverride, true, state, [{ type: types.SET_OVERRIDE, payload: true }]);
});
});
+
+ describe('setIsSaving', () => {
+ it('should commit isSaving mutation', () => {
+ return testAction(setIsSaving, true, state, [{ type: types.SET_IS_SAVING, payload: true }]);
+ });
+ });
+
+ describe('setIsTesting', () => {
+ it('should commit isTesting mutation', () => {
+ return testAction(setIsTesting, true, state, [{ type: types.SET_IS_TESTING, payload: true }]);
+ });
+ });
+
+ describe('setIsResetting', () => {
+ it('should commit isResetting mutation', () => {
+ return testAction(setIsResetting, true, state, [
+ { type: types.SET_IS_RESETTING, payload: true },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/getters_spec.js b/spec/frontend/integrations/edit/store/getters_spec.js
index 3353e0c84cc..7d4532a1059 100644
--- a/spec/frontend/integrations/edit/store/getters_spec.js
+++ b/spec/frontend/integrations/edit/store/getters_spec.js
@@ -1,5 +1,12 @@
-import { currentKey, isInheriting, propsSource } from '~/integrations/edit/store/getters';
+import {
+ currentKey,
+ isInheriting,
+ isDisabled,
+ propsSource,
+} from '~/integrations/edit/store/getters';
import createState from '~/integrations/edit/store/state';
+import mutations from '~/integrations/edit/store/mutations';
+import * as types from '~/integrations/edit/store/mutation_types';
import { mockIntegrationProps } from '../mock_data';
describe('Integration form store getters', () => {
@@ -45,6 +52,29 @@ describe('Integration form store getters', () => {
});
});
+ describe('isDisabled', () => {
+ it.each`
+ isSaving | isTesting | isResetting | expected
+ ${false} | ${false} | ${false} | ${false}
+ ${true} | ${false} | ${false} | ${true}
+ ${false} | ${true} | ${false} | ${true}
+ ${false} | ${false} | ${true} | ${true}
+ ${false} | ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true} | ${true}
+ ${true} | ${true} | ${false} | ${true}
+ ${true} | ${true} | ${true} | ${true}
+ `(
+ 'when isSaving = $isSaving, isTesting = $isTesting, isResetting = $isResetting then isDisabled = $expected',
+ ({ isSaving, isTesting, isResetting, expected }) => {
+ mutations[types.SET_IS_SAVING](state, isSaving);
+ mutations[types.SET_IS_TESTING](state, isTesting);
+ mutations[types.SET_IS_RESETTING](state, isResetting);
+
+ expect(isDisabled(state)).toBe(expected);
+ },
+ );
+ });
+
describe('propsSource', () => {
beforeEach(() => {
state.defaultState = defaultState;
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
index 4b733726d44..4707b4b3714 100644
--- a/spec/frontend/integrations/edit/store/mutations_spec.js
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -16,4 +16,28 @@ describe('Integration form store mutations', () => {
expect(state.override).toBe(true);
});
});
+
+ describe(`${types.SET_IS_SAVING}`, () => {
+ it('sets isSaving', () => {
+ mutations[types.SET_IS_SAVING](state, true);
+
+ expect(state.isSaving).toBe(true);
+ });
+ });
+
+ describe(`${types.SET_IS_TESTING}`, () => {
+ it('sets isTesting', () => {
+ mutations[types.SET_IS_TESTING](state, true);
+
+ expect(state.isTesting).toBe(true);
+ });
+ });
+
+ describe(`${types.SET_IS_RESETTING}`, () => {
+ it('sets isResetting', () => {
+ mutations[types.SET_IS_RESETTING](state, true);
+
+ expect(state.isResetting).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index fc193850a94..4d0f4a1da71 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -7,6 +7,7 @@ describe('Integration form state factory', () => {
customState: {},
isSaving: false,
isTesting: false,
+ isResetting: false,
override: false,
});
});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 6fef5f6b63c..d7cedb939d2 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -325,4 +325,19 @@ describe('text_utility', () => {
expect(textUtils.hasContent(txt)).toEqual(result);
});
});
+
+ describe('isValidSha1Hash', () => {
+ const validSha1Hash = '92d10c15';
+ const stringOver40 = new Array(42).join('a');
+
+ it.each`
+ hash | valid
+ ${validSha1Hash} | ${true}
+ ${'__characters'} | ${false}
+ ${'abc'} | ${false}
+ ${stringOver40} | ${false}
+ `(`returns $valid for $hash`, ({ hash, valid }) => {
+ expect(textUtils.isValidSha1Hash(hash)).toBe(valid);
+ });
+ });
});
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 9befd4f533a..75586970abb 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe API::Invitations do
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
- let(:email) { 'email@example.org' }
+ let(:email) { 'email1@example.com' }
+ let(:email2) { 'email2@example.com' }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
@@ -75,7 +76,7 @@ RSpec.describe API::Invitations do
it 'invites a list of new email addresses' do
expect do
- email_list = 'email1@example.com,email2@example.com'
+ email_list = [email, email2].join(',')
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
@@ -204,4 +205,97 @@ RSpec.describe API::Invitations do
let(:source) { group }
end
end
+
+ shared_examples 'GET /:source_type/:id/invitations' do |source_type|
+ context "with :source_type == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get invitations_url(source, stranger) }
+ end
+
+ %i[maintainer developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+
+ get invitations_url(source, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(0)
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get invitations_url(source, maintainer)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get invitations_url(source, maintainer)
+ end
+
+ invite_member_by_email(source, source_type, email, maintainer)
+
+ expect do
+ get invitations_url(source, maintainer)
+ end.not_to exceed_query_limit(control)
+ end
+
+ it 'does not find confirmed members' do
+ get invitations_url(source, developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(0)
+ expect(json_response.map { |u| u['id'] }).not_to match_array [maintainer.id, developer.id]
+ end
+
+ it 'finds all members with no query string specified' do
+ invite_member_by_email(source, source_type, email, developer)
+ invite_member_by_email(source, source_type, email2, developer)
+
+ get invitations_url(source, developer), params: { query: '' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(2)
+ expect(json_response.map { |u| u['invite_email'] }).to match_array [email, email2]
+ end
+
+ it 'finds the invitation by invite_email with query string' do
+ invite_member_by_email(source, source_type, email, developer)
+ invite_member_by_email(source, source_type, email2, developer)
+
+ get invitations_url(source, developer), params: { query: email }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['invite_email']).to eq(email)
+ expect(json_response.first['created_by_name']).to eq(developer.name)
+ expect(json_response.first['user_name']).to eq(nil)
+ end
+
+ def invite_member_by_email(source, source_type, email, created_by)
+ create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/invitations' do
+ it_behaves_like 'GET /:source_type/:id/invitations', 'project' do
+ let(:source) { project }
+ end
+ end
+
+ describe 'GET /groups/:id/invitations' do
+ it_behaves_like 'GET /:source_type/:id/invitations', 'group' do
+ let(:source) { group }
+ end
+ end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index e03c0d37f3d..da6eaf0ae23 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['spam_check_endpoint_enabled']).to be_falsey
expect(json_response['spam_check_endpoint_url']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
- expect(json_response['require_admin_approval_after_user_signup']).to eq(false)
+ expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
end
end
diff --git a/tooling/README.md b/tooling/README.md
new file mode 100644
index 00000000000..29706a5af14
--- /dev/null
+++ b/tooling/README.md
@@ -0,0 +1,4 @@
+# Tooling
+
+This directory contains tools and configuration for development only.
+
diff --git a/tooling/eslint-config/conditionally_ignore_ee.js b/tooling/eslint-config/conditionally_ignore_ee.js
new file mode 100644
index 00000000000..e5e3c8013f4
--- /dev/null
+++ b/tooling/eslint-config/conditionally_ignore_ee.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/no-commonjs */
+
+const IS_EE = require('../../config/helpers/is_ee_env');
+
+module.exports = IS_EE ? {} : { ignorePatterns: ['ee/**/*.*'] };