diff options
Diffstat (limited to 'app/assets/javascripts/sidebar')
10 files changed, 146 insertions, 36 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index f16b16a6837..3baf4bf0742 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -1,6 +1,6 @@ <script> -import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue'; -import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue'; +import CollapsedAssigneeList from './collapsed_assignee_list.vue'; +import UncollapsedAssigneeList from './uncollapsed_assignee_list.vue'; export default { // name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue new file mode 100644 index 00000000000..bf0c52b2341 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue @@ -0,0 +1,75 @@ +<script> +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql'; +import actionCable from '~/actioncable_consumer'; + +export default { + subscription: null, + name: 'AssigneesRealtime', + props: { + mediator: { + type: Object, + required: true, + }, + issuableIid: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + }, + apollo: { + project: { + query, + variables() { + return { + iid: this.issuableIid, + fullPath: this.projectPath, + }; + }, + result(data) { + this.handleFetchResult(data); + }, + }, + }, + mounted() { + this.initActionCablePolling(); + }, + beforeDestroy() { + this.$options.subscription.unsubscribe(); + }, + methods: { + received(data) { + if (data.event === 'updated') { + this.$apollo.queries.project.refetch(); + } + }, + initActionCablePolling() { + this.$options.subscription = actionCable.subscriptions.create( + { + channel: 'IssuesChannel', + project_path: this.projectPath, + iid: this.issuableIid, + }, + { received: this.received }, + ); + }, + handleFetchResult({ data }) { + const { nodes } = data.project.issue.assignees; + + const assignees = nodes.map(n => ({ + ...n, + avatar_url: n.avatarUrl, + id: getIdFromGraphQLId(n.id), + })); + + this.mediator.store.setAssigneesFromRealtime(assignees); + }, + }, + render() { + return this.$slots.default; + }, +}; +</script> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index ce592720531..0906d5abec3 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -3,8 +3,10 @@ import Flash from '~/flash'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import AssigneeTitle from './assignee_title.vue'; import Assignees from './assignees.vue'; +import AssigneesRealtime from './assignees_realtime.vue'; import { __ } from '~/locale'; export default { @@ -12,7 +14,9 @@ export default { components: { AssigneeTitle, Assignees, + AssigneesRealtime, }, + mixins: [glFeatureFlagsMixin()], props: { mediator: { type: Object, @@ -32,6 +36,14 @@ export default { required: false, default: 'issue', }, + issuableIid: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, }, data() { return { @@ -39,6 +51,12 @@ export default { loading: false, }; }, + 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 === 'issue'; + }, + }, created() { this.removeAssignee = this.store.removeAssignee.bind(this.store); this.addAssignee = this.store.addAssignee.bind(this.store); @@ -84,6 +102,12 @@ export default { <template> <div> + <assignees-realtime + v-if="shouldEnableRealtime" + :issuable-iid="issuableIid" + :project-path="projectPath" + :mediator="mediator" + /> <assignee-title :number-of-assignees="store.assignees.length" :loading="loading || store.isFetching.assignees" 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 3d112bba668..fed9e5886c0 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -66,7 +66,7 @@ export default { <template> <assignee-avatar-link v-if="hasOneUser" - v-slot="{ user }" + #default="{ user }" tooltip-placement="left" :tooltip-has-name="false" :user="firstUser" diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 5b3c3642290..550a1be1e64 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,15 +1,16 @@ <script> +import { mapState } from 'vuex'; import { __ } from '~/locale'; import Flash from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; -import editForm from './edit_form.vue'; +import EditForm from './edit_form.vue'; import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor'; export default { components: { - editForm, + EditForm, Icon, }, directives: { @@ -17,10 +18,6 @@ export default { }, mixins: [recaptchaModalImplementor], props: { - isConfidential: { - required: true, - type: Boolean, - }, isEditable: { required: true, type: Boolean, @@ -36,11 +33,12 @@ export default { }; }, computed: { + ...mapState({ confidential: ({ noteableData }) => noteableData.confidential }), confidentialityIcon() { - return this.isConfidential ? 'eye-slash' : 'eye'; + return this.confidential ? 'eye-slash' : 'eye'; }, tooltipLabel() { - return this.isConfidential ? __('Confidential') : __('Not confidential'); + return this.confidential ? __('Confidential') : __('Not confidential'); }, }, created() { @@ -95,17 +93,16 @@ export default { data-track-label="right_sidebar" data-track-property="confidentiality" @click.prevent="toggleForm" + >{{ __('Edit') }}</a > - {{ __('Edit') }} - </a> </div> <div class="value sidebar-item-value hide-collapsed"> - <editForm + <edit-form v-if="edit" - :is-confidential="isConfidential" + :is-confidential="confidential" :update-confidential-attribute="updateConfidentialAttribute" /> - <div v-if="!isConfidential" class="no-value sidebar-item-value"> + <div v-if="!confidential" class="no-value sidebar-item-value"> <icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" /> {{ __('Not confidential') }} </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 5d0e39e8195..e106afea9f5 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -40,7 +40,12 @@ export default { <button type="button" class="btn btn-default append-right-10" @click="closeForm"> {{ __('Cancel') }} </button> - <button type="button" class="btn btn-close" @click.prevent="submitForm"> + <button + type="button" + class="btn btn-close" + data-testid="confidential-toggle" + @click.prevent="submitForm" + > {{ toggleButtonText }} </button> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index db2e51c3aca..d2904f4157c 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -95,22 +95,18 @@ export default { @click="onClickCollapsedIcon" > <i class="fa fa-users" aria-hidden="true"> </i> - <gl-loading-icon v-if="loading" class="js-participants-collapsed-loading-icon" /> - <span v-else class="js-participants-collapsed-count"> {{ participantCount }} </span> + <gl-loading-icon v-if="loading" /> + <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> </div> <div v-if="showParticipantLabel" class="title hide-collapsed"> - <gl-loading-icon - v-if="loading" - :inline="true" - class="js-participants-expanded-loading-icon" - /> + <gl-loading-icon v-if="loading" :inline="true" /> {{ participantLabel }} </div> <div class="participants-list hide-collapsed"> <div v-for="participant in visibleParticipants" :key="participant.id" - class="participants-author js-participants-author" + class="participants-author" > <a :href="participant.web_url" class="author-link"> <user-avatar-image @@ -125,11 +121,7 @@ export default { </div> </div> <div v-if="hasMoreParticipants" class="participants-more hide-collapsed"> - <button - type="button" - class="btn-transparent btn-link js-toggle-participants-button" - @click="toggleMoreParticipants" - > + <button type="button" class="btn-transparent btn-link" @click="toggleMoreParticipants"> {{ toggleLabel }} </button> </div> diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 2a61f7b5c05..0fb9cf22653 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import '~/gl_dropdown'; -import { escape as esc } from 'lodash'; +import { escape } from 'lodash'; import { __ } from '~/locale'; function isValidProjectId(id) { @@ -49,7 +49,7 @@ class SidebarMoveIssue { renderRow: project => ` <li> <a href="#" class="js-move-issue-dropdown-item"> - ${esc(project.name_with_namespace)} + ${escape(project.name_with_namespace)} </a> </li> `, diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 6f8214b18ee..e371091fc53 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; @@ -8,17 +9,29 @@ import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; import sidebarParticipants from './components/participants/sidebar_participants.vue'; import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; import Translate from '../vue_shared/translate'; +import createDefaultClient from '~/lib/graphql'; +import { store } from '~/notes/stores'; Vue.use(Translate); +Vue.use(VueApollo); + +function getSidebarOptions() { + return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); +} function mountAssigneesComponent(mediator) { const el = document.getElementById('js-vue-sidebar-assignees'); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); if (!el) return; + const { iid, fullPath } = getSidebarOptions(); // eslint-disable-next-line no-new new Vue({ el, + apolloProvider, components: { SidebarAssignees, }, @@ -26,6 +39,8 @@ function mountAssigneesComponent(mediator) { createElement('sidebar-assignees', { props: { mediator, + issuableIid: String(iid), + projectPath: fullPath, field: el.dataset.field, signedIn: el.hasAttribute('data-signed-in'), issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', @@ -45,8 +60,8 @@ function mountConfidentialComponent(mediator) { const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar); new ConfidentialComp({ + store, propsData: { - isConfidential: initialData.is_confidential, isEditable: initialData.is_editable, service: mediator.service, }, @@ -144,6 +159,4 @@ export function mountSidebar(mediator) { mountTimeTrackingComponent(); } -export function getSidebarOptions() { - return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); -} +export { getSidebarOptions }; diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 66f7f9e3c66..095f93b72a9 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -89,6 +89,10 @@ export default class SidebarStore { this.assignees = []; } + setAssigneesFromRealtime(data) { + this.assignees = data; + } + setAutocompleteProjects(projects) { this.autocompleteProjects = projects; } |