diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 14:43:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 14:43:17 +0300 |
commit | dfc94207fec2d84314b1a5410cface22e8b369bd (patch) | |
tree | c54022f61ced104305889a64de080998a0dc773b /app/assets/javascripts/issues | |
parent | b874efeff674f6bf0355d5d242ecf81c6f7155df (diff) |
Add latest changes from gitlab-org/gitlab@15-11-stable-eev15.11.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues')
14 files changed, 245 insertions, 83 deletions
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js index b7d885ed8a7..d35355a8f26 100644 --- a/app/assets/javascripts/issues/constants.js +++ b/app/assets/javascripts/issues/constants.js @@ -5,6 +5,7 @@ export const STATUS_CLOSED = 'closed'; export const STATUS_MERGED = 'merged'; export const STATUS_OPEN = 'opened'; export const STATUS_REOPENED = 'reopened'; +export const STATUS_LOCKED = 'locked'; export const TITLE_LENGTH_MAX = 255; @@ -22,4 +23,6 @@ export const IssuableStatusText = { [STATUS_CLOSED]: __('Closed'), [STATUS_OPEN]: __('Open'), [STATUS_REOPENED]: __('Open'), + [STATUS_MERGED]: __('Merged'), + [STATUS_LOCKED]: __('Open'), }; diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js index c821c18bcb9..de0334b4ffe 100644 --- a/app/assets/javascripts/issues/create_merge_request_dropdown.js +++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js @@ -432,7 +432,7 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); - if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + if (isConfidentialIssue() && !event.currentTarget.classList.contains('js-create-target')) { this.droplab.hooks.forEach((hook) => hook.list.toggle()); return; @@ -442,9 +442,9 @@ export default class CreateMergeRequestDropdown { return; } - if (event.target.dataset.action === CREATE_MERGE_REQUEST) { + if (event.currentTarget.dataset.action === CREATE_MERGE_REQUEST) { xhr = this.createMergeRequest(); - } else if (event.target.dataset.action === CREATE_BRANCH) { + } else if (event.currentTarget.dataset.action === CREATE_BRANCH) { xhr = this.createBranch(); } diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js index 83387d3ac29..61531880842 100644 --- a/app/assets/javascripts/issues/index.js +++ b/app/assets/javascripts/issues/index.js @@ -3,12 +3,10 @@ import IssuableForm from 'ee_else_ce/issuable/issuable_form'; import IssuableLabelSelector from '~/issuable/issuable_label_selector'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import GLForm from '~/gl_form'; import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable'; -import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors'; import { TYPE_INCIDENT } from '~/issues/constants'; import Issue from '~/issues/issue'; -import { initTitleSuggestions, initTypePopover } from '~/issues/new'; +import { initTitleSuggestions, initTypePopover, initTypeSelect } from '~/issues/new'; import { initRelatedMergeRequests } from '~/issues/related_merge_requests'; import { initRelatedIssues } from '~/related_issues'; import { @@ -38,15 +36,14 @@ export function initFilteredSearchServiceDesk() { } export function initForm() { - new GLForm($('.issue-form')); // eslint-disable-line no-new new IssuableForm($('.issue-form')); // eslint-disable-line no-new IssuableLabelSelector(); - new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new new LabelsSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new initTitleSuggestions(); initTypePopover(); + initTypeSelect(); mountMilestoneDropdown(); } diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue index d11540ad3dd..5199c36db5a 100644 --- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue @@ -77,7 +77,7 @@ export default { class="issuable-milestone gl-mr-3" data-testid="issuable-milestone" > - <gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate"> + <gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate" class="gl-font-sm"> <gl-icon name="clock" /> {{ issue.milestone.title }} </gl-link> diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 99064a50e3f..2c6f11b682c 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -76,8 +76,8 @@ export const ALTERNATIVE_FILTER = 'alternativeFilter'; export const i18n = { calendarLabel: __('Subscribe to calendar'), - closed: __('CLOSED'), - closedMoved: __('CLOSED (MOVED)'), + closed: __('Closed'), + closedMoved: __('Closed (moved)'), confidentialNo: __('No'), confidentialYes: __('Yes'), downvotes: __('Downvotes'), diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js index b590006929a..e64870152bd 100644 --- a/app/assets/javascripts/issues/list/graphql.js +++ b/app/assets/javascripts/issues/list/graphql.js @@ -1,7 +1,9 @@ import produce from 'immer'; -import createDefaultClient from '~/lib/graphql'; +import createDefaultClient, { createApolloClientWithCaching } from '~/lib/graphql'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +let client; + const resolvers = { Mutation: { reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { @@ -22,6 +24,10 @@ const resolvers = { }, }; -export const gqlClient = gon.features?.frontendCaching - ? createDefaultClient(resolvers, { localCacheKey: 'issues_list' }) - : createDefaultClient(resolvers); +export async function gqlClient() { + if (client) return client; + client = gon.features?.frontendCaching + ? await createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list' }) + : createDefaultClient(resolvers); + return client; +} diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index aca894549e4..a97b59c1e4f 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -6,7 +6,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue'; import { gqlClient } from './graphql'; -export function mountJiraIssuesListApp() { +export async function mountJiraIssuesListApp() { const el = document.querySelector('.js-jira-issues-import-status-root'); if (!el) { @@ -27,7 +27,7 @@ export function mountJiraIssuesListApp() { el, name: 'JiraIssuesImportStatusRoot', apolloProvider: new VueApollo({ - defaultClient: gqlClient, + defaultClient: await gqlClient(), }), render(createComponent) { return createComponent(JiraIssuesImportStatusApp, { @@ -42,7 +42,7 @@ export function mountJiraIssuesListApp() { }); } -export function mountIssuesListApp() { +export async function mountIssuesListApp() { const el = document.querySelector('.js-issues-list-root'); if (!el) { @@ -100,7 +100,7 @@ export function mountIssuesListApp() { el, name: 'IssuesListRoot', apolloProvider: new VueApollo({ - defaultClient: gqlClient, + defaultClient: await gqlClient(), }), router: new VueRouter({ base: window.location.pathname, diff --git a/app/assets/javascripts/issues/new/components/title_suggestions_item.vue b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue index a01f4f747b9..be2237ae2a2 100644 --- a/app/assets/javascripts/issues/new/components/title_suggestions_item.vue +++ b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue @@ -1,6 +1,7 @@ <script> import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { uniqueId } from 'lodash'; +import { STATUS_CLOSED } from '~/issues/constants'; 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'; @@ -42,7 +43,7 @@ export default { ].filter(({ count }) => count); }, isClosed() { - return this.suggestion.state === 'closed'; + return this.suggestion.state === STATUS_CLOSED; }, stateIconClass() { return this.isClosed ? 'gl-text-blue-500' : 'gl-text-green-500'; diff --git a/app/assets/javascripts/issues/new/components/type_select.vue b/app/assets/javascripts/issues/new/components/type_select.vue new file mode 100644 index 00000000000..81c3a769d26 --- /dev/null +++ b/app/assets/javascripts/issues/new/components/type_select.vue @@ -0,0 +1,113 @@ +<script> +import { GlCollapsibleListbox, GlIcon } from '@gitlab/ui'; +import { TYPE_ISSUE, TYPE_INCIDENT } from '~/issues/constants'; +import { visitUrl } from '~/lib/utils/url_utility'; +import Tracking from '~/tracking'; +import { __ } from '~/locale'; + +export default { + i18n: { + selectType: __('Select type'), + issuableType: { + [TYPE_ISSUE]: __('Issue'), + [TYPE_INCIDENT]: __('Incident'), + }, + }, + components: { + GlCollapsibleListbox, + GlIcon, + }, + mixins: [Tracking.mixin()], + props: { + selectedType: { + required: false, + default: '', + type: String, + }, + isIssueAllowed: { + required: false, + default: false, + type: Boolean, + }, + isIncidentAllowed: { + required: false, + default: false, + type: Boolean, + }, + issuePath: { + required: false, + default: '', + type: String, + }, + incidentPath: { + required: false, + default: '', + type: String, + }, + }, + data() { + return { + selected: this.selectedType, + }; + }, + computed: { + toggleText() { + return this.selectedType + ? this.$options.i18n.issuableType[this.selectedType] + : this.$options.i18n.selectType; + }, + dropdownItems() { + const issueItem = this.isIssueAllowed + ? { + value: TYPE_ISSUE, + text: __('Issue'), + icon: 'issue-type-issue', + href: this.issuePath, + } + : null; + const incidentItem = this.isIncidentAllowed + ? { + value: TYPE_INCIDENT, + text: __('Incident'), + icon: 'issue-type-incident', + href: this.incidentPath, + tracking: { + action: 'select_issue_type_incident', + label: 'select_issue_type_incident_dropdown_option', + }, + } + : null; + + return [issueItem, incidentItem].filter(Boolean); + }, + }, + methods: { + selectType(type) { + const selectedItem = this.dropdownItems.find((item) => item.value === type); + if (selectedItem.tracking) { + const { action, label } = selectedItem.tracking; + this.track(action, { label }); + } + + visitUrl(selectedItem.href); + }, + }, +}; +</script> + +<template> + <gl-collapsible-listbox + v-model="selected" + :header-text="$options.i18n.selectType" + :toggle-text="toggleText" + :items="dropdownItems" + block + class="js-issuable-type-filter-dropdown-wrap" + @select="selectType" + > + <template #list-item="{ item }"> + <gl-icon :name="item.icon" :size="16" /> + {{ item.text }} + </template> + </gl-collapsible-listbox> +</template> diff --git a/app/assets/javascripts/issues/new/index.js b/app/assets/javascripts/issues/new/index.js index 91599502996..84a170e6564 100644 --- a/app/assets/javascripts/issues/new/index.js +++ b/app/assets/javascripts/issues/new/index.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; import TitleSuggestions from './components/title_suggestions.vue'; import TypePopover from './components/type_popover.vue'; +import TypeSelect from './components/type_select.vue'; export function initTitleSuggestions() { const el = document.getElementById('js-suggestions'); @@ -56,3 +58,28 @@ export function initTypePopover() { render: (createElement) => createElement(TypePopover), }); } + +export function initTypeSelect() { + const el = document.getElementById('js-type-select'); + + if (!el) { + return undefined; + } + + const { selectedType, isIssueAllowed, isIncidentAllowed, issuePath, incidentPath } = el.dataset; + + return new Vue({ + el, + name: 'TypeSelectRoot', + render: (createElement) => + createElement(TypeSelect, { + props: { + selectedType, + isIssueAllowed: parseBoolean(isIssueAllowed), + isIncidentAllowed: parseBoolean(isIncidentAllowed), + issuePath, + incidentPath, + }, + }), + }); +} diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue index 15f97222971..bc32a15a420 100644 --- a/app/assets/javascripts/issues/show/components/app.vue +++ b/app/assets/javascripts/issues/show/components/app.vue @@ -14,6 +14,7 @@ import Poll from '~/lib/utils/poll'; import { visitUrl } from '~/lib/utils/url_utility'; import { __, sprintf } from '~/locale'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; +import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection'; import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, POLLING_DELAY } from '../constants'; import eventHub from '../event_hub'; import getIssueStateQuery from '../queries/get_issue_state.query.graphql'; @@ -95,10 +96,10 @@ export default { required: false, default: '', }, - initialTaskStatus: { - type: String, + initialTaskCompletionStatus: { + type: Object, required: false, - default: '', + default: () => ({}), }, updatedAt: { type: String, @@ -197,7 +198,7 @@ export default { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, - taskStatus: this.initialTaskStatus, + taskCompletionStatus: this.initialTaskCompletionStatus, lock_version: this.lockVersion, }); @@ -222,9 +223,6 @@ export default { formState() { return this.store.formState; }, - hasUpdated() { - return Boolean(this.state.updatedAt); - }, issueChanged() { const { store: { @@ -379,7 +377,7 @@ export default { this.showForm = false; }, - updateIssuable() { + async updateIssuable() { this.setFormState({ updateLoading: true }); const { @@ -392,6 +390,14 @@ export default { this.alert?.dismiss(); + if (containsSensitiveToken(issuablePayload.description)) { + const confirmed = await confirmSensitiveAction(i18n.descriptionPrompt); + if (!confirmed) { + this.setFormState({ updateLoading: false }); + return false; + } + } + return this.service .updateIssuable(issuablePayload) .then((res) => res.data) @@ -557,7 +563,6 @@ export default { :description-html="state.descriptionHtml" :description-text="state.descriptionText" :updated-at="state.updatedAt" - :task-status="state.taskStatus" :issuable-type="issuableType" :update-url="updateEndpoint" :lock-version="state.lock_version" @@ -570,7 +575,7 @@ export default { /> <edited-component - v-if="hasUpdated" + :task-completion-status="state.taskCompletionStatus" :updated-at="state.updatedAt" :updated-by-name="state.updatedByName" :updated-by-path="state.updatedByPath" diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index bdee6c5fe9a..3721f224d5e 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -1,6 +1,5 @@ <script> import { GlToast } from '@gitlab/ui'; -import $ from 'jquery'; import Sortable from 'sortablejs'; import Vue from 'vue'; import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; @@ -59,11 +58,6 @@ export default { required: false, default: '', }, - taskStatus: { - type: String, - required: false, - default: '', - }, issuableType: { type: String, required: false, @@ -138,7 +132,10 @@ export default { }, watch: { descriptionHtml(newDescription, oldDescription) { - if (!this.initialUpdate && newDescription !== oldDescription) { + if ( + !this.initialUpdate && + this.stripClientState(newDescription) !== this.stripClientState(oldDescription) + ) { this.animateChange(); } else { this.initialUpdate = false; @@ -148,16 +145,12 @@ export default { this.renderGFM(); }); }, - taskStatus() { - this.updateTaskStatusText(); - }, }, mounted() { eventHub.$on('convert-task-list-item', this.convertTaskListItem); eventHub.$on('delete-task-list-item', this.deleteTaskListItem); this.renderGFM(); - this.updateTaskStatusText(); }, beforeDestroy() { eventHub.$off('convert-task-list-item', this.convertTaskListItem); @@ -282,24 +275,6 @@ export default { this.$emit('taskListUpdateFailed'); }, - updateTaskStatusText() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); - - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text( - `${taskRegexMatches[1]}/${taskRegexMatches[2]} checklist item${ - taskRegexMatches[2] > 1 ? 's' : '' - }`, - ); - } else { - $tasks.text(''); - $tasksShort.text(''); - } - }, createTaskListItemActions(provide) { const app = new Vue({ el: document.createElement('div'), @@ -349,6 +324,9 @@ export default { listItem.append(element); } }, + stripClientState(description) { + return description.replaceAll('<details open="true">', '<details>'); + }, async createTask({ taskTitle, taskDescription, oldDescription }) { try { const { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription); diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue index 5138a4530e9..6a0edb59b65 100644 --- a/app/assets/javascripts/issues/show/components/edited.vue +++ b/app/assets/javascripts/issues/show/components/edited.vue @@ -1,13 +1,20 @@ <script> -import { GlSprintf } from '@gitlab/ui'; +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { n__, sprintf } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { TimeAgoTooltip, + GlLink, GlSprintf, }, props: { + taskCompletionStatus: { + type: Object, + required: false, + default: () => ({}), + }, updatedAt: { type: String, required: false, @@ -25,36 +32,61 @@ export default { }, }, computed: { + completedCount() { + return this.taskCompletionStatus.completed_count; + }, + count() { + return this.taskCompletionStatus.count; + }, hasUpdatedBy() { return this.updatedByName && this.updatedByPath; }, + showCheck() { + return this.completedCount === this.count; + }, + taskStatus() { + const { completedCount, count } = this; + if (!count) { + return undefined; + } + + return sprintf( + n__( + '%{completedCount} of %{count} checklist item completed', + '%{completedCount} of %{count} checklist items completed', + count, + ), + { completedCount, count }, + ); + }, }, }; </script> <template> <small class="edited-text js-issue-widgets"> - <gl-sprintf v-if="!hasUpdatedBy" :message="__('Edited %{timeago}')"> - <template #timeago> - <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> - </template> - </gl-sprintf> - <gl-sprintf v-else-if="!updatedAt" :message="__('Edited by %{author}')"> - <template #author> - <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline"> - <span>{{ updatedByName }}</span> - </a> - </template> - </gl-sprintf> - <gl-sprintf v-else :message="__('Edited %{timeago} by %{author}')"> - <template #timeago> - <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> - </template> - <template #author> - <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline"> - <span>{{ updatedByName }}</span> - </a> - </template> - </gl-sprintf> + <template v-if="taskStatus"> + <template v-if="showCheck">✓</template> + {{ taskStatus }} + <template v-if="updatedAt">·</template> + </template> + + <template v-if="updatedAt"> + <gl-sprintf v-if="!hasUpdatedBy" :message="__('Edited %{timeago}')"> + <template #timeago> + <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="__('Edited %{timeago} by %{author}')"> + <template #timeago> + <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> + </template> + <template #author> + <gl-link :href="updatedByPath" class="gl-font-sm gl-hover-text-gray-900 gl-text-gray-700"> + {{ updatedByName }} + </gl-link> + </template> + </gl-sprintf> + </template> </small> </template> diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue index 243666b2323..8267c0130a3 100644 --- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue +++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue @@ -199,7 +199,7 @@ export default { <p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p> </div> </div> - <gl-form-group v-if="glFeatures.incidentEventTags"> + <gl-form-group> <label class="gl-display-flex gl-align-items-center gl-gap-3" for="timeline-input-tags"> {{ $options.i18n.tagsLabel }} <timeline-events-tags-popover /> |