summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issues/new
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues/new')
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions.vue94
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions_item.vue132
-rw-r--r--app/assets/javascripts/issues/new/components/type_popover.vue41
-rw-r--r--app/assets/javascripts/issues/new/index.js56
-rw-r--r--app/assets/javascripts/issues/new/queries/issues.query.graphql29
5 files changed, 352 insertions, 0 deletions
diff --git a/app/assets/javascripts/issues/new/components/title_suggestions.vue b/app/assets/javascripts/issues/new/components/title_suggestions.vue
new file mode 100644
index 00000000000..0a9cdb12519
--- /dev/null
+++ b/app/assets/javascripts/issues/new/components/title_suggestions.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import query from '../queries/issues.query.graphql';
+import TitleSuggestionsItem from './title_suggestions_item.vue';
+
+export default {
+ components: {
+ GlIcon,
+ TitleSuggestionsItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ search: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ issues: {
+ query,
+ debounce: 1000,
+ skip() {
+ return this.isSearchEmpty;
+ },
+ update: (data) => data.project.issues.edges.map(({ node }) => node),
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ search: this.search,
+ };
+ },
+ },
+ },
+ data() {
+ return {
+ issues: [],
+ loading: 0,
+ };
+ },
+ computed: {
+ isSearchEmpty() {
+ return !this.search.length;
+ },
+ showSuggestions() {
+ return !this.isSearchEmpty && this.issues.length && !this.loading;
+ },
+ },
+ watch: {
+ search() {
+ if (this.isSearchEmpty) {
+ this.issues = [];
+ }
+ },
+ },
+ helpText: __(
+ 'These existing issues have a similar title. It might be better to comment there instead of creating another similar issue.',
+ ),
+};
+</script>
+
+<template>
+ <div v-show="showSuggestions" class="form-group row">
+ <div v-once class="col-form-label col-sm-2 pt-0">
+ {{ __('Similar issues') }}
+ <gl-icon
+ v-gl-tooltip.bottom
+ :title="$options.helpText"
+ :aria-label="$options.helpText"
+ name="question-o"
+ class="text-secondary gl-cursor-help"
+ />
+ </div>
+ <div class="col-sm-10">
+ <ul class="list-unstyled m-0">
+ <li
+ v-for="(suggestion, index) in issues"
+ :key="suggestion.id"
+ :class="{
+ 'gl-mb-3': index !== issues.length - 1,
+ }"
+ >
+ <title-suggestions-item :suggestion="suggestion" />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/new/components/title_suggestions_item.vue b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
new file mode 100644
index 00000000000..a01f4f747b9
--- /dev/null
+++ b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
@@ -0,0 +1,132 @@
+<script>
+import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import timeago from '~/vue_shared/mixins/timeago';
+
+export default {
+ components: {
+ GlTooltip,
+ GlLink,
+ GlIcon,
+ UserAvatarImage,
+ TimeagoTooltip,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeago],
+ props: {
+ suggestion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ counts() {
+ return [
+ {
+ id: uniqueId(),
+ icon: 'thumb-up',
+ tooltipTitle: __('Upvotes'),
+ count: this.suggestion.upvotes,
+ },
+ {
+ id: uniqueId(),
+ icon: 'comment',
+ tooltipTitle: __('Comments'),
+ count: this.suggestion.userNotesCount,
+ },
+ ].filter(({ count }) => count);
+ },
+ isClosed() {
+ return this.suggestion.state === 'closed';
+ },
+ stateIconClass() {
+ return this.isClosed ? 'gl-text-blue-500' : 'gl-text-green-500';
+ },
+ stateIconName() {
+ return this.isClosed ? 'issue-close' : 'issue-open-m';
+ },
+ stateTitle() {
+ return this.isClosed ? __('Closed') : __('Opened');
+ },
+ closedOrCreatedDate() {
+ return this.suggestion.closedAt || this.suggestion.createdAt;
+ },
+ hasUpdated() {
+ return this.suggestion.updatedAt !== this.suggestion.createdAt;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="suggestion-item">
+ <div class="d-flex align-items-center">
+ <gl-icon
+ v-if="suggestion.confidential"
+ v-gl-tooltip.bottom
+ :title="__('Confidential')"
+ name="eye-slash"
+ class="gl-cursor-help gl-mr-2 gl-text-orange-500"
+ />
+ <gl-link
+ :href="suggestion.webUrl"
+ target="_blank"
+ class="suggestion bold str-truncated-100 gl-text-gray-900!"
+ >
+ {{ suggestion.title }}
+ </gl-link>
+ </div>
+ <div class="text-secondary suggestion-footer">
+ <gl-icon ref="state" :name="stateIconName" :class="stateIconClass" class="gl-cursor-help" />
+ <gl-tooltip :target="() => $refs.state" placement="bottom">
+ <span class="d-block">
+ <span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
+ </span>
+ <span class="text-tertiary">{{ tooltipTitle(closedOrCreatedDate) }}</span>
+ </gl-tooltip>
+ #{{ suggestion.iid }} &bull;
+ <timeago-tooltip
+ :time="suggestion.createdAt"
+ tooltip-placement="bottom"
+ class="gl-cursor-help"
+ />
+ {{ __('by') }}
+ <gl-link :href="suggestion.author.webUrl">
+ <user-avatar-image
+ :img-src="suggestion.author.avatarUrl"
+ :size="16"
+ css-classes="mr-0 float-none"
+ tooltip-placement="bottom"
+ class="d-inline-block"
+ >
+ <span class="bold d-block">{{ __('Author') }}</span> {{ suggestion.author.name }}
+ <span class="text-tertiary">@{{ suggestion.author.username }}</span>
+ </user-avatar-image>
+ </gl-link>
+ <template v-if="hasUpdated">
+ &bull; {{ __('updated') }}
+ <timeago-tooltip
+ :time="suggestion.updatedAt"
+ tooltip-placement="bottom"
+ class="gl-cursor-help"
+ />
+ </template>
+ <span class="suggestion-counts">
+ <span
+ v-for="{ count, icon, tooltipTitle, id } in counts"
+ :key="id"
+ v-gl-tooltip.bottom
+ :title="tooltipTitle"
+ class="gl-cursor-help gl-ml-3 text-tertiary"
+ >
+ <gl-icon :name="icon" /> {{ count }}
+ </span>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/new/components/type_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue
new file mode 100644
index 00000000000..a70e79b70f9
--- /dev/null
+++ b/app/assets/javascripts/issues/new/components/type_popover.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlIcon, GlPopover } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ issueTypes: __('Issue types'),
+ issue: __('Issue'),
+ incident: __('Incident'),
+ issueHelpText: __('For general work'),
+ incidentHelpText: __('For investigating IT service disruptions or outages'),
+ },
+ components: {
+ GlIcon,
+ GlPopover,
+ },
+};
+</script>
+
+<template>
+ <span id="popovercontainer">
+ <gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
+ <gl-popover
+ target="issue-type-info"
+ container="popovercontainer"
+ :title="$options.i18n.issueTypes"
+ triggers="focus hover"
+ >
+ <ul class="gl-list-style-none gl-p-0 gl-m-0">
+ <li class="gl-mb-3">
+ <div class="gl-font-weight-bold">{{ $options.i18n.issue }}</div>
+ <span>{{ $options.i18n.issueHelpText }}</span>
+ </li>
+ <li>
+ <div class="gl-font-weight-bold">{{ $options.i18n.incident }}</div>
+ <span>{{ $options.i18n.incidentHelpText }}</span>
+ </li>
+ </ul>
+ </gl-popover>
+ </span>
+</template>
diff --git a/app/assets/javascripts/issues/new/index.js b/app/assets/javascripts/issues/new/index.js
new file mode 100644
index 00000000000..59a7cbec627
--- /dev/null
+++ b/app/assets/javascripts/issues/new/index.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import TitleSuggestions from './components/title_suggestions.vue';
+import TypePopover from './components/type_popover.vue';
+
+export function initTitleSuggestions() {
+ Vue.use(VueApollo);
+
+ const el = document.getElementById('js-suggestions');
+ const issueTitle = document.getElementById('issue_title');
+
+ if (!el) {
+ return undefined;
+ }
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ data() {
+ return {
+ search: issueTitle.value,
+ };
+ },
+ mounted() {
+ issueTitle.addEventListener('input', () => {
+ this.search = issueTitle.value;
+ });
+ },
+ render(createElement) {
+ return createElement(TitleSuggestions, {
+ props: {
+ projectPath: el.dataset.projectPath,
+ search: this.search,
+ },
+ });
+ },
+ });
+}
+
+export function initTypePopover() {
+ const el = document.getElementById('js-type-popover');
+
+ if (!el) {
+ return undefined;
+ }
+
+ return new Vue({
+ el,
+ render: (createElement) => createElement(TypePopover),
+ });
+}
diff --git a/app/assets/javascripts/issues/new/queries/issues.query.graphql b/app/assets/javascripts/issues/new/queries/issues.query.graphql
new file mode 100644
index 00000000000..dc0757b141f
--- /dev/null
+++ b/app/assets/javascripts/issues/new/queries/issues.query.graphql
@@ -0,0 +1,29 @@
+query issueSuggestion($fullPath: ID!, $search: String) {
+ project(fullPath: $fullPath) {
+ id
+ issues(search: $search, sort: updated_desc, first: 5) {
+ edges {
+ node {
+ id
+ iid
+ title
+ confidential
+ userNotesCount
+ upvotes
+ webUrl
+ state
+ closedAt
+ createdAt
+ updatedAt
+ author {
+ id
+ name
+ username
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ }
+}