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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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/assets
parentfcbd3db20f5dfb13ae33ddfee98be8d92cade72f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-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
32 files changed, 710 insertions, 302 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
}
}
}