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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-07 03:08:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-07 03:08:19 +0300
commit175f124d93ba52aeb850b5c032930168612d1e71 (patch)
treeedbc96c83c99bb8fe0a75a4caec3047166a0c10d /app
parent317e9df33c5696e8bd6e07b99e918f64c27948c0 (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.vue141
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue117
-rw-r--r--app/finders/issuable_finder.rb6
-rw-r--r--app/finders/issues_finder.rb12
-rw-r--r--app/models/issue.rb6
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