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/issues')
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js35
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue182
-rw-r--r--app/assets/javascripts/issues/list/components/new_issue_dropdown.vue2
-rw-r--r--app/assets/javascripts/issues/list/constants.js17
-rw-r--r--app/assets/javascripts/issues/list/index.js4
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql3
-rw-r--r--app/assets/javascripts/issues/list/queries/search_milestones.query.graphql15
-rw-r--r--app/assets/javascripts/issues/list/queries/set_sort_preference.mutation.graphql5
-rw-r--r--app/assets/javascripts/issues/list/utils.js17
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js5
-rw-r--r--app/assets/javascripts/issues/new/index.js2
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/index.js1
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue126
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue5
-rw-r--r--app/assets/javascripts/issues/show/index.js7
15 files changed, 267 insertions, 159 deletions
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index 5d36396bc6e..a3752c7043c 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -69,11 +69,11 @@ export default class CreateMergeRequestDropdown {
this.regexps = {
branch: {
createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
- createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
+ createMrPath: new RegExp('(source_branch%5D=)(.+?)(?=&)'),
},
ref: {
createBranchPath: new RegExp('(ref=)(.+?)$'),
- createMrPath: new RegExp('(ref=)(.+?)$'),
+ createMrPath: new RegExp('(target_branch%5D=)(.+?)$'),
},
};
@@ -167,23 +167,18 @@ export default class CreateMergeRequestDropdown {
}
createMergeRequest() {
- this.isCreatingMergeRequest = true;
-
- return axios
- .post(this.createMrPath, {
- target_project_id: canCreateConfidentialMergeRequest()
- ? confidentialMergeRequestState.selectedProject.id
- : null,
- })
- .then(({ data }) => {
- this.mergeRequestCreated = true;
- window.location.href = data.url;
- })
- .catch(() =>
- createFlash({
- message: __('Failed to create merge request. Please try again.'),
- }),
- );
+ return new Promise(() => {
+ this.isCreatingMergeRequest = true;
+
+ return this.createBranch().then(() => {
+ window.location.href = canCreateConfidentialMergeRequest()
+ ? this.createMrPath.replace(
+ this.projectPath,
+ confidentialMergeRequestState.selectedProject.pathWithNamespace,
+ )
+ : this.createMrPath;
+ });
+ });
}
disable() {
@@ -562,5 +557,7 @@ export default class CreateMergeRequestDropdown {
this.regexps[target].createMrPath,
pathReplacement,
);
+
+ this.wrapperEl.dataset.createMrPath = this.createMrPath;
}
}
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 8b15e801f02..3866a7b3305 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -10,16 +10,30 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { orderBy } from 'lodash';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ITEM_TYPE } from '~/groups/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import {
+ DEFAULT_NONE_ANY,
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
import {
@@ -27,8 +41,6 @@ import {
i18n,
MAX_LIST_SIZE,
PAGE_SIZE,
- PARAM_DUE_DATE,
- PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
@@ -41,37 +53,23 @@ import {
TOKEN_TYPE_TYPE,
UPDATED_DESC,
urlSortParams,
-} from '~/issues/list/constants';
+} from '../constants';
+import eventHub from '../eventhub';
+import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
+import searchLabelsQuery from '../queries/search_labels.query.graphql';
+import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
+import searchUsersQuery from '../queries/search_users.query.graphql';
+import setSortPreferenceMutation from '../queries/set_sort_preference.mutation.graphql';
import {
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
- getDueDateValue,
getFilterTokens,
getInitialPageParams,
getSortKey,
getSortOptions,
-} from '~/issues/list/utils';
-import axios from '~/lib/utils/axios_utils';
-import { scrollUp } from '~/lib/utils/scroll_utils';
-import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
-import {
- DEFAULT_NONE_ANY,
- OPERATOR_IS_ONLY,
- TOKEN_TITLE_ASSIGNEE,
- TOKEN_TITLE_AUTHOR,
- TOKEN_TITLE_CONFIDENTIAL,
- TOKEN_TITLE_LABEL,
- TOKEN_TITLE_MILESTONE,
- TOKEN_TITLE_MY_REACTION,
- TOKEN_TITLE_RELEASE,
- TOKEN_TITLE_TYPE,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import eventHub from '../eventhub';
-import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
-import searchLabelsQuery from '../queries/search_labels.query.graphql';
-import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
-import searchUsersQuery from '../queries/search_users.query.graphql';
+ isSortKey,
+} from '../utils';
import NewIssueDropdown from './new_issue_dropdown.vue';
const AuthorToken = () =>
@@ -103,74 +101,31 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- autocompleteAwardEmojisPath: {
- default: '',
- },
- calendarPath: {
- default: '',
- },
- canBulkUpdate: {
- default: false,
- },
- emptyStateSvgPath: {
- default: '',
- },
- exportCsvPath: {
- default: '',
- },
- fullPath: {
- default: '',
- },
- hasAnyIssues: {
- default: false,
- },
- hasAnyProjects: {
- default: false,
- },
- hasBlockedIssuesFeature: {
- default: false,
- },
- hasIssueWeightsFeature: {
- default: false,
- },
- hasMultipleIssueAssigneesFeature: {
- default: false,
- },
- initialEmail: {
- default: '',
- },
- isAnonymousSearchDisabled: {
- default: false,
- },
- isIssueRepositioningDisabled: {
- default: false,
- },
- isProject: {
- default: false,
- },
- isSignedIn: {
- default: false,
- },
- jiraIntegrationPath: {
- default: '',
- },
- newIssuePath: {
- default: '',
- },
- releasesPath: {
- default: '',
- },
- rssPath: {
- default: '',
- },
- showNewIssueLink: {
- default: false,
- },
- signInPath: {
- default: '',
- },
- },
+ inject: [
+ 'autocompleteAwardEmojisPath',
+ 'calendarPath',
+ 'canBulkUpdate',
+ 'emptyStateSvgPath',
+ 'exportCsvPath',
+ 'fullPath',
+ 'hasAnyIssues',
+ 'hasAnyProjects',
+ 'hasBlockedIssuesFeature',
+ 'hasIssueWeightsFeature',
+ 'hasMultipleIssueAssigneesFeature',
+ 'initialEmail',
+ 'initialSort',
+ 'isAnonymousSearchDisabled',
+ 'isIssueRepositioningDisabled',
+ 'isProject',
+ 'isSignedIn',
+ 'jiraIntegrationPath',
+ 'newIssuePath',
+ 'releasesPath',
+ 'rssPath',
+ 'showNewIssueLink',
+ 'signInPath',
+ ],
props: {
eeSearchTokens: {
type: Array,
@@ -181,7 +136,13 @@ export default {
data() {
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
- let sortKey = getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey;
+ const dashboardSortKey = getSortKey(this.initialSort);
+ const graphQLSortKey =
+ isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase();
+
+ // The initial sort is an old enum value when it is saved on the dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
this.showIssueRepositioningMessage();
@@ -198,7 +159,6 @@ export default {
}
return {
- dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search),
issues: [],
@@ -221,6 +181,9 @@ export default {
return data[this.namespace]?.issues.nodes ?? [];
},
result({ data }) {
+ if (!data) {
+ return;
+ }
this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
@@ -341,6 +304,7 @@ export default {
token: MilestoneToken,
fetchMilestones: this.fetchMilestones,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
+ shouldSkipSort: true,
},
{
type: TOKEN_TYPE_LABEL,
@@ -406,7 +370,7 @@ export default {
tokens.sort((a, b) => a.title.localeCompare(b.title));
- return orderBy(tokens, ['title']);
+ return tokens;
},
showPaginationControls() {
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
@@ -427,7 +391,6 @@ export default {
},
urlParams() {
return {
- due_date: this.dueDateFilter,
search: this.searchQuery,
sort: urlSortParams[this.sortKey],
state: this.state,
@@ -584,7 +547,6 @@ export default {
.put(joinPaths(issueToMove.webPath, 'reorder'), {
move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
- group_full_path: this.isProject ? undefined : this.fullPath,
})
.then(() => {
const serializedVariables = JSON.stringify(this.queryVariables);
@@ -608,6 +570,25 @@ export default {
this.pageParams = getInitialPageParams(sortKey);
}
this.sortKey = sortKey;
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
},
showAnonymousSearchingMessage() {
createFlash({
@@ -644,6 +625,7 @@ export default {
:tabs="$options.IssuableListTabs"
:current-tab="state"
:tab-counts="tabCounts"
+ :truncate-counts="!isProject"
:issuables-loading="$apollo.queries.issues.loading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
diff --git a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
index 71f84050ba8..666e80dfd4b 100644
--- a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
@@ -7,10 +7,10 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import createFlash from '~/flash';
-import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchProjectsQuery from '../queries/search_projects.query.graphql';
export default {
i18n: {
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 4a380848b4f..284167a933f 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -55,8 +55,6 @@ export const i18n = {
export const MAX_LIST_SIZE = 10;
export const PAGE_SIZE = 20;
export const PAGE_SIZE_MANUAL = 100;
-export const PARAM_DUE_DATE = 'due_date';
-export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
export const RELATIVE_POSITION = 'relative_position';
@@ -68,21 +66,6 @@ export const largePageSizeParams = {
firstPageSize: PAGE_SIZE_MANUAL,
};
-export const DUE_DATE_NONE = '0';
-export const DUE_DATE_ANY = '';
-export const DUE_DATE_OVERDUE = 'overdue';
-export const DUE_DATE_WEEK = 'week';
-export const DUE_DATE_MONTH = 'month';
-export const DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS = 'next_month_and_previous_two_weeks';
-export const DUE_DATE_VALUES = [
- DUE_DATE_NONE,
- DUE_DATE_ANY,
- DUE_DATE_OVERDUE,
- DUE_DATE_WEEK,
- DUE_DATE_MONTH,
- DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS,
-];
-
export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
export const CREATED_ASC = 'CREATED_ASC';
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 01cc82ed8fd..3b2d37eab74 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -30,6 +30,7 @@ export function mountJiraIssuesListApp() {
return new Vue({
el,
+ name: 'JiraIssuesImportStatusRoot',
apolloProvider,
render(createComponent) {
return createComponent(JiraIssuesImportStatusRoot, {
@@ -99,6 +100,7 @@ export function mountIssuesListApp() {
hasMultipleIssueAssigneesFeature,
importCsvIssuesPath,
initialEmail,
+ initialSort,
isAnonymousSearchDisabled,
isIssueRepositioningDisabled,
isProject,
@@ -118,6 +120,7 @@ export function mountIssuesListApp() {
return new Vue({
el,
+ name: 'IssuesListRoot',
apolloProvider,
provide: {
autocompleteAwardEmojisPath,
@@ -133,6 +136,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
+ initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
isProject: parseBoolean(isProject),
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 07dae3fd756..430d494deab 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -1,4 +1,5 @@
fragment IssueFragment on Issue {
+ __typename
id
iid
closedAt
@@ -18,6 +19,7 @@ fragment IssueFragment on Issue {
webUrl
assignees {
nodes {
+ __typename
id
avatarUrl
name
@@ -26,6 +28,7 @@ fragment IssueFragment on Issue {
}
}
author {
+ __typename
id
avatarUrl
name
diff --git a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
index e7eb08104a6..040240cde99 100644
--- a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
@@ -3,7 +3,13 @@
query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = false) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
- milestones(searchTitle: $search, includeAncestors: true, includeDescendants: true) {
+ milestones(
+ searchTitle: $search
+ includeAncestors: true
+ includeDescendants: true
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ state: active
+ ) {
nodes {
...Milestone
}
@@ -11,7 +17,12 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
}
project(fullPath: $fullPath) @include(if: $isProject) {
id
- milestones(searchTitle: $search, includeAncestors: true) {
+ milestones(
+ searchTitle: $search
+ includeAncestors: true
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ state: active
+ ) {
nodes {
...Milestone
}
diff --git a/app/assets/javascripts/issues/list/queries/set_sort_preference.mutation.graphql b/app/assets/javascripts/issues/list/queries/set_sort_preference.mutation.graphql
new file mode 100644
index 00000000000..ed7b5193c9b
--- /dev/null
+++ b/app/assets/javascripts/issues/list/queries/set_sort_preference.mutation.graphql
@@ -0,0 +1,5 @@
+mutation setSortPreference($input: UserPreferencesUpdateInput!) {
+ userPreferencesUpdate(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 2919bbbfef8..6322968b3f0 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -1,3 +1,9 @@
+import { isPositiveInteger } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATOR_IS_NOT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import {
API_PARAM,
BLOCKING_ISSUES_ASC,
@@ -7,7 +13,6 @@ import {
defaultPageSizeParams,
DUE_DATE_ASC,
DUE_DATE_DESC,
- DUE_DATE_VALUES,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
@@ -36,13 +41,7 @@ import {
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
-} from '~/issues/list/constants';
-import { isPositiveInteger } from '~/lib/utils/number_utils';
-import { __ } from '~/locale';
-import {
- FILTERED_SEARCH_TERM,
- OPERATOR_IS_NOT,
-} from '~/vue_shared/components/filtered_search_bar/constants';
+} from './constants';
export const getInitialPageParams = (sortKey) =>
sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams;
@@ -50,7 +49,7 @@ export const getInitialPageParams = (sortKey) =>
export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
-export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
+export const isSortKey = (sort) => Object.keys(urlSortParams).includes(sort);
export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
const sortOptions = [
diff --git a/app/assets/javascripts/issues/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index c78505d0610..8fb891f62f7 100644
--- a/app/assets/javascripts/issues/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
@@ -7,12 +7,11 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
-const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
+const updateIssue = (url, { move_before_id, move_after_id }) =>
axios
.put(`${url}/reorder`, {
move_before_id,
move_after_id,
- group_full_path: issueList.dataset.groupFullPath,
})
.catch(() => {
createFlash({
@@ -52,7 +51,7 @@ const initManualOrdering = () => {
const beforeId = prev && parseInt(prev.dataset.id, 10);
const afterId = next && parseInt(next.dataset.id, 10);
- updateIssue(url, issueList, { move_after_id: afterId, move_before_id: beforeId });
+ updateIssue(url, { move_after_id: afterId, move_before_id: beforeId });
},
}),
);
diff --git a/app/assets/javascripts/issues/new/index.js b/app/assets/javascripts/issues/new/index.js
index f96cacf2595..91599502996 100644
--- a/app/assets/javascripts/issues/new/index.js
+++ b/app/assets/javascripts/issues/new/index.js
@@ -20,6 +20,7 @@ export function initTitleSuggestions() {
return new Vue({
el,
+ name: 'TitleSuggestionsRoot',
apolloProvider,
data() {
return {
@@ -51,6 +52,7 @@ export function initTypePopover() {
return new Vue({
el,
+ name: 'TypePopoverRoot',
render: (createElement) => createElement(TypePopover),
});
}
diff --git a/app/assets/javascripts/issues/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js
index 5045f7e1a2a..196084093c8 100644
--- a/app/assets/javascripts/issues/related_merge_requests/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/index.js
@@ -13,6 +13,7 @@ export function initRelatedMergeRequests() {
return new Vue({
el,
+ name: 'RelatedMergeRequestsRoot',
store: createStore(),
render: (createElement) =>
createElement(RelatedMergeRequests, {
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 7be4c13f544..eeccf886b65 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,18 +1,31 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import {
+ GlSafeHtmlDirective as SafeHtml,
+ GlModal,
+ GlModalDirective,
+ GlPopover,
+ GlButton,
+} from '@gitlab/ui';
import $ from 'jquery';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import TaskList from '~/task_list';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
export default {
directives: {
SafeHtml,
+ GlModal: GlModalDirective,
},
-
- mixins: [animateMixin],
-
+ components: {
+ GlModal,
+ GlPopover,
+ CreateWorkItem,
+ GlButton,
+ },
+ mixins: [animateMixin, glFeatureFlagMixin()],
props: {
canUpdate: {
type: Boolean,
@@ -53,8 +66,15 @@ export default {
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
+ taskButtons: [],
+ activeTask: {},
};
},
+ computed: {
+ workItemsEnabled() {
+ return this.glFeatures.workItems;
+ },
+ },
watch: {
descriptionHtml(newDescription, oldDescription) {
if (!this.initialUpdate && newDescription !== oldDescription) {
@@ -74,6 +94,10 @@ export default {
mounted() {
this.renderGFM();
this.updateTaskStatusText();
+
+ if (this.workItemsEnabled) {
+ this.renderTaskActions();
+ }
},
methods: {
renderGFM() {
@@ -132,6 +156,63 @@ export default {
$tasksShort.text('');
}
},
+ renderTaskActions() {
+ if (!this.$el?.querySelectorAll) {
+ return;
+ }
+
+ const taskListFields = this.$el.querySelectorAll('.task-list-item');
+
+ taskListFields.forEach((item, index) => {
+ const button = document.createElement('button');
+ button.classList.add(
+ 'btn',
+ 'btn-default',
+ 'btn-md',
+ 'gl-button',
+ 'btn-default-tertiary',
+ 'gl-left-0',
+ 'gl-p-0!',
+ 'gl-top-2',
+ 'gl-absolute',
+ 'js-add-task',
+ );
+ button.id = `js-task-button-${index}`;
+ this.taskButtons.push(button.id);
+ button.innerHTML = `
+ <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
+ <use href="${gon.sprite_icons}#ellipsis_v"></use>
+ </svg>
+ `;
+ item.prepend(button);
+ });
+ },
+ openCreateTaskModal(id) {
+ this.activeTask = { id, title: this.$el.querySelector(`#${id}`).parentElement.innerText };
+ this.$refs.modal.show();
+ },
+ closeCreateTaskModal() {
+ this.$refs.modal.hide();
+ },
+ handleCreateTask(title) {
+ const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement;
+ const taskBadge = document.createElement('span');
+ taskBadge.innerHTML = `
+ <svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12">
+ <use href="${gon.sprite_icons}#issue-open-m"></use>
+ </svg>
+ <span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
+ ${__('Task')}
+ </span>
+ <a href="#">${title}</a>
+ `;
+ listItem.insertBefore(taskBadge, listItem.lastChild);
+ listItem.removeChild(listItem.lastChild);
+ this.closeCreateTaskModal();
+ },
+ focusButton() {
+ this.$refs.convertButton[0].$el.focus();
+ },
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
};
@@ -142,12 +223,14 @@ export default {
v-if="descriptionHtml"
:class="{
'js-task-list-container': canUpdate,
+ 'work-items-enabled': workItemsEnabled,
}"
class="description"
>
<div
ref="gfm-content"
v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
+ data-testid="gfm-content"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
@@ -157,13 +240,46 @@ export default {
<!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="descriptionText"
- ref="textarea"
v-model="descriptionText"
:data-update-url="updateUrl"
class="hidden js-task-list-field"
dir="auto"
+ data-testid="textarea"
>
</textarea>
<!-- eslint-enable vue/no-mutating-props -->
+ <gl-modal
+ ref="modal"
+ modal-id="create-task-modal"
+ :title="s__('WorkItem|New Task')"
+ hide-footer
+ body-class="gl-p-0!"
+ >
+ <create-work-item
+ :is-modal="true"
+ :initial-title="activeTask.title"
+ @closeModal="closeCreateTaskModal"
+ @onCreate="handleCreateTask"
+ />
+ </gl-modal>
+ <template v-if="workItemsEnabled">
+ <gl-popover
+ v-for="item in taskButtons"
+ :key="item"
+ :target="item"
+ placement="top"
+ triggers="focus"
+ @shown="focusButton"
+ >
+ <gl-button
+ ref="convertButton"
+ variant="link"
+ data-testid="convert-to-task"
+ class="gl-text-gray-900! gl-text-decoration-none! gl-outline-0!"
+ @click="openCreateTaskModal(item)"
+ >{{ s__('WorkItem|Convert to work item') }}</gl-button
+ >
+ </gl-popover>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 5476a1ef897..d5ac7b28afc 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,13 +1,12 @@
<script>
import markdownField from '~/vue_shared/components/markdown/field.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateMixin from '../../mixins/update';
export default {
components: {
markdownField,
},
- mixins: [glFeatureFlagsMixin(), updateMixin],
+ mixins: [updateMixin],
props: {
formState: {
type: Object,
@@ -56,7 +55,7 @@ export default {
v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea"
dir="auto"
- :data-supports-quick-actions="!glFeatures.tributeAutocomplete"
+ data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files hereā€¦')"
@keydown.meta.enter="updateIssuable"
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 7f5a0e32f72..f5c71f9691f 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -44,6 +44,7 @@ export function initIncidentApp(issueData = {}) {
return new Vue({
el,
+ name: 'DescriptionRoot',
apolloProvider,
provide: {
issueType: INCIDENT_TYPE,
@@ -74,6 +75,8 @@ export function initIssueApp(issueData, store) {
return undefined;
}
+ const { fullPath } = el.dataset;
+
if (gon?.features?.fixCommentScroll) {
scrollToTargetOnResize();
}
@@ -84,10 +87,12 @@ export function initIssueApp(issueData, store) {
return new Vue({
el,
+ name: 'DescriptionRoot',
apolloProvider,
store,
provide: {
canCreateIncident,
+ fullPath,
},
computed: {
...mapGetters(['getNoteableData']),
@@ -120,6 +125,7 @@ export function initHeaderActions(store, type = '') {
return new Vue({
el,
+ name: 'HeaderActionsRoot',
apolloProvider,
store,
provide: {
@@ -154,6 +160,7 @@ export function initSentryErrorStackTrace() {
return new Vue({
el,
+ name: 'SentryErrorStackTraceRoot',
store: errorTrackingStore,
render: (createElement) =>
createElement(SentryErrorStackTrace, { props: { issueStackTracePath } }),