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:
Diffstat (limited to 'app/assets/javascripts/service_desk')
-rw-r--r--app/assets/javascripts/service_desk/components/info_banner.vue64
-rw-r--r--app/assets/javascripts/service_desk/components/service_desk_list_app.vue151
-rw-r--r--app/assets/javascripts/service_desk/constants.js17
-rw-r--r--app/assets/javascripts/service_desk/graphql.js24
-rw-r--r--app/assets/javascripts/service_desk/index.js55
-rw-r--r--app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql72
-rw-r--r--app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql91
-rw-r--r--app/assets/javascripts/service_desk/queries/issue.fragment.graphql60
8 files changed, 534 insertions, 0 deletions
diff --git a/app/assets/javascripts/service_desk/components/info_banner.vue b/app/assets/javascripts/service_desk/components/info_banner.vue
new file mode 100644
index 00000000000..8aaced839a5
--- /dev/null
+++ b/app/assets/javascripts/service_desk/components/info_banner.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlLink, GlButton } from '@gitlab/ui';
+import {
+ infoBannerTitle,
+ infoBannerAdminNote,
+ infoBannerUserNote,
+ enableServiceDesk,
+ learnMore,
+} from '../constants';
+
+export default {
+ name: 'InfoBanner',
+ components: {
+ GlLink,
+ GlButton,
+ },
+ inject: [
+ 'serviceDeskCalloutSvgPath',
+ 'serviceDeskEmailAddress',
+ 'canAdminIssues',
+ 'canEditProjectSettings',
+ 'serviceDeskSettingsPath',
+ 'isServiceDeskEnabled',
+ 'serviceDeskHelpPath',
+ ],
+ i18n: { infoBannerTitle, infoBannerAdminNote, infoBannerUserNote, enableServiceDesk, learnMore },
+ computed: {
+ canSeeEmailAddress() {
+ return this.canAdminIssues && this.isServiceDeskEnabled;
+ },
+ canEnableServiceDesk() {
+ return this.canEditProjectSettings && !this.isServiceDeskEnabled;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-border-b gl-pb-3 gl-display-flex gl-align-items-flex-start">
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
+ <img
+ :src="serviceDeskCalloutSvgPath"
+ alt=""
+ class="gl-display-none gl-sm-display-block gl-p-5"
+ />
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ <div class="gl-mt-3 gl-ml-3">
+ <h5>{{ $options.i18n.infoBannerTitle }}</h5>
+ <p v-if="canSeeEmailAddress">
+ {{ $options.i18n.infoBannerAdminNote }} <code>{{ serviceDeskEmailAddress }}</code>
+ </p>
+ <p>
+ {{ $options.i18n.infoBannerUserNote }}
+ <gl-link :href="serviceDeskHelpPath" target="_blank">{{ $options.i18n.learnMore }}</gl-link
+ >.
+ </p>
+ <p v-if="canEnableServiceDesk" class="gl-mt-3">
+ <gl-button :href="serviceDeskSettingsPath" variant="confirm">{{
+ $options.i18n.enableServiceDesk
+ }}</gl-button>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
new file mode 100644
index 00000000000..e8b05642e7d
--- /dev/null
+++ b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
@@ -0,0 +1,151 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { fetchPolicies } from '~/lib/graphql';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
+import { STATUS_OPEN, STATUS_CLOSED, STATUS_ALL } from '~/issues/constants';
+import getServiceDeskIssuesQuery from '../queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCounts from '../queries/get_service_desk_issues_counts.query.graphql';
+import {
+ errorFetchingCounts,
+ errorFetchingIssues,
+ noSearchNoFilterTitle,
+ searchPlaceholder,
+ SERVICE_DESK_BOT_USERNAME,
+} from '../constants';
+import InfoBanner from './info_banner.vue';
+
+export default {
+ i18n: {
+ errorFetchingCounts,
+ errorFetchingIssues,
+ noSearchNoFilterTitle,
+ searchPlaceholder,
+ },
+ issuableListTabs,
+ components: {
+ GlEmptyState,
+ IssuableList,
+ InfoBanner,
+ },
+ inject: [
+ 'emptyStateSvgPath',
+ 'isProject',
+ 'isSignedIn',
+ 'fullPath',
+ 'isServiceDeskSupported',
+ 'hasAnyIssues',
+ ],
+ data() {
+ return {
+ serviceDeskIssues: [],
+ serviceDeskIssuesCounts: {},
+ searchTokens: [],
+ sortOptions: [],
+ state: STATUS_OPEN,
+ issuesError: null,
+ };
+ },
+ apollo: {
+ serviceDeskIssues: {
+ query: getServiceDeskIssuesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.project.issues.nodes ?? [];
+ },
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ // We need this for handling loading state when using frontend cache
+ // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106004#note_1217325202 for details
+ notifyOnNetworkStatusChange: true,
+ result({ data }) {
+ if (!data) {
+ return;
+ }
+ this.pageInfo = data?.project.issues.pageInfo ?? {};
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingIssues;
+ Sentry.captureException(error);
+ },
+ skip() {
+ return !this.hasAnyIssues;
+ },
+ },
+ serviceDeskIssuesCounts: {
+ query: getServiceDeskIssuesCounts,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data?.project ?? {};
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingCounts;
+ Sentry.captureException(error);
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ },
+ },
+ computed: {
+ queryVariables() {
+ return {
+ fullPath: this.fullPath,
+ isProject: this.isProject,
+ isSignedIn: this.isSignedIn,
+ authorUsername: SERVICE_DESK_BOT_USERNAME,
+ state: this.state,
+ };
+ },
+ tabCounts() {
+ const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts;
+ return {
+ [STATUS_OPEN]: openedIssues?.count,
+ [STATUS_CLOSED]: closedIssues?.count,
+ [STATUS_ALL]: allIssues?.count,
+ };
+ },
+ isInfoBannerVisible() {
+ return this.isServiceDeskSupported && this.hasAnyIssues;
+ },
+ },
+ methods: {
+ handleClickTab(state) {
+ if (this.state === state) {
+ return;
+ }
+ this.state = state;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <info-banner v-if="isInfoBannerVisible" />
+ <issuable-list
+ namespace="service-desk"
+ recent-searches-storage-key="issues"
+ :error="issuesError"
+ :search-input-placeholder="$options.i18n.searchPlaceholder"
+ :search-tokens="searchTokens"
+ :sort-options="sortOptions"
+ :issuables="serviceDeskIssues"
+ :tabs="$options.issuableListTabs"
+ :tab-counts="tabCounts"
+ :current-tab="state"
+ @click-tab="handleClickTab"
+ >
+ <template #empty-state>
+ <gl-empty-state
+ :svg-path="emptyStateSvgPath"
+ :title="$options.i18n.noSearchNoFilterTitle"
+ />
+ </template>
+ </issuable-list>
+ </section>
+</template>
diff --git a/app/assets/javascripts/service_desk/constants.js b/app/assets/javascripts/service_desk/constants.js
new file mode 100644
index 00000000000..685ad738792
--- /dev/null
+++ b/app/assets/javascripts/service_desk/constants.js
@@ -0,0 +1,17 @@
+import { __, s__ } from '~/locale';
+
+export const SERVICE_DESK_BOT_USERNAME = 'support-bot';
+
+export const errorFetchingCounts = __('An error occurred while getting issue counts');
+export const errorFetchingIssues = __('An error occurred while loading issues');
+export const noSearchNoFilterTitle = __('Please select at least one filter to see results');
+export const searchPlaceholder = __('Search or filter results...');
+export const infoBannerTitle = s__(
+ 'ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab',
+);
+export const infoBannerAdminNote = s__('ServiceDesk|Your users can send emails to this address:');
+export const infoBannerUserNote = s__(
+ 'ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation.',
+);
+export const enableServiceDesk = s__('ServiceDesk|Enable Service Desk');
+export const learnMore = __('Learn more');
diff --git a/app/assets/javascripts/service_desk/graphql.js b/app/assets/javascripts/service_desk/graphql.js
new file mode 100644
index 00000000000..e01973f1e8a
--- /dev/null
+++ b/app/assets/javascripts/service_desk/graphql.js
@@ -0,0 +1,24 @@
+import createDefaultClient, { createApolloClientWithCaching } from '~/lib/graphql';
+
+let client;
+
+const typePolicies = {
+ Project: {
+ fields: {
+ issues: {
+ merge: true,
+ },
+ },
+ },
+};
+
+export async function gqlClient() {
+ if (client) return client;
+ client = gon.features?.frontendCaching
+ ? await createApolloClientWithCaching(
+ {},
+ { localCacheKey: 'service_desk_list', cacheConfig: { typePolicies } },
+ )
+ : createDefaultClient({}, { cacheConfig: { typePolicies } });
+ return client;
+}
diff --git a/app/assets/javascripts/service_desk/index.js b/app/assets/javascripts/service_desk/index.js
new file mode 100644
index 00000000000..a9172f96540
--- /dev/null
+++ b/app/assets/javascripts/service_desk/index.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { gqlClient } from './graphql';
+import ServiceDeskListApp from './components/service_desk_list_app.vue';
+
+export async function mountServiceDeskListApp() {
+ const el = document.querySelector('.js-service-desk-list');
+
+ if (!el) {
+ return null;
+ }
+
+ const {
+ projectDataEmptyStateSvgPath,
+ projectDataFullPath,
+ projectDataIsProject,
+ projectDataIsSignedIn,
+ projectDataHasAnyIssues,
+ serviceDeskEmailAddress,
+ canAdminIssues,
+ canEditProjectSettings,
+ serviceDeskCalloutSvgPath,
+ serviceDeskSettingsPath,
+ serviceDeskHelpPath,
+ isServiceDeskSupported,
+ isServiceDeskEnabled,
+ } = el.dataset;
+
+ Vue.use(VueApollo);
+
+ return new Vue({
+ el,
+ name: 'ServiceDeskListRoot',
+ apolloProvider: new VueApollo({
+ defaultClient: await gqlClient(),
+ }),
+ provide: {
+ emptyStateSvgPath: projectDataEmptyStateSvgPath,
+ fullPath: projectDataFullPath,
+ isProject: parseBoolean(projectDataIsProject),
+ isSignedIn: parseBoolean(projectDataIsSignedIn),
+ serviceDeskEmailAddress,
+ canAdminIssues: parseBoolean(canAdminIssues),
+ canEditProjectSettings: parseBoolean(canEditProjectSettings),
+ serviceDeskCalloutSvgPath,
+ serviceDeskSettingsPath,
+ serviceDeskHelpPath,
+ isServiceDeskSupported: parseBoolean(isServiceDeskSupported),
+ isServiceDeskEnabled: parseBoolean(isServiceDeskEnabled),
+ hasAnyIssues: parseBoolean(projectDataHasAnyIssues),
+ },
+ render: (createComponent) => createComponent(ServiceDeskListApp),
+ });
+}
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql b/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql
new file mode 100644
index 00000000000..c678b8dd8ab
--- /dev/null
+++ b/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql
@@ -0,0 +1,72 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "./issue.fragment.graphql"
+
+query getServiceDeskIssues(
+ $hideUsers: Boolean = false
+ $isProject: Boolean = false
+ $isSignedIn: Boolean = false
+ $fullPath: ID!
+ $iid: String
+ $search: String
+ $sort: IssueSort
+ $state: IssuableState
+ $in: [IssuableSearchableField!]
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $confidential: Boolean
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
+ $types: [IssueType!]
+ $crmContactId: String
+ $crmOrganizationId: String
+ $not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
+ $beforeCursor: String
+ $afterCursor: String
+ $firstPageSize: Int
+ $lastPageSize: Int
+) {
+ project(fullPath: $fullPath) @include(if: $isProject) @persist {
+ id
+ issues(
+ iid: $iid
+ search: $search
+ sort: $sort
+ state: $state
+ in: $in
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
+ not: $not
+ or: $or
+ before: $beforeCursor
+ after: $afterCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ __persist
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ __persist
+ ...IssueFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql b/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql
new file mode 100644
index 00000000000..c2ba397d76f
--- /dev/null
+++ b/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql
@@ -0,0 +1,91 @@
+query getServiceDeskIssuesCount(
+ $isProject: Boolean = false
+ $fullPath: ID!
+ $iid: String
+ $search: String
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $confidential: Boolean
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
+ $types: [IssueType!]
+ $crmContactId: String
+ $crmOrganizationId: String
+ $not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
+) {
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ id
+ openedIssues: issues(
+ state: opened
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
+ not: $not
+ or: $or
+ ) {
+ count
+ }
+ closedIssues: issues(
+ state: closed
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
+ not: $not
+ or: $or
+ ) {
+ count
+ }
+ allIssues: issues(
+ state: all
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
+ not: $not
+ or: $or
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/service_desk/queries/issue.fragment.graphql b/app/assets/javascripts/service_desk/queries/issue.fragment.graphql
new file mode 100644
index 00000000000..3b49c0efb14
--- /dev/null
+++ b/app/assets/javascripts/service_desk/queries/issue.fragment.graphql
@@ -0,0 +1,60 @@
+fragment IssueFragment on Issue {
+ id
+ iid
+ confidential
+ createdAt
+ downvotes
+ dueDate
+ hidden
+ humanTimeEstimate
+ mergeRequestsCount
+ moved
+ state
+ title
+ updatedAt
+ closedAt
+ upvotes
+ userDiscussionsCount @include(if: $isSignedIn)
+ webPath
+ webUrl
+ type
+ assignees @skip(if: $hideUsers) {
+ nodes {
+ __persist
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ }
+ author @skip(if: $hideUsers) {
+ __persist
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ labels {
+ nodes {
+ __persist
+ id
+ color
+ title
+ description
+ }
+ }
+ milestone {
+ __persist
+ id
+ dueDate
+ startDate
+ webPath
+ title
+ }
+ taskCompletionStatus {
+ completedCount
+ count
+ }
+}