diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-07 03:08:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-07 03:08:19 +0300 |
commit | 175f124d93ba52aeb850b5c032930168612d1e71 (patch) | |
tree | edbc96c83c99bb8fe0a75a4caec3047166a0c10d /app | |
parent | 317e9df33c5696e8bd6e07b99e918f64c27948c0 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_description.vue | 141 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_description_rendered.vue | 117 | ||||
-rw-r--r-- | app/finders/issuable_finder.rb | 6 | ||||
-rw-r--r-- | app/finders/issues_finder.rb | 12 | ||||
-rw-r--r-- | app/models/issue.rb | 6 |
5 files changed, 204 insertions, 78 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index f317688b136..6f29852a4b0 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlFormGroup, GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlButton, GlFormGroup } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { helpPagePath } from '~/helpers/help_page_helper'; import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave'; @@ -11,16 +11,15 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { getWorkItemQuery } from '../utils'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants'; +import WorkItemDescriptionRendered from './work_item_description_rendered.vue'; export default { - directives: { - SafeHtml: GlSafeHtmlDirective, - }, components: { EditedAt, GlButton, GlFormGroup, MarkdownField, + WorkItemDescriptionRendered, }, mixins: [Tracking.mixin()], props: { @@ -50,6 +49,7 @@ export default { isSubmitting: false, isSubmittingWithKeydown: false, descriptionText: '', + descriptionHtml: '', }; }, apollo: { @@ -66,6 +66,10 @@ export default { skip() { return !this.workItemId; }, + result() { + this.descriptionText = this.workItemDescription?.description; + this.descriptionHtml = this.workItemDescription?.descriptionHtml; + }, error() { this.error = i18n.fetchError; }, @@ -76,7 +80,7 @@ export default { return this.workItemId; }, canEdit() { - return this.workItem?.userPermissions?.updateWorkItem; + return this.workItem?.userPermissions?.updateWorkItem || false; }, tracking() { return { @@ -85,12 +89,6 @@ export default { property: `type_${this.workItemType}`, }; }, - descriptionHtml() { - return this.workItemDescription?.descriptionHtml; - }, - descriptionEmpty() { - return this.descriptionHtml?.trim() === ''; - }, workItemDescription() { const descriptionWidget = this.workItem?.widgets?.find( (widget) => widget.type === WIDGET_TYPE_DESCRIPTION, @@ -154,8 +152,10 @@ export default { updateDraft(this.autosaveKey, this.descriptionText); }, - async updateWorkItem(event) { - if (event.key) { + async updateWorkItem(event = {}) { + const { key } = event; + + if (key) { this.isSubmittingWithKeydown = true; } @@ -191,73 +191,70 @@ export default { this.isSubmitting = false; }, + handleDescriptionTextUpdated(newText) { + this.descriptionText = newText; + this.updateWorkItem(); + }, }, }; </script> <template> - <gl-form-group - v-if="isEditing" - class="gl-my-5 gl-border-t gl-pt-6" - :label="__('Description')" - label-for="work-item-description" - > - <markdown-field - can-attach-file - :textarea-value="descriptionText" - :is-submitting="isSubmitting" - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="$options.markdownDocsPath" - class="gl-p-3 bordered-box gl-mt-5" + <div> + <gl-form-group + v-if="isEditing" + class="gl-mb-5 gl-border-t gl-pt-6" + :label="__('Description')" + label-for="work-item-description" > - <template #textarea> - <textarea - id="work-item-description" - ref="textarea" - v-model="descriptionText" - :disabled="isSubmitting" - class="note-textarea js-gfm-input js-autosize markdown-area" - dir="auto" - data-supports-quick-actions="false" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files here…')" - @keydown.meta.enter="updateWorkItem" - @keydown.ctrl.enter="updateWorkItem" - @keydown.exact.esc.stop="cancelEditing" - @input="onInput" - ></textarea> - </template> - </markdown-field> - - <div class="gl-display-flex"> - <gl-button - category="primary" - variant="confirm" - :loading="isSubmitting" - data-testid="save-description" - @click="updateWorkItem" - >{{ __('Save') }}</gl-button + <markdown-field + can-attach-file + :textarea-value="descriptionText" + :is-submitting="isSubmitting" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="$options.markdownDocsPath" + class="gl-p-3 bordered-box gl-mt-5" > - <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{ - __('Cancel') - }}</gl-button> - </div> - </gl-form-group> - <div v-else class="gl-mb-5 gl-border-t"> - <div class="gl-display-inline-flex gl-align-items-center gl-mb-5"> - <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label> - <gl-button - v-if="canEdit" - class="gl-ml-auto" - icon="pencil" - data-testid="edit-description" - :aria-label="__('Edit description')" - @click="startEditing" - /> - </div> + <template #textarea> + <textarea + id="work-item-description" + ref="textarea" + v-model="descriptionText" + :disabled="isSubmitting" + class="note-textarea js-gfm-input js-autosize markdown-area" + dir="auto" + data-supports-quick-actions="false" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" + @keydown.meta.enter="updateWorkItem" + @keydown.ctrl.enter="updateWorkItem" + @keydown.exact.esc.stop="cancelEditing" + @input="onInput" + ></textarea> + </template> + </markdown-field> - <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div> - <div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div> + <div class="gl-display-flex"> + <gl-button + category="primary" + variant="confirm" + :loading="isSubmitting" + data-testid="save-description" + @click="updateWorkItem" + >{{ __('Save') }} + </gl-button> + <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing" + >{{ __('Cancel') }} + </gl-button> + </div> + </gl-form-group> + <work-item-description-rendered + v-else + :work-item-description="workItemDescription" + :can-edit="canEdit" + @startEditing="startEditing" + @descriptionUpdated="handleDescriptionTextUpdated" + /> <edited-at v-if="lastEditedAt" :updated-at="lastEditedAt" diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue new file mode 100644 index 00000000000..e6f8a301c5e --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -0,0 +1,117 @@ +<script> +import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui'; +import $ from 'jquery'; +import '~/behaviors/markdown/render_gfm'; + +const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox'); + +export default { + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + components: { + GlButton, + }, + props: { + workItemDescription: { + type: Object, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + }, + computed: { + descriptionText() { + return this.workItemDescription?.description; + }, + descriptionHtml() { + return this.workItemDescription?.descriptionHtml; + }, + descriptionEmpty() { + return this.descriptionHtml?.trim() === ''; + }, + }, + watch: { + descriptionHtml: { + handler() { + this.renderGFM(); + }, + immediate: true, + }, + }, + methods: { + async renderGFM() { + await this.$nextTick(); + + $(this.$refs['gfm-content']).renderGFM(); + + if (this.canEdit) { + this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox'); + + // enable boxes, disabled by default in markdown + this.checkboxes.forEach((checkbox) => { + // eslint-disable-next-line no-param-reassign + checkbox.disabled = false; + }); + } + }, + toggleCheckboxes(event) { + const { target } = event; + + if (isCheckbox(target)) { + target.disabled = true; + + const { sourcepos } = target.parentElement.dataset; + + if (!sourcepos) return; + + const [startRange] = sourcepos.split('-'); + let [startRow] = startRange.split(':'); + startRow = Number(startRow) - 1; + + const descriptionTextRows = this.descriptionText.split('\n'); + const newDescriptionText = descriptionTextRows + .map((row, index) => { + if (startRow === index) { + if (target.checked) { + return row.replace(/\[ \]/, '[x]'); + } + return row.replace(/\[[x~]\]/i, '[ ]'); + } + return row; + }) + .join('\n'); + + this.$emit('descriptionUpdated', newDescriptionText); + } + }, + }, +}; +</script> + +<template> + <div class="gl-mb-5 gl-border-t gl-pt-5"> + <div class="gl-display-inline-flex gl-align-items-center gl-mb-5"> + <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label> + <gl-button + v-if="canEdit" + class="gl-ml-auto" + icon="pencil" + data-testid="edit-description" + :aria-label="__('Edit description')" + @click="$emit('startEditing')" + /> + </div> + + <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div> + <div + v-else + ref="gfm-content" + v-safe-html="descriptionHtml" + class="md gl-mb-5 gl-min-h-8" + @change="toggleCheckboxes" + ></div> + </div> +</template> diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a6f5c826243..a90fe399285 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -335,7 +335,7 @@ class IssuableFinder return items if items.is_a?(ActiveRecord::NullRelation) return items if Feature.enabled?(:disable_anonymous_search, type: :ops) && current_user.nil? - return items.pg_full_text_search(search, matched_columns: params[:in].to_s.split(',')) if use_full_text_search? + return filter_by_full_text_search(items) if use_full_text_search? if use_cte_for_search? cte = Gitlab::SQL::CTE.new(klass.table_name, items) @@ -353,6 +353,10 @@ class IssuableFinder Feature.enabled?(:issues_full_text_search, params.project || params.group) end + def filter_by_full_text_search(items) + items.pg_full_text_search(search, matched_columns: params[:in].to_s.split(',')) + end + # rubocop: disable CodeReuse/ActiveRecord def by_iids(items) params[:iids].present? ? items.where(iid: params[:iids]) : items diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 9f96abcd4e5..e12dce744b5 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -29,6 +29,8 @@ # issue_types: array of strings (one of WorkItems::Type.base_types) # class IssuesFinder < IssuableFinder + extend ::Gitlab::Utils::Override + CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER def self.scalar_params @@ -96,6 +98,16 @@ class IssuesFinder < IssuableFinder by_negated_issue_types(issues) end + override :filter_by_full_text_search + def filter_by_full_text_search(items) + # This project condition is used as a hint to PG about the partitions that need searching + # because the search data is partitioned by project. + # In certain cases, like the recent items search, the query plan is much better without this condition. + return super if params[:skip_full_text_search_project_condition].present? + + super.with_projects_matching_search_data + end + def by_confidential(items) return items if params[:confidential].nil? diff --git a/app/models/issue.rb b/app/models/issue.rb index 7c2270c7558..6c0a4c1260a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -212,6 +212,7 @@ class Issue < ApplicationRecord end scope :with_null_relative_position, -> { where(relative_position: nil) } scope :with_non_null_relative_position, -> { where.not(relative_position: nil) } + scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') } before_validation :ensure_namespace_id, :ensure_work_item_type @@ -271,11 +272,6 @@ class Issue < ApplicationRecord def order_upvotes_asc reorder(upvotes_count: :asc) end - - override :pg_full_text_search - def pg_full_text_search(query, matched_columns: []) - super.where('issue_search_data.project_id = issues.project_id') - end end def self.participant_includes |