diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/issues | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues')
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 } }), |