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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-13 12:11:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-13 12:11:10 +0300
commit37974ac0b196b06ffcc6cbea44385eaac1cc57bd (patch)
tree98450a46516f93a71018ec6b8d718fc023744575 /app
parentfcbd3db20f5dfb13ae33ddfee98be8d92cade72f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/filtered_search.vue54
-rw-r--r--app/assets/javascripts/boards/filtered_search.js25
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql2
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_modal.vue4
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_trigger.vue8
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_modal.js9
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_trigger.js6
-rw-r--r--app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql16
-rw-r--r--app/assets/javascripts/pages/projects/project.js17
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js25
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue25
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue37
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue279
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue51
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue39
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue203
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue38
-rw-r--r--app/assets/javascripts/sidebar/constants.js9
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js96
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql10
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql3
-rw-r--r--app/controllers/admin/services_controller.rb2
-rw-r--r--app/helpers/issuables_helper.rb3
-rw-r--r--app/models/bulk_imports/stage.rb65
-rw-r--r--app/models/bulk_imports/tracker.rb25
-rw-r--r--app/models/concerns/issuable.rb8
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/services/issuable/bulk_update_service.rb13
-rw-r--r--app/views/admin/services/_form.html.haml4
-rw-r--r--app/views/admin/services/_service_templates_deprecated_alert.html.haml8
-rw-r--r--app/views/admin/services/index.html.haml22
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml36
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml4
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/bulk_import_worker.rb13
-rw-r--r--app/workers/bulk_imports/entity_worker.rb49
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb70
48 files changed, 966 insertions, 380 deletions
diff --git a/app/assets/javascripts/boards/components/filtered_search.vue b/app/assets/javascripts/boards/components/filtered_search.vue
deleted file mode 100644
index 8505ea39a6b..00000000000
--- a/app/assets/javascripts/boards/components/filtered_search.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
-import { mapActions } from 'vuex';
-import { historyPushState } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-
-export default {
- i18n: {
- search: __('Search'),
- },
- components: { FilteredSearch },
- props: {
- search: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- initialSearch() {
- return [{ type: 'filtered-search-term', value: { data: this.search } }];
- },
- },
- methods: {
- ...mapActions(['performSearch']),
- handleSearch(filters) {
- let itemValue = '';
- const [item] = filters;
-
- if (filters.length === 0) {
- itemValue = '';
- } else {
- itemValue = item?.value?.data;
- }
-
- historyPushState(setUrlParams({ search: itemValue }, window.location.href));
-
- this.performSearch();
- },
- },
-};
-</script>
-
-<template>
- <filtered-search
- class="gl-w-full"
- namespace=""
- :tokens="[]"
- :search-input-placeholder="$options.i18n.search"
- :initial-filter-value="initialSearch"
- @onFilter="handleSearch"
- />
-</template>
diff --git a/app/assets/javascripts/boards/filtered_search.js b/app/assets/javascripts/boards/filtered_search.js
deleted file mode 100644
index 182a2cf3724..00000000000
--- a/app/assets/javascripts/boards/filtered_search.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Vue from 'vue';
-import store from '~/boards/stores';
-import { queryToObject } from '~/lib/utils/url_utility';
-import FilteredSearch from './components/filtered_search.vue';
-
-export default () => {
- const queryParams = queryToObject(window.location.search);
- const el = document.getElementById('js-board-filtered-search');
-
- /*
- When https://github.com/vuejs/vue-apollo/pull/1153 is merged and deployed
- we can remove apolloProvider option from here. Currently without it its causing
- an error
- */
-
- return new Vue({
- el,
- store,
- apolloProvider: {},
- render: (createElement) =>
- createElement(FilteredSearch, {
- props: { search: queryParams.search },
- }),
- });
-};
diff --git a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
new file mode 100644
index 00000000000..0b451262b5a
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
@@ -0,0 +1,5 @@
+fragment UserAvailability on User {
+ status {
+ availability
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index 3397a2529f1..e18eea33041 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -1,4 +1,5 @@
#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query usersSearch($search: String!, $fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
@@ -6,6 +7,7 @@ query usersSearch($search: String!, $fullPath: ID!) {
nodes {
user {
...User
+ ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
index 144c1a2c22a..ec77e49ae53 100644
--- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue
+++ b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
@@ -19,8 +19,10 @@ export default {
GlLink,
GlModal,
},
- inject: {
+ props: {
membersPath: {
+ type: String,
+ required: false,
default: '',
},
},
diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
index 56cf1ab2fc2..ee89e0bbf71 100644
--- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
+++ b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
@@ -7,14 +7,20 @@ export default {
components: {
GlLink,
},
- inject: {
+ props: {
displayText: {
+ type: String,
+ required: false,
default: '',
},
event: {
+ type: String,
+ required: false,
default: '',
},
label: {
+ type: String,
+ required: false,
default: '',
},
},
diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js
index c292bda1931..108f636ee3e 100644
--- a/app/assets/javascripts/invite_member/init_invite_member_modal.js
+++ b/app/assets/javascripts/invite_member/init_invite_member_modal.js
@@ -1,5 +1,6 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
+import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils';
import InviteMemberModal from './components/invite_member_modal.vue';
Vue.use(GlToast);
@@ -7,7 +8,7 @@ Vue.use(GlToast);
export default function initInviteMembersModal() {
const el = document.querySelector('.js-invite-member-modal');
- if (!el) {
+ if (!el || isInDesignPage() || isInIssuePage()) {
return false;
}
@@ -15,7 +16,9 @@ export default function initInviteMembersModal() {
return new Vue({
el,
- provide: { membersPath },
- render: (createElement) => createElement(InviteMemberModal),
+ render: (createElement) =>
+ createElement(InviteMemberModal, {
+ props: { membersPath },
+ }),
});
}
diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js
index 5e763e4f47d..eb765ae83b0 100644
--- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js
+++ b/app/assets/javascripts/invite_member/init_invite_member_trigger.js
@@ -10,7 +10,9 @@ export default function initInviteMembersTrigger() {
return new Vue({
el,
- provide: { ...el.dataset },
- render: (createElement) => createElement(InviteMemberTrigger),
+ render: (createElement) =>
+ createElement(InviteMemberTrigger, {
+ props: { ...el.dataset },
+ }),
});
}
diff --git a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
deleted file mode 100644
index 42e646391a8..00000000000
--- a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-
-query getProjectIssue($iid: String!, $fullPath: ID!) {
- project(fullPath: $fullPath) {
- issue(iid: $iid) {
- id
- assignees {
- nodes {
- ...Author
- id
- state
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index da8dc527d79..91f376060f8 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -123,10 +123,19 @@ export default class Project {
const loc = window.location.href;
if (loc.includes('/-/')) {
- const refs = this.fullData.Branches.concat(this.fullData.Tags);
- const currentRef = refs.find((ref) => loc.indexOf(ref) > -1);
- if (currentRef) {
- const targetPath = loc.split(currentRef)[1].slice(1).split('#')[0];
+ // Since the current ref in renderRow is outdated on page changes
+ // (To be addressed in: https://gitlab.com/gitlab-org/gitlab/-/issues/327085)
+ // We are deciphering the current ref from the dropdown data instead
+ const currentRef = $dropdown.data('ref');
+ // The split and startWith is to ensure an exact word match
+ // and avoid partial match ie. currentRef is "dev" and loc is "development"
+ const splitPathAfterRefPortion = loc.split(currentRef)[1];
+ const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/');
+
+ if (doesPathContainRef) {
+ // We are ignoring the url containing the ref portion
+ // and plucking the thereafter portion to reconstructure the url that is correct
+ const targetPath = splitPathAfterRefPortion?.slice(1).split('#')[0];
selectedUrl.searchParams.set('path', targetPath);
selectedUrl.hash = window.location.hash;
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 849f4c27988..cc53532b554 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -6,6 +6,8 @@ import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
import TestReports from './components/test_reports/test_reports.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import createDagApp from './pipeline_details_dag';
+import { createPipelinesDetailApp } from './pipeline_details_graph';
+import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './utils';
@@ -80,20 +82,19 @@ const createTestDetails = () => {
};
export default async function initPipelineDetailsBundle() {
- createTestDetails();
- createDagApp(apolloProvider);
-
const canShowNewPipelineDetails =
gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
+ try {
+ createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
+ } catch {
+ Flash(__('An error occurred while loading a section of this page.'));
+ }
+
if (canShowNewPipelineDetails) {
try {
- const { createPipelinesDetailApp } = await import(
- /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'
- );
-
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
Flash(__('An error occurred while loading the pipeline.'));
@@ -108,12 +109,6 @@ export default async function initPipelineDetailsBundle() {
createLegacyPipelinesDetailApp(mediator);
}
- try {
- const { createPipelineHeaderApp } = await import(
- /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
- );
- createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
- } catch {
- Flash(__('An error occurred while loading a section of this page.'));
- }
+ createDagApp(apolloProvider);
+ createTestDetails();
}
diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
index 822dfc09d81..cb9d8b64b33 100644
--- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
@@ -46,14 +46,7 @@ export default {
this.emitTargetProject(repo.name);
},
setDefaultRepo() {
- if (this.isSourceRevision) {
- this.selectedRepo = this.projectTo;
- return;
- }
-
- const [defaultTargetProject] = this.projectsFrom;
- this.emitTargetProject(defaultTargetProject.name);
- this.selectedRepo = defaultTargetProject;
+ this.selectedRepo = this.projectTo;
},
emitTargetProject(name) {
if (!this.isSourceRevision) {
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index d0a65b48522..98fc0b0a783 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -103,10 +103,10 @@ export default {
v-gl-tooltip="tooltipOption"
:href="assigneeUrl"
:title="tooltipTitle"
- class="d-inline-block"
+ class="gl-display-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
- <span class="d-flex">
+ <span class="gl-display-flex">
<assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot></slot>
</span>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index ca86d6c6c3e..f98798582c1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,7 +1,7 @@
<script>
import actionCable from '~/actioncable_consumer';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
+import { assigneesQueries } from '~/sidebar/constants';
export default {
subscription: null,
@@ -9,7 +9,8 @@ export default {
props: {
mediator: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
issuableIid: {
type: String,
@@ -19,10 +20,16 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
apollo: {
- project: {
- query,
+ workspace: {
+ query() {
+ return assigneesQueries[this.issuableType].query;
+ },
variables() {
return {
iid: this.issuableIid,
@@ -30,7 +37,9 @@ export default {
};
},
result(data) {
- this.handleFetchResult(data);
+ if (this.mediator) {
+ this.handleFetchResult(data);
+ }
},
},
},
@@ -43,7 +52,7 @@ export default {
methods: {
received(data) {
if (data.event === 'updated') {
- this.$apollo.queries.project.refetch();
+ this.$apollo.queries.workspace.refetch();
}
},
initActionCablePolling() {
@@ -57,7 +66,7 @@ export default {
);
},
handleFetchResult({ data }) {
- const { nodes } = data.project.issue.assignees;
+ const { nodes } = data.workspace.issuable.assignees;
const assignees = nodes.map((n) => ({
...n,
@@ -69,7 +78,7 @@ export default {
},
},
render() {
- return this.$slots.default;
+ return null;
},
};
</script>
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index b53b7039018..e93aced12f3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -18,6 +18,11 @@ export default {
required: false,
default: 'issue',
},
+ signedIn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
assigneesText() {
@@ -34,20 +39,28 @@ export default {
<div class="gl-display-flex gl-flex-direction-column issuable-assignees">
<div
v-if="emptyUsers"
- class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-2 hide-collapsed"
data-testid="none"
>
- <span> {{ __('None') }} -</span>
- <gl-button
- data-testid="assign-yourself"
- category="tertiary"
- variant="link"
- class="gl-ml-2"
- @click="$emit('assign-self')"
- >
- <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
- </gl-button>
+ <span> {{ __('None') }}</span>
+ <template v-if="signedIn">
+ <span class="gl-ml-2">-</span>
+ <gl-button
+ data-testid="assign-yourself"
+ category="tertiary"
+ variant="link"
+ class="gl-ml-2"
+ @click="$emit('assign-self')"
+ >
+ <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
+ </gl-button>
+ </template>
</div>
- <uncollapsed-assignee-list v-else :users="users" :issuable-type="issuableType" />
+ <uncollapsed-assignee-list
+ v-else
+ :users="users"
+ :issuable-type="issuableType"
+ class="gl-mt-2 hide-collapsed"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 6595debf9a5..e15ea595190 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -123,6 +123,7 @@ export default {
v-if="shouldEnableRealtime"
:issuable-iid="issuableIid"
:project-path="projectPath"
+ :issuable-type="issuableType"
:mediator="mediator"
/>
<assignee-title
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index cc2201ad359..34bc5cf5c2b 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,26 +1,28 @@
<script>
-import {
- GlDropdownItem,
- GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
- GlSearchBoxByType,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlDropdownItem, GlDropdownDivider, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
import searchUsers from '~/graphql_shared/queries/users_search.query.graphql';
import { IssuableType } from '~/issue_show/constants';
import { __, n__ } from '~/locale';
+import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SidebarInviteMembers from './sidebar_invite_members.vue';
+import SidebarParticipant from './sidebar_participant.vue';
export const assigneesWidget = Vue.observable({
updateAssignees: null,
});
+
+const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
+ bubbles: true,
+});
+
export default {
i18n: {
unassigned: __('Unassigned'),
@@ -28,17 +30,26 @@ export default {
assignees: __('Assignees'),
assignTo: __('Assign to'),
},
- assigneesQueries,
components: {
SidebarEditableItem,
IssuableAssignees,
MultiSelectDropdown,
GlDropdownItem,
GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
GlSearchBoxByType,
GlLoadingIcon,
+ SidebarInviteMembers,
+ SidebarParticipant,
+ SidebarAssigneesRealtime,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: {
+ directlyInviteMembers: {
+ default: false,
+ },
+ indirectlyInviteMembers: {
+ default: false,
+ },
},
props: {
iid: {
@@ -76,12 +87,13 @@ export default {
selected: [],
isSettingAssignees: false,
isSearching: false,
+ isDirty: false,
};
},
apollo: {
issuable: {
query() {
- return this.$options.assigneesQueries[this.issuableType].query;
+ return assigneesQueries[this.issuableType].query;
},
variables() {
return this.queryVariables;
@@ -134,6 +146,10 @@ export default {
},
},
computed: {
+ shouldEnableRealtime() {
+ // Note: Realtime is only available on issues right now, future support for MR wil be built later.
+ return this.glFeatures.realTimeIssueSidebar && this.issuableType === IssuableType.Issue;
+ },
queryVariables() {
return {
iid: this.iid,
@@ -155,6 +171,9 @@ export default {
},
assigneeText() {
const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected;
+ if (!items) {
+ return __('Assignee');
+ }
return n__('Assignee', '%d Assignees', items.length);
},
selectedFiltered() {
@@ -197,8 +216,15 @@ export default {
noUsersFound() {
return !this.isSearchEmpty && this.searchUsers.length === 0;
},
+ signedIn() {
+ return this.currentUser.username !== undefined;
+ },
showCurrentUser() {
- return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching);
+ return (
+ this.signedIn &&
+ !this.isCurrentUserInParticipants &&
+ (this.isSearchEmpty || this.isSearching)
+ );
},
},
watch: {
@@ -221,7 +247,7 @@ export default {
this.isSettingAssignees = true;
return this.$apollo
.mutate({
- mutation: this.$options.assigneesQueries[this.issuableType].mutation,
+ mutation: assigneesQueries[this.issuableType].mutation,
variables: {
...this.queryVariables,
assigneeUsernames,
@@ -239,20 +265,22 @@ export default {
});
},
selectAssignee(name) {
- if (name === undefined) {
- this.clearSelected();
- return;
- }
+ this.isDirty = true;
if (!this.multipleAssignees) {
- this.selected = [name];
+ this.selected = name ? [name] : [];
this.collapseWidget();
- } else {
- this.selected = this.selected.concat(name);
+ return;
}
+ if (name === undefined) {
+ this.clearSelected();
+ return;
+ }
+ this.selected = this.selected.concat(name);
},
unselect(name) {
this.selected = this.selected.filter((user) => user.username !== name);
+ this.isDirty = true;
if (!this.multipleAssignees) {
this.collapseWidget();
@@ -265,7 +293,9 @@ export default {
this.selected = [];
},
saveAssignees() {
+ this.isDirty = false;
this.updateAssignees(this.selectedUserNames);
+ this.$el.dispatchEvent(hideDropdownEvent);
},
isChecked(id) {
return this.selectedUserNames.includes(id);
@@ -291,6 +321,9 @@ export default {
collapseWidget() {
this.$refs.toggle.collapse();
},
+ expandWidget() {
+ this.$refs.toggle.expand();
+ },
showDivider(list) {
return list.length > 0 && this.isSearchEmpty;
},
@@ -299,121 +332,113 @@ export default {
</script>
<template>
- <div
- v-if="isAssigneesLoading"
- class="gl-display-flex gl-align-items-center assignee"
- data-testid="loading-assignees"
- >
- {{ __('Assignee') }}
- <gl-loading-icon size="sm" class="gl-ml-2" />
- </div>
- <sidebar-editable-item
- v-else
- ref="toggle"
- :loading="isSettingAssignees"
- :title="assigneeText"
- @open="focusSearch"
- @close="saveAssignees"
- >
- <template #collapsed>
- <issuable-assignees
- :users="assignees"
- :issuable-type="issuableType"
- class="gl-mt-2"
- @assign-self="assignSelf"
- />
- </template>
+ <div data-testid="assignees-widget">
+ <sidebar-assignees-realtime
+ v-if="shouldEnableRealtime"
+ :project-path="fullPath"
+ :issuable-iid="iid"
+ :issuable-type="issuableType"
+ />
+ <sidebar-editable-item
+ ref="toggle"
+ :loading="isSettingAssignees"
+ :initial-loading="isAssigneesLoading"
+ :title="assigneeText"
+ :is-dirty="isDirty"
+ @open="focusSearch"
+ @close="saveAssignees"
+ >
+ <template #collapsed>
+ <slot name="collapsed" :users="assignees" :on-click="expandWidget"></slot>
+ <issuable-assignees
+ :users="assignees"
+ :issuable-type="issuableType"
+ :signed-in="signedIn"
+ @assign-self="assignSelf"
+ @expand-widget="expandWidget"
+ />
+ </template>
- <template #default>
- <multi-select-dropdown
- class="gl-w-full dropdown-menu-user"
- :text="$options.i18n.assignees"
- :header-text="$options.i18n.assignTo"
- @toggle="collapseWidget"
- >
- <template #search>
- <gl-search-box-by-type ref="search" v-model.trim="search" />
- </template>
- <template #items>
- <gl-loading-icon
- v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading"
- data-testid="loading-participants"
- size="lg"
- />
- <template v-else>
- <template v-if="isSearchEmpty || isSearching">
+ <template #default>
+ <multi-select-dropdown
+ class="gl-w-full dropdown-menu-user"
+ :text="$options.i18n.assignees"
+ :header-text="$options.i18n.assignTo"
+ @toggle="collapseWidget"
+ >
+ <template #search>
+ <gl-search-box-by-type
+ ref="search"
+ v-model.trim="search"
+ class="js-dropdown-input-field"
+ />
+ </template>
+ <template #items>
+ <gl-loading-icon
+ v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading"
+ data-testid="loading-participants"
+ size="lg"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty || isSearching">
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ :is-check-centered="true"
+ data-testid="unassign"
+ @click="selectAssignee()"
+ >
+ <span
+ :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
+ class="gl-font-weight-bold"
+ >{{ $options.i18n.unassigned }}</span
+ ></gl-dropdown-item
+ >
+ </template>
+ <gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
<gl-dropdown-item
- :is-checked="selectedIsEmpty"
+ v-for="item in selectedFiltered"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
:is-check-centered="true"
- data-testid="unassign"
- @click="selectAssignee()"
+ data-testid="selected-participant"
+ @click.stop="unselect(item.username)"
>
- <span
- :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
- class="gl-font-weight-bold"
- >{{ $options.i18n.unassigned }}</span
- ></gl-dropdown-item
+ <sidebar-participant :user="item" />
+ </gl-dropdown-item>
+ <template v-if="showCurrentUser">
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ data-testid="current-user"
+ @click.stop="selectAssignee(currentUser)"
+ >
+ <sidebar-participant :user="currentUser" class="gl-pl-6!" />
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unselectedFiltered"
+ :key="unselectedUser.id"
+ data-testid="unselected-participant"
+ @click="selectAssignee(unselectedUser)"
>
- </template>
- <gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
- <gl-dropdown-item
- v-for="item in selectedFiltered"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- :is-check-centered="true"
- data-testid="selected-participant"
- @click.stop="unselect(item.username)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar || item.avatar_url"
- class="gl-align-items-center"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <template v-if="showCurrentUser">
- <gl-dropdown-divider />
+ <sidebar-participant :user="unselectedUser" class="gl-pl-6!" />
+ </gl-dropdown-item>
<gl-dropdown-item
- data-testid="current-user"
- @click.stop="selectAssignee(currentUser)"
+ v-if="noUsersFound && !isSearching"
+ data-testid="empty-results"
+ class="gl-pl-6!"
>
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="currentUser.name"
- :sub-label="currentUser.username"
- :src="currentUser.avatarUrl"
- class="gl-align-items-center gl-pl-6!"
- />
- </gl-avatar-link>
+ {{ __('No matching results') }}
</gl-dropdown-item>
</template>
- <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
- <gl-dropdown-item
- v-for="unselectedUser in unselectedFiltered"
- :key="unselectedUser.id"
- data-testid="unselected-participant"
- @click="selectAssignee(unselectedUser)"
- >
- <gl-avatar-link class="gl-pl-6!">
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- class="gl-align-items-center"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-item v-if="noUsersFound && !isSearching" data-testid="empty-results">
- {{ __('No matching results') }}
+ </template>
+ <template #footer>
+ <gl-dropdown-item>
+ <sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" />
</gl-dropdown-item>
</template>
- </template>
- </multi-select-dropdown>
- </template>
- </sidebar-editable-item>
+ </multi-select-dropdown>
+ </template>
+ </sidebar-editable-item>
+ </div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
new file mode 100644
index 00000000000..9952c6db582
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -0,0 +1,51 @@
+<script>
+import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
+import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { __ } from '~/locale';
+
+export default {
+ displayText: __('Invite members'),
+ dataTrackLabel: 'edit_assignee',
+ components: {
+ InviteMemberTrigger,
+ InviteMemberModal,
+ InviteMembersTrigger,
+ },
+ inject: {
+ projectMembersPath: {
+ default: '',
+ },
+ directlyInviteMembers: {
+ default: false,
+ },
+ },
+ computed: {
+ trackEvent() {
+ return this.directlyInviteMembers ? 'click_invite_members' : 'click_invite_members_version_b';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <invite-members-trigger
+ v-if="directlyInviteMembers"
+ trigger-element="anchor"
+ :display-text="$options.displayText"
+ :event="trackEvent"
+ :label="$options.dataTrackLabel"
+ classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
+ />
+ <template v-else>
+ <invite-member-trigger
+ :display-text="$options.displayText"
+ :event="trackEvent"
+ :label="$options.dataTrackLabel"
+ class="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
+ />
+ <invite-member-modal :members-path="projectMembersPath" />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
new file mode 100644
index 00000000000..e2a38a100b9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ userLabel() {
+ if (!this.user.status) {
+ return this.user.name;
+ }
+ return sprintf(s__('UserAvailability|%{author} (Busy)'), {
+ author: this.user.name,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="userLabel"
+ :sub-label="user.username"
+ :src="user.avatarUrl || user.avatar || user.avatar_url"
+ class="gl-align-items-center"
+ />
+ </gl-avatar-link>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index d0da4a9c75a..b7080bb05b8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,4 +1,5 @@
<script>
+import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
import UserNameWithStatus from './user_name_with_status.vue';
@@ -58,7 +59,10 @@ export default {
this.showLess = !this.showLess;
},
userAvailability(u) {
- return u?.availability || '';
+ if (this.issuableType === IssuableType.MergeRequest) {
+ return u?.availability || '';
+ }
+ return u?.status?.availability || '';
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
new file mode 100644
index 00000000000..141c2b3aae9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
@@ -0,0 +1,203 @@
+<script>
+import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
+import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { dueDateQueries } from '~/sidebar/constants';
+
+const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
+ bubbles: true,
+});
+
+export default {
+ tracking: {
+ event: 'click_edit_button',
+ label: 'right_sidebar',
+ property: 'dueDate',
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlButton,
+ GlIcon,
+ GlDatepicker,
+ SidebarEditableItem,
+ },
+ inject: ['fullPath', 'iid', 'canUpdate'],
+ props: {
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ dueDate: null,
+ loading: false,
+ };
+ },
+ apollo: {
+ dueDate: {
+ query() {
+ return dueDateQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.iid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.dueDate || null;
+ },
+ result({ data }) {
+ this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate);
+ },
+ error() {
+ createFlash({
+ message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
+ issuableType: this.issuableType,
+ }),
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.dueDate.loading || this.loading;
+ },
+ hasDueDate() {
+ return this.dueDate !== null;
+ },
+ parsedDueDate() {
+ if (!this.hasDueDate) {
+ return null;
+ }
+
+ return parsePikadayDate(this.dueDate);
+ },
+ formattedDueDate() {
+ if (!this.hasDueDate) {
+ return this.$options.i18n.noDueDate;
+ }
+
+ return dateInWords(this.parsedDueDate, true);
+ },
+ workspacePath() {
+ return this.issuableType === IssuableType.Issue
+ ? {
+ projectPath: this.fullPath,
+ }
+ : {
+ groupPath: this.fullPath,
+ };
+ },
+ },
+ methods: {
+ closeForm() {
+ this.$refs.editable.collapse();
+ this.$el.dispatchEvent(hideDropdownEvent);
+ this.$emit('closeForm');
+ },
+ openDatePicker() {
+ this.$refs.datePicker.calendar.show();
+ },
+ setDueDate(date) {
+ this.loading = true;
+ this.$refs.editable.collapse();
+ this.$apollo
+ .mutate({
+ mutation: dueDateQueries[this.issuableType].mutation,
+ variables: {
+ input: {
+ ...this.workspacePath,
+ iid: this.iid,
+ dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null,
+ },
+ },
+ })
+ .then(
+ ({
+ data: {
+ issuableSetDueDate: { errors },
+ },
+ }) => {
+ if (errors.length) {
+ createFlash({
+ message: errors[0],
+ });
+ } else {
+ this.$emit('closeForm');
+ }
+ },
+ )
+ .catch(() => {
+ createFlash({
+ message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
+ issuableType: this.issuableType,
+ }),
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ },
+ i18n: {
+ dueDate: __('Due date'),
+ noDueDate: __('None'),
+ removeDueDate: __('remove due date'),
+ },
+};
+</script>
+
+<template>
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.dueDate"
+ :tracking="$options.tracking"
+ :loading="isLoading"
+ class="block"
+ data-testid="due-date"
+ @open="openDatePicker"
+ >
+ <template #collapsed>
+ <div v-gl-tooltip :title="$options.i18n.dueDate" class="sidebar-collapsed-icon">
+ <gl-icon :size="16" name="calendar" />
+ <span class="collapse-truncated-title">{{ formattedDueDate }}</span>
+ </div>
+ <div class="gl-display-flex gl-align-items-center hide-collapsed">
+ <span
+ :class="hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
+ data-testid="sidebar-duedate-value"
+ >
+ {{ formattedDueDate }}
+ </span>
+ <div v-if="hasDueDate && canUpdate" class="gl-display-flex">
+ <span class="gl-px-2">-</span>
+ <gl-button
+ variant="link"
+ class="gl-text-gray-500!"
+ data-testid="reset-button"
+ :disabled="isLoading"
+ @click="setDueDate(null)"
+ >
+ {{ $options.i18n.removeDueDate }}
+ </gl-button>
+ </div>
+ </div>
+ </template>
+ <template #default>
+ <gl-datepicker
+ ref="datePicker"
+ :value="parsedDueDate"
+ show-clear-button
+ @input="setDueDate"
+ @clear="setDueDate(null)"
+ />
+ </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 4ab4606ac1c..caf1c92c28a 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: { GlButton, GlLoadingIcon },
@@ -20,6 +21,16 @@ export default {
required: false,
default: false,
},
+ initialLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isDirty: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
tracking: {
type: Object,
required: false,
@@ -35,6 +46,11 @@ export default {
edit: false,
};
},
+ computed: {
+ editButtonText() {
+ return this.isDirty ? __('Apply') : __('Edit');
+ },
+ },
destroyed() {
window.removeEventListener('click', this.collapseWhenOffClick);
window.removeEventListener('keyup', this.collapseOnEscape);
@@ -86,15 +102,15 @@ export default {
<template>
<div>
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
- <span class="hide-collapsed" data-testid="title">{{ title }}</span>
- <gl-loading-icon v-if="loading" inline class="gl-ml-2 hide-collapsed" />
+ <span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span>
+ <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" />
<gl-loading-icon
v-if="loading && isClassicSidebar"
inline
class="gl-mx-auto gl-my-0 hide-expanded"
/>
<gl-button
- v-if="canUpdate"
+ v-if="canUpdate && !initialLoading"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button"
@@ -105,14 +121,16 @@ export default {
@keyup.esc="toggle"
@click="toggle"
>
- {{ __('Edit') }}
+ {{ editButtonText }}
</gl-button>
</div>
- <div v-show="!edit" data-testid="collapsed-content">
- <slot name="collapsed">{{ __('None') }}</slot>
- </div>
- <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }">
- <slot :edit="edit"></slot>
- </div>
+ <template v-if="!initialLoading">
+ <div v-show="!edit" data-testid="collapsed-content">
+ <slot name="collapsed">{{ __('None') }}</slot>
+ </div>
+ <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }">
+ <slot :edit="edit"></slot>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index a0e636488f4..80e07d556bf 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,10 +1,12 @@
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
+import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
@@ -42,3 +44,10 @@ export const referenceQueries = {
query: mergeRequestReferenceQuery,
},
};
+
+export const dueDateQueries = {
+ [IssuableType.Issue]: {
+ query: issueDueDateQuery,
+ mutation: updateIssueDueDateMutation,
+ },
+};
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index b98c8a8c5dc..f5c8ed6bd61 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -10,7 +10,10 @@ import {
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
+import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
@@ -32,15 +35,6 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
-/**
- * Extracts the list of assignees with availability information from a hidden input
- * field and converts to a key:value pair for use in the sidebar assignees component.
- * The assignee username is used as the key and their busy status is the value
- *
- * e.g { root: 'busy', admin: '' }
- *
- * @returns {Object}
- */
function getSidebarAssigneeAvailabilityData() {
const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
return Array.from(sidebarAssigneeEl)
@@ -54,7 +48,7 @@ function getSidebarAssigneeAvailabilityData() {
);
}
-function mountAssigneesComponent(mediator) {
+function mountAssigneesComponentDeprecated(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
if (!el) return;
@@ -86,6 +80,51 @@ function mountAssigneesComponent(mediator) {
});
}
+function mountAssigneesComponent() {
+ const el = document.getElementById('js-vue-sidebar-assignees');
+
+ if (!el) return;
+
+ const { iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ apolloProvider,
+ components: {
+ SidebarAssigneesWidget,
+ },
+ provide: {
+ canUpdate: editable,
+ projectMembersPath,
+ directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
+ indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'),
+ },
+ render: (createElement) =>
+ createElement('sidebar-assignees-widget', {
+ props: {
+ iid: String(iid),
+ fullPath,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
+ multipleAssignees: !el.dataset.maxAssignees,
+ },
+ scopedSlots: {
+ collapsed: ({ users, onClick }) =>
+ createElement(CollapsedAssigneeList, {
+ props: {
+ users,
+ },
+ nativeOn: {
+ click: onClick,
+ },
+ }),
+ },
+ }),
+ });
+}
+
function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers');
@@ -168,6 +207,36 @@ function mountConfidentialComponent() {
});
}
+function mountDueDateComponent() {
+ const el = document.getElementById('js-due-date-entry-point');
+ if (!el) {
+ return;
+ }
+
+ const { fullPath, iid, editable } = getSidebarOptions();
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ apolloProvider,
+ components: {
+ SidebarDueDateWidget,
+ },
+ provide: {
+ iid: String(iid),
+ fullPath,
+ canUpdate: editable,
+ },
+
+ render: (createElement) =>
+ createElement('sidebar-due-date-widget', {
+ props: {
+ issuableType: IssuableType.Issue,
+ },
+ }),
+ });
+}
+
function mountReferenceComponent() {
const el = document.getElementById('js-reference-entry-point');
if (!el) {
@@ -342,9 +411,14 @@ function mountCopyEmailComponent() {
}
export function mountSidebar(mediator) {
- mountAssigneesComponent(mediator);
+ if (isInIssuePage() || isInDesignPage()) {
+ mountAssigneesComponent();
+ } else {
+ mountAssigneesComponentDeprecated(mediator);
+ }
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator);
+ mountDueDateComponent(mediator);
mountReferenceComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
diff --git a/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
new file mode 100644
index 00000000000..6d3f782bd0a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
@@ -0,0 +1,10 @@
+query issueDueDate($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: issue(iid: $iid) {
+ __typename
+ id
+ dueDate
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
new file mode 100644
index 00000000000..cf7eccd61c7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateIssueDueDate($input: UpdateIssueInput!) {
+ issuableSetDueDate: updateIssue(input: $input) {
+ issuable: issue {
+ id
+ dueDate
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index d53c829a48e..aeb698a3adb 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -45,6 +45,9 @@ export default {
activeAuthor() {
return this.authors.find((author) => author.username.toLowerCase() === this.currentValue);
},
+ activeAuthorAvatar() {
+ return this.avatarUrl(this.activeAuthor);
+ },
},
watch: {
active: {
@@ -74,6 +77,9 @@ export default {
this.loading = false;
});
},
+ avatarUrl(author) {
+ return author.avatarUrl || author.avatar_url;
+ },
searchAuthors: debounce(function debouncedSearch({ data }) {
this.fetchAuthorBySearchTerm(data);
}, DEBOUNCE_DELAY),
@@ -92,7 +98,7 @@ export default {
<gl-avatar
v-if="activeAuthor"
:size="16"
- :src="activeAuthor.avatar_url"
+ :src="activeAuthorAvatar"
shape="circle"
class="gl-mr-2"
/>
@@ -115,7 +121,7 @@ export default {
:value="author.username"
>
<div class="d-flex">
- <gl-avatar :size="32" :src="author.avatar_url" />
+ <gl-avatar :size="32" :src="avatarUrl(author)" />
<div>
<div>{{ author.name }}</div>
<div>@{{ author.username }}</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
index ef5f052527b..17904f20341 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
@@ -30,5 +30,8 @@ export default {
<gl-dropdown-form>
<slot name="items"></slot>
</gl-dropdown-form>
+ <template #footer>
+ <slot name="footer"></slot>
+ </template>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 459ea27e9cd..3885127fa8e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query issueParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
@@ -9,11 +10,13 @@ query issueParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
+ ...UserAvailability
}
}
assignees {
nodes {
...User
+ ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 43bd9f17e9a..63482873b69 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query getMrParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
@@ -7,11 +8,13 @@ query getMrParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
+ ...UserAvailability
}
}
assignees {
nodes {
...User
+ ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
index 8ee8de2cb5c..3f40c0368d7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
issuableSetAssignees: issueSetAssignees(
@@ -9,11 +10,13 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP
assignees {
nodes {
...User
+ ...UserAvailability
}
}
participants {
nodes {
...User
+ ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
index a0f15a07692..77140ea36d8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
mergeRequestSetAssignees(
@@ -9,11 +10,13 @@ mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!,
assignees {
nodes {
...User
+ ...UserAvailability
}
}
participants {
nodes {
...User
+ ...UserAvailability
}
}
}
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index bcfffc7b889..9f951e838c8 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -9,7 +9,7 @@ class Admin::ServicesController < Admin::ApplicationController
feature_category :integrations
def index
- @services = Service.find_or_create_templates.sort_by(&:title)
+ @activated_services = Service.for_template.active.sort_by(&:title)
@existing_instance_types = Service.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 6153cad693c..8ebc773bb25 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -389,7 +389,8 @@ module IssuablesHelper
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
createNoteEmail: issuable[:create_note_email],
- issuableType: issuable[:type]
+ issuableType: issuable[:type],
+ projectMembersPath: project_project_members_path(@project, sort: :access_level_desc)
}
end
diff --git a/app/models/bulk_imports/stage.rb b/app/models/bulk_imports/stage.rb
new file mode 100644
index 00000000000..050c2c76ce8
--- /dev/null
+++ b/app/models/bulk_imports/stage.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Stage
+ include Singleton
+
+ CONFIG = {
+ group: {
+ pipeline: BulkImports::Groups::Pipelines::GroupPipeline,
+ stage: 0
+ },
+ subgroups: {
+ pipeline: BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
+ stage: 1
+ },
+ members: {
+ pipeline: BulkImports::Groups::Pipelines::MembersPipeline,
+ stage: 1
+ },
+ labels: {
+ pipeline: BulkImports::Groups::Pipelines::LabelsPipeline,
+ stage: 1
+ },
+ milestones: {
+ pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
+ stage: 1
+ },
+ badges: {
+ pipeline: BulkImports::Groups::Pipelines::BadgesPipeline,
+ stage: 1
+ },
+ finisher: {
+ pipeline: BulkImports::Groups::Pipelines::EntityFinisher,
+ stage: 2
+ }
+ }.freeze
+
+ def self.pipelines
+ instance.pipelines
+ end
+
+ def self.pipeline_exists?(name)
+ pipelines.any? do |(_, pipeline)|
+ pipeline.to_s == name.to_s
+ end
+ end
+
+ def pipelines
+ @pipelines ||= config
+ .values
+ .sort_by { |entry| entry[:stage] }
+ .map do |entry|
+ [entry[:stage], entry[:pipeline]]
+ end
+ end
+
+ private
+
+ def config
+ @config ||= CONFIG
+ end
+ end
+end
+
+::BulkImports::Stage.prepend_if_ee('::EE::BulkImports::Stage')
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index cf84414f9cf..282ba9e19ac 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -20,6 +20,27 @@ class BulkImports::Tracker < ApplicationRecord
DEFAULT_PAGE_SIZE = 500
+ scope :next_pipeline_trackers_for, -> (entity_id) {
+ entity_scope = where(bulk_import_entity_id: entity_id)
+ next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)')
+
+ entity_scope.where(stage: next_stage_scope)
+ }
+
+ def self.stage_running?(entity_id, stage)
+ where(stage: stage, bulk_import_entity_id: entity_id)
+ .with_status(:created, :started)
+ .exists?
+ end
+
+ def pipeline_class
+ unless BulkImports::Stage.pipeline_exists?(pipeline_name)
+ raise NameError.new("'#{pipeline_name}' is not a valid BulkImport Pipeline")
+ end
+
+ pipeline_name.constantize
+ end
+
state_machine :status, initial: :created do
state :created, value: 0
state :started, value: 1
@@ -32,10 +53,6 @@ class BulkImports::Tracker < ApplicationRecord
end
event :finish do
- # When applying the concurrent model,
- # remove the created => finished transaction
- # https://gitlab.com/gitlab-org/gitlab/-/issues/323384
- transition created: :finished
transition started: :finished
transition failed: :failed
transition skipped: :skipped
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 478c7cd156f..1e44321e148 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -137,6 +137,14 @@ module Issuable
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
+ scope :includes_for_bulk_update, -> do
+ associations = %i[author assignees epic group labels metrics project source_project target_project].select do |association|
+ reflect_on_association(association)
+ end
+
+ includes(*associations)
+ end
+
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index ccb334343ff..d42417bb6c1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -39,11 +39,13 @@ module Milestoneable
private
def milestone_is_valid
- errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
+ errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && !milestone_available?
end
end
def milestone_available?
+ return true if milestone_id.blank?
+
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index d3d543edcd7..13e289716ef 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -15,7 +15,7 @@ module Issuable
set_update_params(type)
items = update_issuables(type, ids)
- response_success(payload: { count: items.count })
+ response_success(payload: { count: items.size })
rescue ArgumentError => e
response_error(e.message, 422)
end
@@ -59,10 +59,17 @@ module Issuable
def find_issuables(parent, model_class, ids)
if parent.is_a?(Project)
- model_class.id_in(ids).of_projects(parent)
+ projects = parent
elsif parent.is_a?(Group)
- model_class.id_in(ids).of_projects(parent.all_projects)
+ projects = parent.all_projects
+ else
+ return
end
+
+ model_class
+ .id_in(ids)
+ .of_projects(projects)
+ .includes_for_bulk_update
end
def response_success(message: nil, payload: nil)
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index c17ab5e08a7..4d9fa6d3d57 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -1,7 +1,9 @@
+= render "service_templates_deprecated_alert"
+
%h3.page-title
= @service.title
-%p #{@service.description} template.
+%p= @service.description
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
= render 'shared/service_settings', form: form, integration: @service
diff --git a/app/views/admin/services/_service_templates_deprecated_alert.html.haml b/app/views/admin/services/_service_templates_deprecated_alert.html.haml
new file mode 100644
index 00000000000..0cc44099049
--- /dev/null
+++ b/app/views/admin/services/_service_templates_deprecated_alert.html.haml
@@ -0,0 +1,8 @@
+- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
+- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
+
+.gl-alert.gl-alert-danger.gl-mt-5{ role: 'alert' }
+ = sprite_icon('error', css_class: 'gl-alert-icon gl-alert-icon-no-title')
+ %h4.gl-alert-title= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
+ .gl-alert-body
+ = html_escape_once(s_("AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}.")).html_safe % { settings_link_start: settings_link_start, doc_link_start: doc_link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index a1b485e888d..91706452402 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -1,23 +1,13 @@
- page_title _("Service Templates")
- @content_class = 'limit-container-width' unless fluid_layout
-- if show_service_templates_deprecated?
- .gl-alert.gl-alert-tip.js-service-templates-deprecated.gl-mt-5{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED, dismiss_endpoint: user_callouts_path } }
- = sprite_icon('bulb', css_class: 'gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', aria: { label: _('Dismiss') } }
- = sprite_icon('close')
- %h4.gl-alert-title= s_('AdminSettings|Service Templates will soon be deprecated.')
- .gl-alert-body
- = s_('AdminSettings|Try using the latest version of Integrations instead.')
- .gl-alert-actions
- = link_to _('Go to Integrations'), integrations_admin_application_settings_path, class: 'btn btn-info gl-alert-action gl-button'
- = link_to _('Learn more'), help_page_path('user/admin_area/settings/project_integration_management'), class: 'btn btn-default gl-alert-action btn-secondary gl-button', target: '_blank', rel: 'noopener noreferrer'
+= render "service_templates_deprecated_alert"
-%h3.page-title Service templates
-%p.light= s_('AdminSettings|Service template allows you to set default values for integrations')
+- if @activated_services.any?
+ %h3.page-title Service templates
+ %p= s_('AdminSettings|Service template allows you to set default values for integrations')
-.table-holder
- %table.table
+ %table.table.b-table.gl-table
%colgroup
%col
%col
@@ -29,7 +19,7 @@
%th= _('Service')
%th= _('Description')
%th= _('Last edit')
- - @services.each do |service|
+ - @activated_services.each do |service|
- if service.type.in?(@existing_instance_types)
%tr
%td
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b555e73c808..fb2019bef15 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -70,41 +70,7 @@
= _('Time tracking')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
- if issuable_sidebar.has_key?(:due_date)
- .block.due_date
- .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
- = sprite_icon('calendar')
- %span.js-due-date-sidebar-value
- = issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
- .title.hide-collapsed
- = _('Due date')
- = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
- .value.hide-collapsed
- %span.value-content
- - if issuable_sidebar[:due_date]
- %span.bold= issuable_sidebar[:due_date].to_s(:medium)
- - else
- %span.no-value
- = _('None')
- - if can_edit_issuable
- %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
- \-
- %a.js-remove-due-date{ href: "#", role: "button" }
- = _('remove due date')
- - if can_edit_issuable
- .selectbox.hide-collapsed
- = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
- .dropdown
- %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')
- = 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
- .js-due-date-calendar
-
+ #js-due-date-entry-point
.js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 0f1b77eea6e..47e7ff0e4bc 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,6 +1,7 @@
- issuable_type = issuable_sidebar[:type]
+- dropdown_options = assignees_dropdown_options(issuable_type)
-#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in } }
+#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: directly_invite_members?, indirectly_invite_members: indirectly_invite_members? } }
.title.hide-collapsed
= _('Assignee')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
@@ -29,7 +30,6 @@
null_user: true,
display: 'static' } }
- - dropdown_options = assignees_dropdown_options(issuable_type)
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable_type}[assignee_ids][]" }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 00f33eb08b8..ff3d3740ed6 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1539,6 +1539,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: bulk_imports_pipeline
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: chat_notification
:feature_category: :chatops
:has_external_dependencies: true
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index e6bc54895a7..b4b9d9b05c1 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -21,9 +21,11 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
@bulk_import.start! if @bulk_import.created?
created_entities.first(next_batch_size).each do |entity|
- entity.start!
+ create_pipeline_tracker_for(entity)
BulkImports::EntityWorker.perform_async(entity.id)
+
+ entity.start!
end
re_enqueue
@@ -65,4 +67,13 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
def re_enqueue
BulkImportWorker.perform_in(PERFORM_DELAY, @bulk_import.id)
end
+
+ def create_pipeline_tracker_for(entity)
+ BulkImports::Stage.pipelines.each do |stage, pipeline|
+ entity.trackers.create!(
+ stage: stage,
+ pipeline_name: pipeline
+ )
+ end
+ end
end
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index 5b41ccbdea1..7f173b738cf 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -10,24 +10,47 @@ module BulkImports
worker_has_external_dependencies!
- def perform(entity_id)
- entity = BulkImports::Entity.with_status(:started).find_by_id(entity_id)
+ def perform(entity_id, current_stage = nil)
+ return if stage_running?(entity_id, current_stage)
+
+ logger.info(
+ worker: self.class.name,
+ entity_id: entity_id,
+ current_stage: current_stage
+ )
+
+ next_pipeline_trackers_for(entity_id).each do |pipeline_tracker|
+ BulkImports::PipelineWorker.perform_async(
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity_id
+ )
+ end
+ rescue => e
+ logger.error(
+ worker: self.class.name,
+ entity_id: entity_id,
+ current_stage: current_stage,
+ error_message: e.message
+ )
+
+ Gitlab::ErrorTracking.track_exception(e, entity_id: entity_id)
+ end
- if entity
- entity.update!(jid: jid)
+ private
- BulkImports::Importers::GroupImporter.new(entity).execute
- end
+ def stage_running?(entity_id, stage)
+ return unless stage
- rescue => e
- extra = {
- bulk_import_id: entity&.bulk_import&.id,
- entity_id: entity&.id
- }
+ BulkImports::Tracker.stage_running?(entity_id, stage)
+ end
- Gitlab::ErrorTracking.track_exception(e, extra)
+ def next_pipeline_trackers_for(entity_id)
+ BulkImports::Tracker.next_pipeline_trackers_for(entity_id)
+ end
- entity&.fail_op
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
new file mode 100644
index 00000000000..a6de3c36205
--- /dev/null
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ feature_category :importers
+
+ sidekiq_options retry: false, dead: false
+
+ worker_has_external_dependencies!
+
+ def perform(pipeline_tracker_id, stage, entity_id)
+ pipeline_tracker = ::BulkImports::Tracker
+ .with_status(:created)
+ .find_by_id(pipeline_tracker_id)
+
+ if pipeline_tracker.present?
+ logger.info(
+ worker: self.class.name,
+ entity_id: pipeline_tracker.entity.id,
+ pipeline_name: pipeline_tracker.pipeline_name
+ )
+
+ run(pipeline_tracker)
+ else
+ logger.error(
+ worker: self.class.name,
+ entity_id: entity_id,
+ pipeline_tracker_id: pipeline_tracker_id,
+ message: 'Unstarted pipeline not found'
+ )
+ end
+
+ ensure
+ ::BulkImports::EntityWorker.perform_async(entity_id, stage)
+ end
+
+ private
+
+ def run(pipeline_tracker)
+ pipeline_tracker.update!(status_event: 'start', jid: jid)
+
+ context = ::BulkImports::Pipeline::Context.new(pipeline_tracker)
+
+ pipeline_tracker.pipeline_class.new(context).run
+
+ pipeline_tracker.finish!
+ rescue => e
+ pipeline_tracker.fail_op!
+
+ logger.error(
+ worker: self.class.name,
+ entity_id: pipeline_tracker.entity.id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ message: e.message
+ )
+
+ Gitlab::ErrorTracking.track_exception(
+ e,
+ entity_id: pipeline_tracker.entity.id,
+ pipeline_name: pipeline_tracker.pipeline_name
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+ end
+end