diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-14 03:14:57 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-14 03:14:57 +0300 |
commit | 1d54f384d509d9581730e24a64561e94132e41c1 (patch) | |
tree | b53346e16f2129de0872f9e1cc28bb468fffee04 /app | |
parent | f163fc8ce6d7661ccf0ff9aa4561f6e5a708b71b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 227 insertions, 122 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index c22f532d7ac..edc6573a489 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -140,6 +140,7 @@ export default { class="header-search gl-relative" > <gl-search-box-by-type + id="search" v-model="searchText" role="searchbox" class="gl-z-index-1" diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index 74d99d02fc5..177e3d6f9cc 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -1,6 +1,5 @@ import { s__, __ } from '~/locale'; -export const TEST_INTEGRATION_EVENT = 'testIntegration'; export const SAVE_INTEGRATION_EVENT = 'saveIntegration'; export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm'; diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index a30c84bd4d2..60a6d13ce69 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -1,14 +1,18 @@ <script> import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { mapState, mapActions, mapGetters } from 'vuex'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { - TEST_INTEGRATION_EVENT, SAVE_INTEGRATION_EVENT, + VALIDATE_INTEGRATION_FORM_EVENT, + I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE, + I18N_DEFAULT_ERROR_MESSAGE, + I18N_SUCCESSFUL_CONNECTION_MESSAGE, integrationLevels, } from '~/integrations/constants'; import eventHub from '../event_hub'; - +import { testIntegrationSettings } from '../api'; import ActiveCheckbox from './active_checkbox.vue'; import ConfirmationModal from './confirmation_modal.vue'; import DynamicField from './dynamic_field.vue'; @@ -50,18 +54,12 @@ export default { data() { return { integrationActive: false, + testingLoading: false, }; }, computed: { ...mapGetters(['currentKey', 'propsSource', 'isDisabled']), - ...mapState([ - 'defaultState', - 'customState', - 'override', - 'isSaving', - 'isTesting', - 'isResetting', - ]), + ...mapState(['defaultState', 'customState', 'override', 'isSaving', 'isResetting']), isEditable() { return this.propsSource.editable; }, @@ -74,9 +72,18 @@ export default { this.customState.integrationLevel === integrationLevels.GROUP ); }, - showReset() { + showResetButton() { return this.isInstanceOrGroupLevel && this.propsSource.resetPath; }, + showTestButton() { + return this.propsSource.canTest; + }, + disableSaveButton() { + return Boolean(this.isResetting || this.testingLoading); + }, + disableResetButton() { + return Boolean(this.isSaving || this.testingLoading); + }, }, mounted() { // this form element is defined in Haml @@ -86,7 +93,6 @@ export default { ...mapActions([ 'setOverride', 'setIsSaving', - 'setIsTesting', 'setIsResetting', 'fetchResetIntegration', 'requestJiraIssueTypes', @@ -98,17 +104,39 @@ export default { eventHub.$emit(SAVE_INTEGRATION_EVENT, formValid); }, onTestClick() { - this.setIsTesting(true); + this.testingLoading = true; - const formValid = this.form.checkValidity(); - eventHub.$emit(TEST_INTEGRATION_EVENT, formValid); + if (!this.form.checkValidity()) { + eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); + return; + } + + testIntegrationSettings(this.propsSource.testPath, this.getFormData()) + .then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => { + if (error) { + eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); + this.$toast.show(message); + return; + } + + this.$toast.show(I18N_SUCCESSFUL_CONNECTION_MESSAGE); + }) + .catch((error) => { + this.$toast.show(I18N_DEFAULT_ERROR_MESSAGE); + Sentry.captureException(error); + }) + .finally(() => { + this.testingLoading = false; + }); }, onResetClick() { this.fetchResetIntegration(); }, onRequestJiraIssueTypes() { - const formData = new FormData(this.form); - this.requestJiraIssueTypes(formData); + this.requestJiraIssueTypes(this.getFormData()); + }, + getFormData() { + return new FormData(this.form); }, onToggleIntegrationState(integrationActive) { this.integrationActive = integrationActive; @@ -183,7 +211,7 @@ export default { category="primary" variant="confirm" :loading="isSaving" - :disabled="isDisabled" + :disabled="disableSaveButton" data-qa-selector="save_changes_button" > {{ __('Save changes') }} @@ -196,7 +224,7 @@ export default { variant="confirm" type="submit" :loading="isSaving" - :disabled="isDisabled" + :disabled="disableSaveButton" data-testid="save-button" data-qa-selector="save_changes_button" @click.prevent="onSaveClick" @@ -205,25 +233,24 @@ export default { </gl-button> <gl-button - v-if="propsSource.canTest" + v-if="showTestButton" category="secondary" variant="confirm" - :loading="isTesting" + :loading="testingLoading" :disabled="isDisabled" - :href="propsSource.testPath" data-testid="test-button" @click.prevent="onTestClick" > {{ __('Test settings') }} </gl-button> - <template v-if="showReset"> + <template v-if="showResetButton"> <gl-button v-gl-modal.confirmResetIntegration category="secondary" variant="confirm" :loading="isResetting" - :disabled="isDisabled" + :disabled="disableResetButton" data-testid="reset-button" > {{ __('Reset') }} diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js index b81ae1b1cb6..6c1bfaa7858 100644 --- a/app/assets/javascripts/integrations/edit/store/actions.js +++ b/app/assets/javascripts/integrations/edit/store/actions.js @@ -11,7 +11,6 @@ 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 39e14de2d0d..10d370de856 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 isDisabled = (state) => state.isSaving || state.isTesting || state.isResetting; +export const isDisabled = (state) => state.isSaving || 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 c681056a515..dd4a79f6fe0 100644 --- a/app/assets/javascripts/integrations/edit/store/mutation_types.js +++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js @@ -1,6 +1,5 @@ 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'; export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES'; diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js index 279df1b9266..f76081e62f0 100644 --- a/app/assets/javascripts/integrations/edit/store/mutations.js +++ b/app/assets/javascripts/integrations/edit/store/mutations.js @@ -7,9 +7,6 @@ export default { [types.SET_IS_SAVING](state, isSaving) { state.isSaving = isSaving; }, - [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 1c0b274e4ef..3d40d1b90d5 100644 --- a/app/assets/javascripts/integrations/edit/store/state.js +++ b/app/assets/javascripts/integrations/edit/store/state.js @@ -6,7 +6,6 @@ export default ({ defaultState = null, customState = {} } = {}) => { defaultState, customState, isSaving: false, - isTesting: false, isResetting: false, isLoadingJiraIssueTypes: false, loadingJiraIssueTypesErrorMessage: '', diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index d3c227df1ee..193bc1c4e9b 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,15 +1,7 @@ import { delay } from 'lodash'; -import toast from '~/vue_shared/plugins/global_toast'; import initForm from './edit'; import eventHub from './edit/event_hub'; -import { - TEST_INTEGRATION_EVENT, - SAVE_INTEGRATION_EVENT, - VALIDATE_INTEGRATION_FORM_EVENT, - I18N_DEFAULT_ERROR_MESSAGE, - I18N_SUCCESSFUL_CONNECTION_MESSAGE, -} from './constants'; -import { testIntegrationSettings } from './edit/api'; +import { SAVE_INTEGRATION_EVENT, VALIDATE_INTEGRATION_FORM_EVENT } from './constants'; export default class IntegrationSettingsForm { constructor(formSelector) { @@ -29,9 +21,6 @@ export default class IntegrationSettingsForm { document.querySelector('.js-vue-default-integration-settings'), this.formSelector, ); - eventHub.$on(TEST_INTEGRATION_EVENT, (formValid) => { - this.testIntegration(formValid); - }); eventHub.$on(SAVE_INTEGRATION_EVENT, (formValid) => { this.saveIntegration(formValid); }); @@ -53,47 +42,4 @@ export default class IntegrationSettingsForm { this.vue.$store.dispatch('setIsSaving', false); } } - - testIntegration(formValid) { - // Service was marked active so now we check; - // 1) If form contents are valid - // 2) If this service can be tested - // If both conditions are true, we override form submission - // and test the service using provided configuration. - if (formValid) { - this.testSettings(new FormData(this.$form)); - } else { - eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); - this.vue.$store.dispatch('setIsTesting', false); - } - } - - /** - * Get a list of Jira issue types for the currently configured project - * - * @param {string} formData - URL encoded string containing the form data - * - * @return {Promise} - */ - - /** - * Test Integration config - */ - testSettings(formData) { - return testIntegrationSettings(this.testEndPoint, formData) - .then(({ data }) => { - if (data.error) { - toast(`${data.message} ${data.service_response}`); - } else { - this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes); - toast(I18N_SUCCESSFUL_CONNECTION_MESSAGE); - } - }) - .catch(() => { - toast(I18N_DEFAULT_ERROR_MESSAGE); - }) - .finally(() => { - this.vue.$store.dispatch('setIsTesting', false); - }); - } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0d307d7744b..99e8058b57d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -14,7 +14,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { initRails } from '~/lib/utils/rails_ujs'; import * as popovers from '~/popovers'; import * as tooltips from '~/tooltips'; -import { initHeaderSearchApp } from '~/header_search'; import initAlertHandler from './alert_handler'; import { addDismissFlashClickListener } from './flash'; import initTodoToggle from './header'; @@ -100,24 +99,29 @@ function deferredInitialisation() { initFeatureHighlight(); initCopyCodeButton(); - if (gon.features?.newHeaderSearch) { - initHeaderSearchApp(); - } else { - const search = document.querySelector('#search'); - if (search) { - search.addEventListener( - 'focus', - () => { + const search = document.querySelector('#search'); + if (search) { + search.addEventListener( + 'focus', + () => { + if (gon.features?.newHeaderSearch) { + import(/* webpackChunkName: 'globalSearch' */ '~/header_search') + .then(async ({ initHeaderSearchApp }) => { + await initHeaderSearchApp(); + document.querySelector('#search').focus(); + }) + .catch(() => {}); + } else { import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete') .then(({ default: initSearchAutocomplete }) => { const searchDropdown = initSearchAutocomplete(); searchDropdown.onSearchInputFocus(); }) .catch(() => {}); - }, - { once: true }, - ); - } + } + }, + { once: true }, + ); } addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index f07fb9d926a..e3aa29d5f89 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -230,7 +230,7 @@ export default { <gl-button category="primary" type="submit" - variant="success" + variant="confirm" :disabled="updatePrevented" data-qa-selector="submit_button" data-testid="snippet-submit-btn" diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index a5c98a7ad90..9b24c8afe37 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -113,7 +113,7 @@ export default { href: this.snippet.project ? joinPaths(this.snippet.project.webUrl, '-/snippets/new') : joinPaths('/', gon.relative_url_root, '/-/snippets/new'), - variant: 'success', + variant: 'confirm', category: 'secondary', }, { diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 03dcbd2b8f9..345c180d164 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -376,6 +376,18 @@ } } } + + li > a.gl-link { + // undo gl-link text items for things in the sidebar - including sub menus + // defined in https://gitlab.com/gitlab-org/gitlab-ui/-/blob/5431e0ca5149d4e02e3d5d617d194ac9609bb82d/src/components/base/link/link.scss + @include gl-text-body; + + &:active, + &:focus, + &:focus:active { + @include gl-text-decoration-none; + } + } } .sidebar-sub-level-items { diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 9914e573247..63305bc592d 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -407,6 +407,34 @@ h1 { .gl-form-input.form-control::placeholder { color: #868686; } +.gl-icon { + fill: currentColor; +} +.gl-icon.s12 { + width: 12px; + height: 12px; +} +.gl-icon.s16 { + width: 16px; + height: 16px; +} +.gl-icon.s32 { + width: 32px; + height: 32px; +} +.gl-link { + font-size: 0.875rem; + color: #428fdc; +} +.gl-link:active { + color: #9dc7f1; +} +.gl-link:active { + text-decoration: underline; + box-shadow: 0 0 0 1px rgba(51, 51, 51, 0.4), + 0 0 0 4px rgba(66, 143, 220, 0.48); + outline: none; +} .gl-button { display: inline-flex; } @@ -439,6 +467,29 @@ h1 { outline: none; background-color: #404040; } +.gl-button.gl-button.btn-default:active .gl-icon, +.gl-button.gl-button.btn-default.active .gl-icon { + color: #fafafa; +} +.gl-button.gl-button.btn-default .gl-icon { + color: #999; +} +.gl-search-box-by-type-search-icon { + margin: 0.5rem; + color: #999; + width: 1rem; + position: absolute; +} +.gl-search-box-by-type { + display: flex; + position: relative; +} +.gl-search-box-by-type-input, +.gl-search-box-by-type-input.gl-form-input { + height: 2rem; + padding-right: 2rem; + padding-left: 1.75rem; +} body, .form-control, .search form { @@ -1334,6 +1385,12 @@ input { font-weight: 400; color: #9dc7f1; } +.sidebar-top-level-items li > a.gl-link { + color: #fafafa; +} +.sidebar-top-level-items li > a.gl-link:active { + text-decoration: none; +} .sidebar-sub-level-items { padding-top: 0; padding-bottom: 0; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index e6d7185e29c..a57202515ad 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -388,6 +388,34 @@ h1 { .gl-form-input.form-control::placeholder { color: #868686; } +.gl-icon { + fill: currentColor; +} +.gl-icon.s12 { + width: 12px; + height: 12px; +} +.gl-icon.s16 { + width: 16px; + height: 16px; +} +.gl-icon.s32 { + width: 32px; + height: 32px; +} +.gl-link { + font-size: 0.875rem; + color: #1f75cb; +} +.gl-link:active { + color: #0b5cad; +} +.gl-link:active { + text-decoration: underline; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.4), + 0 0 0 4px rgba(31, 117, 203, 0.48); + outline: none; +} .gl-button { display: inline-flex; } @@ -420,6 +448,29 @@ h1 { outline: none; background-color: #dbdbdb; } +.gl-button.gl-button.btn-default:active .gl-icon, +.gl-button.gl-button.btn-default.active .gl-icon { + color: #303030; +} +.gl-button.gl-button.btn-default .gl-icon { + color: #666; +} +.gl-search-box-by-type-search-icon { + margin: 0.5rem; + color: #666; + width: 1rem; + position: absolute; +} +.gl-search-box-by-type { + display: flex; + position: relative; +} +.gl-search-box-by-type-input, +.gl-search-box-by-type-input.gl-form-input { + height: 2rem; + padding-right: 2rem; + padding-left: 1.75rem; +} body, .form-control, .search form { @@ -1315,6 +1366,12 @@ input { font-weight: 400; color: #0b5cad; } +.sidebar-top-level-items li > a.gl-link { + color: #303030; +} +.sidebar-top-level-items li > a.gl-link:active { + text-decoration: none; +} .sidebar-sub-level-items { padding-top: 0; padding-bottom: 0; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index d2cc1f8640f..a8110d15d8e 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -291,18 +291,6 @@ $gl-line-height-42: px-to-rem(42px); @include gl-focus($gl-border-size-1, $gray-900, true); } -// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2476 -.gl-md-max-w-50p { - @include gl-media-breakpoint-up(md) { - max-width: 50%; - } -} - -// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2497 -.gl-z-index-200 { - z-index: 200; -} - // Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1637 .gl-lg-w-25p { @include gl-media-breakpoint-up(lg) { diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2daf098b94a..1d1d9b6bec7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -56,6 +56,8 @@ class IssuableBaseService < ::BaseProjectService # confidential attribute is a special type of metadata and needs to be allowed to be set # by non-members on issues in public projects so that security issues can be reported as confidential. params.delete(:confidential) unless can?(current_user, :set_confidentiality, issuable) + params.delete(:add_contacts) unless can?(current_user, :set_issue_crm_contacts, issuable) + params.delete(:remove_contacts) unless can?(current_user, :set_issue_crm_contacts, issuable) filter_assignees(issuable) filter_milestone @@ -206,6 +208,9 @@ class IssuableBaseService < ::BaseProjectService params[:assignee_ids] = process_assignee_ids(params, extra_assignee_ids: issuable.assignee_ids.to_a) end + params.delete(:remove_contacts) + add_crm_contact_emails = params.delete(:add_contacts) + issuable.assign_attributes(allowed_create_params(params)) before_create(issuable) @@ -219,6 +224,7 @@ class IssuableBaseService < ::BaseProjectService handle_changes(issuable, { params: params }) after_create(issuable) + set_crm_contacts(issuable, add_crm_contact_emails) execute_hooks(issuable) users_to_invalidate = issuable.allows_reviewers? ? issuable.assignees | issuable.reviewers : issuable.assignees @@ -229,6 +235,12 @@ class IssuableBaseService < ::BaseProjectService issuable end + def set_crm_contacts(issuable, add_crm_contact_emails, remove_crm_contact_emails = []) + return unless add_crm_contact_emails.present? || remove_crm_contact_emails.present? + + ::Issues::SetCrmContactsService.new(project: project, current_user: current_user, params: { add_emails: add_crm_contact_emails, remove_emails: remove_crm_contact_emails }).execute(issuable) + end + def before_create(issuable) # To be overridden by subclasses end @@ -254,6 +266,7 @@ class IssuableBaseService < ::BaseProjectService assign_requested_labels(issuable) assign_requested_assignees(issuable) + assign_requested_crm_contacts(issuable) if issuable.changed? || params.present? issuable.assign_attributes(allowed_update_params(params)) @@ -414,6 +427,12 @@ class IssuableBaseService < ::BaseProjectService issuable.touch end + def assign_requested_crm_contacts(issuable) + add_crm_contact_emails = params.delete(:add_contacts) + remove_crm_contact_emails = params.delete(:remove_contacts) + set_crm_contacts(issuable, add_crm_contact_emails, remove_crm_contact_emails) + end + def assign_requested_assignees(issuable) return if issuable.is_a?(Epic) diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb index f0d95934ea4..c435ab81b4d 100644 --- a/app/services/issues/set_crm_contacts_service.rb +++ b/app/services/issues/set_crm_contacts_service.rb @@ -12,7 +12,7 @@ module Issues return error_no_permissions unless allowed? return error_invalid_params unless valid_params? - @existing_ids = issue.issue_customer_relations_contacts.map(&:contact_id) + @existing_ids = issue.customer_relations_contact_ids determine_changes if params[:replace_ids].present? return error_too_many if too_many? @@ -24,6 +24,7 @@ module Issues if issue.valid? GraphqlTriggers.issue_crm_contacts_updated(issue) + issue.touch ServiceResponse.success(payload: issue) else # The default error isn't very helpful: "Issue customer relations contacts is invalid" diff --git a/app/views/groups/_invite_members_side_nav_link.html.haml b/app/views/groups/_invite_members_side_nav_link.html.haml index bccfa9897da..3046669b53b 100644 --- a/app/views/groups/_invite_members_side_nav_link.html.haml +++ b/app/views/groups/_invite_members_side_nav_link.html.haml @@ -1,5 +1,4 @@ .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'} } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 01dfecb585a..a7c08bafba6 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,7 +36,9 @@ 'issues-path' => issues_dashboard_path, 'mr-path' => merge_requests_dashboard_path, 'autocomplete-path' => search_autocomplete_path } } - %input{ type: "text", placeholder: _('Search or jump to...'), class: 'form-control gl-form-input' } + .gl-search-box-by-type + = sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon') + %input{ type: "text", placeholder: _('Search or jump to...'), class: 'form-control gl-form-input gl-search-box-by-type-input', id: 'search', autocomplete: 'off' } - else = render 'layouts/search' %li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' } diff --git a/app/views/projects/_invite_members_side_nav_link.html.haml b/app/views/projects/_invite_members_side_nav_link.html.haml index ea6174d19f0..fae681b1a71 100644 --- a/app/views/projects/_invite_members_side_nav_link.html.haml +++ b/app/views/projects/_invite_members_side_nav_link.html.haml @@ -1,5 +1,4 @@ .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'} } diff --git a/app/views/shared/nav/_scope_menu.html.haml b/app/views/shared/nav/_scope_menu.html.haml index 1a7089fb570..4e570086bf8 100644 --- a/app/views/shared/nav/_scope_menu.html.haml +++ b/app/views/shared/nav/_scope_menu.html.haml @@ -1,5 +1,5 @@ = nav_link(**scope_menu.active_routes, html_options: scope_menu.nav_link_html_options) do - = link_to scope_menu.link, **scope_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: scope_qa_menu_item(scope_menu.container) } do + = link_to scope_menu.link, **scope_menu.link_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: scope_qa_menu_item(scope_menu.container) } do %span{ class: scope_avatar_classes(scope_menu.container) } = source_icon(scope_menu.container, alt: scope_menu.title, class: ['avatar', 'avatar-tile', 's32'], width: 32, height: 32) %span.sidebar-context-title diff --git a/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml index 953f7a8ae60..d0ae5e99707 100644 --- a/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml +++ b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml @@ -1,3 +1,3 @@ %li.hidden - = link_to sidebar_hidden_menu_item.link, **sidebar_hidden_menu_item.container_html_options do + = link_to sidebar_hidden_menu_item.link, **sidebar_hidden_menu_item.link_html_options do = sidebar_hidden_menu_item.title diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml index 3f71368aff3..4c4ceb9ea70 100644 --- a/app/views/shared/nav/_sidebar_menu.html.haml +++ b/app/views/shared/nav/_sidebar_menu.html.haml @@ -2,7 +2,7 @@ - if sidebar_menu.menu_with_partial? = render_if_exists sidebar_menu.menu_partial, **sidebar_menu.menu_partial_options - else - = link_to sidebar_menu.link, **sidebar_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: sidebar_menu.title } do + = link_to sidebar_menu.link, **sidebar_menu.link_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: sidebar_menu.title } do - if sidebar_menu.icon_or_image? %span.nav-icon-container - if sidebar_menu.image_path diff --git a/app/views/shared/nav/_sidebar_menu_item.html.haml b/app/views/shared/nav/_sidebar_menu_item.html.haml index 674ce593ee2..5452cd486da 100644 --- a/app/views/shared/nav/_sidebar_menu_item.html.haml +++ b/app/views/shared/nav/_sidebar_menu_item.html.haml @@ -1,5 +1,5 @@ = nav_link(**sidebar_menu_item.active_routes, html_options: sidebar_menu_item.nav_link_html_options) do - = link_to sidebar_menu_item.link, **sidebar_menu_item.container_html_options, data: { qa_selector: 'sidebar_menu_item_link', qa_menu_item: sidebar_menu_item.title } do + = link_to sidebar_menu_item.link, **sidebar_menu_item.link_html_options, data: { qa_selector: 'sidebar_menu_item_link', qa_menu_item: sidebar_menu_item.title } do %span = sidebar_menu_item.title - if sidebar_menu_item.sprite_icon |