diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-20 00:11:07 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-20 00:11:07 +0300 |
commit | 83d7a3922367d95a0875f9bdbdbf34a37f84e3fb (patch) | |
tree | 8b8bcfea60335e24f38d813e950624d30a4a94ff /app | |
parent | fe389e7d345a391a8b6e6392dcf1bbd23546b323 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 380 insertions, 203 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index c7e6b98a934..a9bb2909c39 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,7 @@ const Api = { projectPackagesPath: '/api/:version/projects/:id/packages', projectPackagePath: '/api/:version/projects/:id/packages/:package_id', groupProjectsPath: '/api/:version/groups/:id/projects.json', + groupSharePath: '/api/:version/groups/:id/share', projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', forkedProjectsPath: '/api/:version/projects/:id/forks', @@ -39,6 +40,7 @@ const Api = { projectRunnersPath: '/api/:version/projects/:id/runners', projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches', projectSearchPath: '/api/:version/projects/:id/search', + projectSharePath: '/api/:version/projects/:id/share', projectMilestonesPath: '/api/:version/projects/:id/milestones', projectIssuePath: '/api/:version/projects/:id/issues/:issue_iid', mergeRequestsPath: '/api/:version/merge_requests', @@ -365,6 +367,16 @@ const Api = { }); }, + projectShareWithGroup(id, options = {}) { + const url = Api.buildUrl(Api.projectSharePath).replace(':id', encodeURIComponent(id)); + + return axios.post(url, { + expires_at: options.expires_at, + group_access: options.group_access, + group_id: options.group_id, + }); + }, + projectMilestones(id, params = {}) { const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); @@ -426,6 +438,16 @@ const Api = { }); }, + groupShareWithGroup(id, options = {}) { + const url = Api.buildUrl(Api.groupSharePath).replace(':id', encodeURIComponent(id)); + + return axios.post(url, { + expires_at: options.expires_at, + group_access: options.group_access, + group_id: options.group_id, + }); + }, + commit(id, sha, params = {}) { const url = Api.buildUrl(this.commitPath) .replace(':id', encodeURIComponent(id)) diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue new file mode 100644 index 00000000000..4a72e97db8c --- /dev/null +++ b/app/assets/javascripts/invite_members/components/group_select.vue @@ -0,0 +1,103 @@ +<script> +import { GlDropdown, GlDropdownItem, GlDropdownText, GlSearchBoxByType } from '@gitlab/ui'; +import { debounce } from 'lodash'; +import Api from '~/api'; +import { s__ } from '~/locale'; +import { SEARCH_DELAY } from '../constants'; + +export default { + name: 'GroupSelect', + components: { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + }, + model: { + prop: 'selectedGroup', + }, + data() { + return { + isFetching: false, + groups: [], + selectedGroup: {}, + searchTerm: '', + }; + }, + computed: { + selectedGroupName() { + return this.selectedGroup.name || this.$options.i18n.dropdownText; + }, + isFetchResultEmpty() { + return this.groups.length === 0; + }, + }, + watch: { + searchTerm() { + this.retrieveGroups(); + }, + }, + mounted() { + this.retrieveGroups(); + }, + methods: { + retrieveGroups: debounce(function debouncedRetrieveGroups() { + this.isFetching = true; + return Api.groups(this.searchTerm, this.$options.defaultFetchOptions) + .then((response) => { + this.groups = response.map((group) => ({ + id: group.id, + name: group.full_name, + path: group.path, + })); + this.isFetching = false; + }) + .catch(() => { + this.isFetching = false; + }); + }, SEARCH_DELAY), + selectGroup(group) { + this.selectedGroup = group; + + this.$emit('input', this.selectedGroup); + }, + }, + i18n: { + dropdownText: s__('GroupSelect|Select a group'), + searchPlaceholder: s__('GroupSelect|Search groups'), + emptySearchResult: s__('GroupSelect|No matching results'), + }, + defaultFetchOptions: { + exclude_internal: true, + active: true, + }, +}; +</script> +<template> + <div> + <gl-dropdown + data-testid="group-select-dropdown" + :text="selectedGroupName" + block + menu-class="gl-w-full!" + > + <gl-search-box-by-type + v-model.trim="searchTerm" + :is-loading="isFetching" + :placeholder="$options.i18n.searchPlaceholder" + data-qa-selector="group_select_dropdown_search_field" + /> + <gl-dropdown-item + v-for="group in groups" + :key="group.id" + :name="group.name" + @click="selectGroup(group)" + > + {{ group.name }} + </gl-dropdown-item> + <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message"> + <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> + </gl-dropdown-text> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue new file mode 100644 index 00000000000..c9de078319a --- /dev/null +++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue @@ -0,0 +1,34 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import eventHub from '../event_hub'; + +export default { + components: { + GlButton, + }, + props: { + displayText: { + type: String, + required: false, + default: s__('InviteMembers|Invite a group'), + }, + classes: { + type: String, + required: false, + default: '', + }, + }, + methods: { + openModal() { + eventHub.$emit('openModal', { inviteeType: 'group' }); + }, + }, +}; +</script> + +<template> + <gl-button :class="classes" data-qa-selector="invite_a_group_button" @click="openModal"> + {{ displayText }} + </gl-button> +</template> diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index f5a65882fba..cd9c3b0b5d3 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -11,9 +11,10 @@ import { } from '@gitlab/ui'; import { partition, isString } from 'lodash'; import Api from '~/api'; +import GroupSelect from '~/invite_members/components/group_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { s__, __, sprintf } from '~/locale'; +import { s__, sprintf } from '~/locale'; import eventHub from '../event_hub'; export default { @@ -28,6 +29,7 @@ export default { GlButton, GlFormInput, MembersTokenSelect, + GroupSelect, }, props: { id: { @@ -60,21 +62,21 @@ export default { visible: true, modalId: 'invite-members-modal', selectedAccessLevel: this.defaultAccessLevel, + inviteeType: 'members', newUsersToInvite: [], selectedDate: undefined, + groupToBeSharedWith: {}, }; }, computed: { - inviteToName() { - return this.name.toUpperCase(); - }, - inviteToType() { - return this.isProject ? __('project') : __('group'); + isInviteGroup() { + return this.inviteeType === 'group'; }, introText() { - return sprintf(s__("InviteMembersModal|You're inviting members to the %{name} %{type}"), { - name: this.inviteToName, - type: this.inviteToType, + const inviteTo = this.isProject ? 'toProject' : 'toGroup'; + + return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, { + name: this.name.toUpperCase(), }); }, toastOptions() { @@ -82,12 +84,12 @@ export default { onComplete: () => { this.selectedAccessLevel = this.defaultAccessLevel; this.newUsersToInvite = []; + this.groupToBeSharedWith = {}; }, }; }, basePostData() { return { - access_level: this.selectedAccessLevel, expires_at: this.selectedDate, format: 'json', }; @@ -97,9 +99,16 @@ export default { (key) => this.accessLevels[key] === Number(this.selectedAccessLevel), ); }, + inviteDisabled() { + return ( + this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 + ); + }, }, mounted() { - eventHub.$on('openModal', this.openModal); + eventHub.$on('openModal', (options) => { + this.openModal(options); + }); }, methods: { partitionNewUsersToInvite() { @@ -113,26 +122,42 @@ export default { usersToAddById.map((user) => user.id).join(','), ]; }, - openModal() { + openModal({ inviteeType }) { + this.inviteeType = inviteeType; + this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, closeModal() { this.$root.$emit(BV_HIDE_MODAL, this.modalId); }, sendInvite() { - this.submitForm(); + if (this.isInviteGroup) { + this.submitShareWithGroup(); + } else { + this.submitInviteMembers(); + } this.closeModal(); }, cancelInvite() { this.selectedAccessLevel = this.defaultAccessLevel; this.selectedDate = undefined; - this.newUsersToInvite = ''; + this.newUsersToInvite = []; + this.groupToBeSharedWith = {}; this.closeModal(); }, changeSelectedItem(item) { this.selectedAccessLevel = item; }, - submitForm() { + submitShareWithGroup() { + const apiShareWithGroup = this.isProject + ? Api.projectShareWithGroup.bind(Api) + : Api.groupShareWithGroup.bind(Api); + + apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id)) + .then(this.showToastMessageSuccess) + .catch(this.showToastMessageError); + }, + submitInviteMembers() { const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const promises = []; @@ -155,10 +180,25 @@ export default { Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError); }, inviteByEmailPostData(usersToInviteByEmail) { - return { ...this.basePostData, email: usersToInviteByEmail }; + return { + ...this.basePostData, + email: usersToInviteByEmail, + access_level: this.selectedAccessLevel, + }; }, addByUserIdPostData(usersToAddById) { - return { ...this.basePostData, user_id: usersToAddById }; + return { + ...this.basePostData, + user_id: usersToAddById, + access_level: this.selectedAccessLevel, + }; + }, + shareWithGroupPostData(groupToBeSharedWith) { + return { + ...this.basePostData, + group_id: groupToBeSharedWith, + group_access: this.selectedAccessLevel, + }; }, showToastMessageSuccess() { this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); @@ -170,9 +210,28 @@ export default { }, }, labels: { - modalTitle: s__('InviteMembersModal|Invite team members'), - newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'), - userPlaceholder: s__('InviteMembersModal|Search for members to invite'), + members: { + modalTitle: s__('InviteMembersModal|Invite team members'), + searchField: s__('InviteMembersModal|GitLab member or Email address'), + placeHolder: s__('InviteMembersModal|Search for members to invite'), + toGroup: { + introText: s__("InviteMembersModal|You're inviting members to the %{name} group"), + }, + toProject: { + introText: s__("InviteMembersModal|You're inviting members to the %{name} project"), + }, + }, + group: { + modalTitle: s__('InviteMembersModal|Invite a group'), + searchField: s__('InviteMembersModal|Select a group to invite'), + placeHolder: s__('InviteMembersModal|Search for a group to invite'), + toGroup: { + introText: s__("InviteMembersModal|You're inviting a group to the %{name} group"), + }, + toProject: { + introText: s__("InviteMembersModal|You're inviting a group to the %{name} project"), + }, + }, accessLevel: s__('InviteMembersModal|Choose a role permission'), accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'), toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'), @@ -189,27 +248,34 @@ export default { <gl-modal :modal-id="modalId" size="sm" - :title="$options.labels.modalTitle" + data-qa-selector="invite_members_modal_content" + :title="$options.labels[inviteeType].modalTitle" :header-close-label="$options.labels.headerCloseLabel" > - <div class="gl-ml-5 gl-mr-5"> - <div>{{ introText }}</div> + <div> + <p ref="introText">{{ introText }}</p> <label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{ - $options.labels.newUsersToInvite + $options.labels[inviteeType].searchField }}</label> <div class="gl-mt-2"> <members-token-select + v-if="!isInviteGroup" v-model="newUsersToInvite" - :label="$options.labels.newUsersToInvite" :aria-labelledby="$options.membersTokenSelectLabelId" - :placeholder="$options.labels.userPlaceholder" + :placeholder="$options.labels[inviteeType].placeHolder" /> + <group-select v-if="isInviteGroup" v-model="groupToBeSharedWith" /> </div> - <label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label> + <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName"> + <gl-dropdown + class="gl-shadow-none gl-w-full" + data-qa-selector="access_level_dropdown" + v-bind="$attrs" + :text="selectedRoleName" + > <template v-for="(key, item) in accessLevels"> <gl-dropdown-item :key="key" @@ -223,7 +289,7 @@ export default { </gl-dropdown> </div> - <div class="gl-mt-2"> + <div class="gl-mt-2 gl-w-half gl-xs-w-full"> <gl-sprintf :message="$options.labels.readMoreText"> <template #link="{ content }"> <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> @@ -231,7 +297,7 @@ export default { </gl-sprintf> </div> - <label class="gl-font-weight-bold gl-mt-5" for="expires_at">{{ + <label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{ $options.labels.accessExpireDate }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> @@ -253,15 +319,16 @@ export default { </div> <template #modal-footer> - <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-p-3"> + <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"> <gl-button ref="cancelButton" @click="cancelInvite"> {{ $options.labels.cancelButtonText }} </gl-button> <div class="gl-mr-3"></div> <gl-button ref="inviteButton" - :disabled="!newUsersToInvite" + :disabled="inviteDisabled" variant="success" + data-qa-selector="invite_button" @click="sendInvite" >{{ $options.labels.inviteButtonText }}</gl-button > diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index eb97c458f88..f8cc74511d9 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -27,14 +27,14 @@ export default { }, methods: { openModal() { - eventHub.$emit('openModal'); + eventHub.$emit('openModal', { inviteeType: 'members' }); }, }, }; </script> <template> - <gl-link :class="classes" @click="openModal"> + <gl-link :class="classes" data-qa-selector="invite_members_button" @click="openModal"> <div v-if="icon" class="nav-icon-container"> <gl-icon :size="16" :name="icon" /> </div> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 233a214013b..db6a7888786 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -3,7 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/u import { debounce } from 'lodash'; import { __ } from '~/locale'; import { getUsers } from '~/rest_api'; -import { USER_SEARCH_DELAY } from '../constants'; +import { SEARCH_DELAY } from '../constants'; export default { components: { @@ -67,7 +67,7 @@ export default { .catch(() => { this.loading = false; }); - }, USER_SEARCH_DELAY), + }, SEARCH_DELAY), handleInput() { this.$emit('input', this.selectedTokens); }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 1ff2125c292..2044dad896f 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -1 +1 @@ -export const USER_SEARCH_DELAY = 200; +export const SEARCH_DELAY = 200; diff --git a/app/assets/javascripts/invite_members/init_invite_group_trigger.js b/app/assets/javascripts/invite_members/init_invite_group_trigger.js new file mode 100644 index 00000000000..c01bb1bae28 --- /dev/null +++ b/app/assets/javascripts/invite_members/init_invite_group_trigger.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import InviteGroupTrigger from '~/invite_members/components/invite_group_trigger.vue'; + +export default function initInviteGroupTrigger() { + const el = document.querySelector('.js-invite-group-trigger'); + + if (!el) { + return false; + } + + return new Vue({ + el, + render: (createElement) => + createElement(InviteGroupTrigger, { + props: { + ...el.dataset, + }, + }), + }); +} diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index a4ba86dc6a1..4290971623e 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -60,15 +60,15 @@ export default { <template> <div> - <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false"> + <gl-alert v-if="errorMessage" class="gl-mb-7" variant="danger" :dismissible="false"> {{ errorMessage }} </gl-alert> - <h2>{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> + <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <div v-if="showNewUI" - class="gl-display-flex gl-justify-content-space-between gl-my-7 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200" + class="jira-connect-app-body gl-display-flex gl-justify-content-space-between gl-my-7 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200" > <h5 class="gl-align-self-center gl-mb-0" data-testid="new-jira-connect-ui-heading"> {{ s__('Integrations|Linked namespaces') }} diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index 7191fce3c33..96029e2711b 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -77,6 +77,7 @@ export async function initJiraConnect() { Vue.use(GlFeatureFlagsPlugin); const { groupsPath, subscriptionsPath, usersPath } = el.dataset; + AP.sizeToParent(); return new Vue({ el, diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index 3496f699b06..63afc6fe94d 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import { groupMemberRequestFormatter } from '~/groups/members/utils'; import groupsSelect from '~/groups_select'; +import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { s__ } from '~/locale'; @@ -70,5 +71,6 @@ memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal(); initInviteMembersModal(); initInviteMembersTrigger(); +initInviteGroupTrigger(); new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/feature_flags_user_lists/new/index.js b/app/assets/javascripts/pages/projects/feature_flags_user_lists/new/index.js index 679f0af8efc..e855447d5ce 100644 --- a/app/assets/javascripts/pages/projects/feature_flags_user_lists/new/index.js +++ b/app/assets/javascripts/pages/projects/feature_flags_user_lists/new/index.js @@ -1,3 +1,5 @@ +/* eslint-disable no-new */ + import Vue from 'vue'; import Vuex from 'vuex'; import NewUserList from '~/user_lists/components/new_user_list.vue'; @@ -5,18 +7,16 @@ import createStore from '~/user_lists/store/new'; Vue.use(Vuex); -document.addEventListener('DOMContentLoaded', () => { - const el = document.getElementById('js-new-user-list'); - const { userListsDocsPath, featureFlagsPath } = el.dataset; - return new Vue({ - el, - store: createStore(el.dataset), - provide: { - userListsDocsPath, - featureFlagsPath, - }, - render(h) { - return h(NewUserList); - }, - }); +const el = document.getElementById('js-new-user-list'); +const { userListsDocsPath, featureFlagsPath } = el.dataset; +new Vue({ + el, + store: createStore(el.dataset), + provide: { + userListsDocsPath, + featureFlagsPath, + }, + render(h) { + return h(NewUserList); + }, }); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index ed11b07be4a..8d403b7688a 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import { deprecatedCreateFlash as flash } from '~/flash'; import groupsSelect from '~/groups_select'; +import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { __ } from '~/locale'; @@ -29,6 +30,7 @@ memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal(); initInviteMembersModal(); initInviteMembersTrigger(); +initInviteGroupTrigger(); new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue index 8aa6e29adf1..c5fdb5fc242 100644 --- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -1,11 +1,11 @@ <script> +import { GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { isFunction } from 'lodash'; import { hasHorizontalOverflow } from '~/lib/utils/dom_utils'; -import tooltip from '../directives/tooltip'; export default { directives: { - tooltip, + GlTooltip, }, props: { title: { @@ -59,9 +59,8 @@ export default { <template> <span v-if="showTooltip" - v-tooltip + v-gl-tooltip="{ placement }" :title="title" - :data-placement="placement" class="js-show-tooltip gl-min-w-0" > <slot></slot> diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js deleted file mode 100644 index 0eb505bfce8..00000000000 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'jquery'; -import '~/commons/bootstrap'; -import { parseBoolean } from '~/lib/utils/common_utils'; - -export default { - bind(el) { - const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); - const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; - - $(el).tooltip({ - trigger: 'hover', - delay, - // By default, sanitize is run even if there is no `html` or `template` present - // so let's optimize to only run this when necessary. - // https://github.com/twbs/bootstrap/blob/c5966de27395a407f9a3d20d0eb2ff8e8fb7b564/js/src/tooltip.js#L716 - sanitize: parseBoolean(el.dataset.html) || Boolean(el.dataset.template), - }); - }, - - componentUpdated(el) { - $(el).tooltip('_fixTitle'); - - // update visible tooltips - const tooltipInstance = $(el).data('bs.tooltip'); - const tip = tooltipInstance.getTipElement(); - tooltipInstance.setElementContent( - $(tip.querySelectorAll('.tooltip-inner')), - tooltipInstance.getTitle(), - ); - }, - - unbind(el) { - $(el).tooltip('dispose'); - }, -}; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 7931f4deea0..1fe94a796f5 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -42,7 +42,6 @@ @import 'framework/notes'; @import 'framework/tabs'; @import 'framework/timeline'; -@import 'framework/tooltips'; @import 'framework/toggle'; @import 'framework/typography'; @import 'framework/zen'; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index b0334da6943..b81e9828dec 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -283,12 +283,6 @@ ul.indent-list { to { transform: rotate(360deg); } } -.namespace-title { - .tooltip-inner { - max-width: 350px; - } -} - .horizontal-list { padding-left: 0; list-style: none; diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss deleted file mode 100644 index edc2fb532c8..00000000000 --- a/app/assets/stylesheets/framework/tooltips.scss +++ /dev/null @@ -1,6 +0,0 @@ -.tooltip-inner { - font-size: $gl-font-size-small; - border-radius: $border-radius-default; - line-height: $gl-line-height; - font-weight: $gl-font-weight-normal; -} diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index 25401a161da..eb2dd6e578e 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -20,36 +20,23 @@ @import '@gitlab/ui/src/components/base/tooltip/tooltip'; $atlaskit-border-color: #dfe1e6; +$header-height: 40px; -.ac-content { - margin: 20px; - - .subscription-form { - margin-bottom: 20px; - - .field-group-input { - display: flex; - padding-top: $gl-padding-4; +.subscription-form { + .field-group-input { + display: flex; + padding-top: $gl-padding-4; - .ak-button { - align-items: center; - height: auto; - margin-left: $btn-margin-5; - } + .ak-button { + align-items: center; + height: auto; + margin-left: $btn-margin-5; } } } -$header-height: 40px; - .jira-connect-header { - border-bottom: 1px solid $gray-100; - display: flex; - align-items: center; - justify-content: center; min-height: $header-height; - padding-left: 16px; - padding-right: 16px; position: fixed; top: 0; left: 0; @@ -57,7 +44,6 @@ $header-height: 40px; } .jira-connect-user { - font-size: $gl-font-size; position: fixed; top: 10px; right: 20px; @@ -65,20 +51,17 @@ $header-height: 40px; .jira-connect-app { margin-top: $header-height; + height: calc(100% - #{$header-height}); + max-width: 1000px; +} + +.jira-connect-app-body { max-width: 600px; - min-height: 95vh; - padding-top: 48px; - padding-left: 16px; - padding-right: 16px; margin-left: auto; margin-right: auto; - text-align: center; -} - -.gl-mt-5 { - margin-top: 16px; } +// for external_link buttons svg { fill: currentColor; @@ -115,12 +98,3 @@ svg { } } } - -.empty-subscriptions { - color: $gray-900; -} - -.browser-limitations-notice { - font-size: $gl-font-size; - margin-top: 32px; -} diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a5257ff20bc..de9859230d5 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -16,8 +16,9 @@ = html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - if can_invite_members_for_group?(@group) .gl-w-half.gl-xs-w-full - .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3 - .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } } + .gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3 + .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } } + .js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } } = render 'groups/invite_members_modal', group: @group - if can_manage_members && !can_invite_members_for_group?(@group) %hr.gl-mt-4 diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index a549ed3540b..6a549227185 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -1,61 +1,62 @@ -%header.jira-connect-header - = brand_header_logo +%header.jira-connect-header.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-px-5.gl-border-b-solid.gl-border-b-gray-100.gl-border-b-1.gl-bg-white + = link_to brand_header_logo, Gitlab.config.gitlab.url, target: '_blank', rel: 'noopener noreferrer' -.jira-connect-user +.jira-connect-user.gl-font-base - if current_user - user_link = link_to(current_user.to_reference, jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in') = _('Signed in to GitLab as %{user_link}').html_safe % { user_link: user_link } - elsif @subscriptions.present? = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' -.jira-connect-app +%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto - if current_user.blank? && @subscriptions.empty? - %h2= s_('JiraService|GitLab for Jira Configuration') - %p= s_('JiraService|Sign in to GitLab.com to get started.') + .jira-connect-app-body.gl-text-center + %h2= s_('JiraService|GitLab for Jira Configuration') + %p= s_('JiraService|Sign in to GitLab.com to get started.') - .gl-mt-7 - - sign_in_button_class = new_jira_connect_ui? ? 'btn gl-button btn-confirm' : 'ak-button ak-button__appearance-primary' - = external_link _('Sign in to GitLab'), jira_connect_users_path, class: "#{sign_in_button_class} js-jira-connect-sign-in" + .gl-mt-7 + - sign_in_button_class = new_jira_connect_ui? ? 'btn gl-button btn-confirm' : 'ak-button ak-button__appearance-primary' + = external_link _('Sign in to GitLab'), jira_connect_users_path, class: "#{sign_in_button_class} js-jira-connect-sign-in" - .gl-mt-7 - %p Note: this integration only works with accounts on GitLab.com (SaaS). + .gl-mt-7 + %p= s_('Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).') - else .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) } - - unless new_jira_connect_ui? - %form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path } - .ak-field-group - %label - GitLab namespace + .jira-connect-app-body + - unless new_jira_connect_ui? + %form#add-subscription-form.subscription-form.gl-mb-5{ action: jira_connect_subscriptions_path } + .ak-field-group + %label= _('GitLab namespace') - .ak-field-group.field-group-input - %input#namespace-input.ak-field-text{ type: 'text', required: true, placeholder: 'e.g. "MyCompany" or "MyCompany/GroupName"' } - %button.ak-button.ak-button__appearance-primary{ type: 'submit' } - Link namespace to Jira + .ak-field-group.field-group-input + %input#namespace-input.ak-field-text{ type: 'text', required: true, placeholder: 'e.g. "MyCompany" or "MyCompany/GroupName"' } + %button.ak-button.ak-button__appearance-primary{ type: 'submit' } + = s_('Integrations|Link namespace to Jira') - - if @subscriptions.present? - %table.subscriptions.gl-w-full - %thead - %tr - %th Namespace - %th Added - %th - %tbody - - @subscriptions.each do |subscription| + - if @subscriptions.present? + %table.subscriptions.gl-w-full + %thead %tr - %td= subscription.namespace.full_path - %td= subscription.created_at - %td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'js-jira-connect-remove-subscription' - - else - %h4.empty-subscriptions - No linked namespaces - %p= s_('Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance.') + %th= _('Namespace') + %th= _('Added') + %th + %tbody + - @subscriptions.each do |subscription| + %tr + %td= subscription.namespace.full_path + %td= subscription.created_at + %td= link_to _('Remove'), jira_connect_subscription_path(subscription), class: 'js-jira-connect-remove-subscription' + - else + .gl-text-center + %h4= s_('Integrations|No linked namespaces') + %p= s_('Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance.') - %p.browser-limitations-notice - %strong Browser limitations: - Adding a namespace currently works only in browsers that allow cross‑site cookies. Please make sure to use - %a{ href: 'https://www.mozilla.org/en-US/firefox/', target: '_blank', rel: 'noopener noreferrer' } Firefox - or enable cross‑site cookies in your browser when adding a namespace. + %p.jira-connect-app-body.gl-mt-7.gl-font-base.gl-text-center + %strong= s_('Integrations|Browser limitations') + - firefox_link_url = 'https://www.mozilla.org/en-US/firefox/' + - firefox_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: firefox_link_url } + = s_('Integrations|Adding a namespace currently works only in browsers that allow cross‑site cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable cross‑site cookies in your browser when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe } = link_to _('Learn more'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/284211', target: '_blank', rel: 'noopener noreferrer' = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? diff --git a/app/views/layouts/jira_connect.html.haml b/app/views/layouts/jira_connect.html.haml index da45d84a83b..e9da940ca7a 100644 --- a/app/views/layouts/jira_connect.html.haml +++ b/app/views/layouts/jira_connect.html.haml @@ -12,5 +12,4 @@ = Gon::Base.render_data(nonce: content_security_policy_nonce) = yield :head %body - .ac-content - = yield + = yield diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index beb435d268a..509092b64c9 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -18,8 +18,9 @@ %p = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe } .col-md-12.col-lg-6 - .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3 - .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } } + .gl-display-flex.gl-flex-wrap.gl-justify-content-end + .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } } + .js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } } = render 'projects/invite_members_modal', project: @project - else diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 59c6b27e942..b8481382577 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -355,14 +355,6 @@ :weight: 1 :idempotent: :tags: [] -- :name: cronjob:releases_create_evidence - :feature_category: :release_evidence - :has_external_dependencies: - :urgency: :low - :resource_boundary: :unknown - :weight: 1 - :idempotent: - :tags: [] - :name: cronjob:releases_manage_evidence :feature_category: :release_evidence :has_external_dependencies: @@ -2100,6 +2092,14 @@ :weight: 2 :idempotent: :tags: [] +- :name: releases_create_evidence + :feature_category: :release_evidence + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] - :name: remote_mirror_notification :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/releases/create_evidence_worker.rb b/app/workers/releases/create_evidence_worker.rb index db75fae1639..d22329216f9 100644 --- a/app/workers/releases/create_evidence_worker.rb +++ b/app/workers/releases/create_evidence_worker.rb @@ -3,7 +3,6 @@ module Releases class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker - include CronjobQueue # rubocop:disable Scalability/CronWorkerContext feature_category :release_evidence |