diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-24 21:09:31 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-24 21:09:31 +0300 |
commit | 1d041b2968384605660d49c96a3fcd7a6c51875d (patch) | |
tree | 90ebe0d46183ea57be38273f36b93b2a2adcccf1 | |
parent | 036de5f31f136a7482f9e2b1d1e98a9c4b45ea80 (diff) |
Add latest changes from gitlab-org/gitlab@master
28 files changed, 1170 insertions, 758 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index affe95ffb9f..a739c3356f3 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -3577,3 +3577,19 @@ Performance/OpenStruct: - 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb' - 'lib/gitlab/testing/request_inspector_middleware.rb' - 'lib/mattermost/session.rb' + +# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/324629 +Gitlab/DelegatePredicateMethods: + Exclude: + - 'app/models/clusters/cluster.rb' + - 'app/models/clusters/platforms/kubernetes.rb' + - 'app/models/concerns/ci/metadatable.rb' + - 'app/models/concerns/diff_positionable_note.rb' + - 'app/models/concerns/resolvable_discussion.rb' + - 'app/models/concerns/services/data_fields.rb' + - 'app/models/project.rb' + - 'ee/app/models/concerns/ee/ci/metadatable.rb' + - 'ee/app/models/ee/group.rb' + - 'ee/app/models/ee/namespace.rb' + - 'ee/app/models/license.rb' + - 'lib/gitlab/ci/trace/stream.rb' @@ -198,7 +198,7 @@ gem 'acts-as-taggable-on', '~> 7.0' gem 'sidekiq', '~> 5.2.7' gem 'sidekiq-cron', '~> 1.0' gem 'redis-namespace', '~> 1.7.0' -gem 'gitlab-sidekiq-fetcher', '0.5.5', require: 'sidekiq-reliable-fetch' +gem 'gitlab-sidekiq-fetcher', '0.5.6', require: 'sidekiq-reliable-fetch' # Cron Parser gem 'fugit', '~> 1.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 5d63cb1cb2d..891ed84c940 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -470,7 +470,7 @@ GEM gitlab-pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) - gitlab-sidekiq-fetcher (0.5.5) + gitlab-sidekiq-fetcher (0.5.6) sidekiq (~> 5) gitlab-styles (6.2.0) rubocop (~> 0.91, >= 0.91.1) @@ -1431,7 +1431,7 @@ DEPENDENCIES gitlab-markup (~> 1.7.1) gitlab-net-dns (~> 0.9.1) gitlab-pry-byebug - gitlab-sidekiq-fetcher (= 0.5.5) + gitlab-sidekiq-fetcher (= 0.5.6) gitlab-styles (~> 6.2.0) gitlab_chronic_duration (~> 0.10.6.2) gitlab_omniauth-ldap (~> 2.1.1) diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js index f5eac26431f..b23f8a8eba4 100644 --- a/app/assets/javascripts/alert_management/list.js +++ b/app/assets/javascripts/alert_management/list.js @@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants'; import AlertManagementList from './components/alert_management_list_wrapper.vue'; +import alertsHelpUrlQuery from './graphql/queries/alert_help_url.query.graphql'; Vue.use(VueApollo); @@ -41,7 +42,8 @@ export default () => { ), }); - apolloProvider.clients.defaultClient.cache.writeData({ + apolloProvider.clients.defaultClient.cache.writeQuery({ + query: alertsHelpUrlQuery, data: { alertsHelpUrl, }, diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue index 3c7c792b787..a77696b70cc 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column.vue @@ -1,10 +1,5 @@ <script> -import { - GlFormRadio, - GlFormRadioGroup, - GlLabel, - GlTooltipDirective as GlTooltip, -} from '@gitlab/ui'; +import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import { ListType } from '~/boards/constants'; @@ -17,7 +12,6 @@ export default { BoardAddNewColumnForm, GlFormRadio, GlFormRadioGroup, - GlLabel, }, directives: { GlTooltip, @@ -99,25 +93,25 @@ export default { <template> <board-add-new-column-form :loading="labelsLoading" - :form-description="__('A label list displays issues with the selected label.')" - :search-label="__('Select label')" + :none-selected="__('Select a label')" :search-placeholder="__('Search labels')" :selected-id="selectedId" @filter-items="filterItems" @add-list="addList" > - <template slot="selected"> - <gl-label - v-if="selectedLabel" - v-gl-tooltip - :title="selectedLabel.title" - :description="selectedLabel.description" - :background-color="selectedLabel.color" - :scoped="showScopedLabels(selectedLabel)" - /> + <template #selected> + <template v-if="selectedLabel"> + <span + class="dropdown-label-box gl-top-0 gl-flex-shrink-0" + :style="{ + backgroundColor: selectedLabel.color, + }" + ></span> + <div class="gl-text-truncate">{{ selectedLabel.title }}</div> + </template> </template> - <template slot="items"> + <template #items> <gl-form-radio-group v-if="labels.length > 0" v-model="selectedId" @@ -126,11 +120,11 @@ export default { <label v-for="label in labels" :key="label.id" - class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal" + class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word" > - <gl-form-radio :value="label.id" class="gl-mb-0" /> + <gl-form-radio :value="label.id" /> <span - class="dropdown-label-box gl-top-0" + class="dropdown-label-box gl-top-0 gl-flex-shrink-0" :style="{ backgroundColor: label.color, }" diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue index d85343a5390..70ba90bb1d4 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue @@ -1,5 +1,12 @@ <script> -import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { + GlButton, + GlDropdown, + GlFormGroup, + GlIcon, + GlSearchBoxByType, + GlSkeletonLoader, +} from '@gitlab/ui'; import { mapActions } from 'vuex'; import { __ } from '~/locale'; @@ -8,13 +15,16 @@ export default { add: __('Add to board'), cancel: __('Cancel'), newList: __('New list'), - noneSelected: __('None'), noResults: __('No matching results'), + scope: __('Scope'), + scopeDescription: __('Issues must match this scope to appear in this list.'), selected: __('Selected'), }, components: { GlButton, + GlDropdown, GlFormGroup, + GlIcon, GlSearchBoxByType, GlSkeletonLoader, }, @@ -23,11 +33,12 @@ export default { type: Boolean, required: true, }, - formDescription: { + searchLabel: { type: String, - required: true, + required: false, + default: null, }, - searchLabel: { + noneSelected: { type: String, required: true, }, @@ -46,8 +57,23 @@ export default { searchValue: '', }; }, + watch: { + selectedId(val) { + if (val) { + this.$refs.dropdown.hide(true); + } + }, + }, methods: { ...mapActions(['setAddColumnFormVisibility']), + setFocus() { + this.$refs.searchBox.focusInput(); + }, + onHide() { + this.searchValue = ''; + this.$emit('filter-items', ''); + this.$emit('hide'); + }, }, }; </script> @@ -62,51 +88,64 @@ export default { class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white" > <h3 - class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" data-testid="board-add-column-form-title" > {{ $options.i18n.newList }} </h3> - <div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden"> - <slot name="select-list-type"> - <div class="gl-mb-5"></div> - </slot> + <div + class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start" + > + <div class="gl-px-5"> + <h3 class="gl-font-lg gl-mt-5 gl-mb-2"> + {{ $options.i18n.scope }} + </h3> + <p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p> + </div> - <p class="gl-px-5">{{ formDescription }}</p> + <slot name="select-list-type"></slot> - <div class="gl-px-5 gl-pb-4"> - <label class="gl-mb-2">{{ $options.i18n.selected }}</label> - <slot name="selected"> - <div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div> - </slot> - </div> + <gl-form-group class="gl-px-5 lg-mb-3 gl-max-w-full" :label="searchLabel"> + <gl-dropdown + ref="dropdown" + class="gl-mb-3 gl-max-w-full" + toggle-class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate" + boundary="viewport" + @shown="setFocus" + @hide="onHide" + > + <template #button-content> + <slot name="selected"> + <div>{{ noneSelected }}</div> + </slot> + <gl-icon class="dropdown-chevron gl-flex-shrink-0" name="chevron-down" /> + </template> - <gl-form-group - class="gl-mx-5 gl-mb-3" - :label="searchLabel" - label-for="board-available-column-entities" - > - <gl-search-box-by-type - id="board-available-column-entities" - v-model="searchValue" - debounce="250" - :placeholder="searchPlaceholder" - @input="$emit('filter-items', $event)" - /> - </gl-form-group> + <template #header> + <gl-search-box-by-type + ref="searchBox" + v-model="searchValue" + debounce="250" + class="gl-mt-0!" + :placeholder="searchPlaceholder" + @input="$emit('filter-items', $event)" + /> + </template> - <div v-if="loading" class="gl-px-5"> - <gl-skeleton-loader :width="500" :height="172"> - <rect width="480" height="20" x="10" y="15" rx="4" /> - <rect width="380" height="20" x="10" y="50" rx="4" /> - <rect width="430" height="20" x="10" y="85" rx="4" /> - </gl-skeleton-loader> - </div> + <div v-if="loading" class="gl-px-5"> + <gl-skeleton-loader :width="400" :height="172"> + <rect width="380" height="20" x="10" y="15" rx="4" /> + <rect width="280" height="20" x="10" y="50" rx="4" /> + <rect width="330" height="20" x="10" y="85" rx="4" /> + </gl-skeleton-loader> + </div> - <slot v-else name="items"> - <p class="gl-mx-5">{{ $options.i18n.noResults }}</p> - </slot> + <slot v-else name="items"> + <p class="gl-mx-5">{{ $options.i18n.noResults }}</p> + </slot> + </gl-dropdown> + </gl-form-group> </div> <div class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base" diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue index d59fbcc1b31..0534e027c86 100644 --- a/app/assets/javascripts/boards/components/board_list_deprecated.vue +++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue @@ -134,9 +134,10 @@ export default { e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); const toBoardType = containerEl.dataset.boardType; const cloneActions = { - label: ['milestone', 'assignee'], - assignee: ['milestone', 'label'], - milestone: ['label', 'assignee'], + label: ['milestone', 'assignee', 'iteration'], + assignee: ['milestone', 'label', 'iteration'], + milestone: ['label', 'assignee', 'iteration'], + iteration: ['label', 'assignee', 'milestone'], }; if (toBoardType) { diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index 9bf77239a6b..57569340aa5 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -1,5 +1,8 @@ <script> -import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import { sortOrders, sortOrderOptions } from '../constants'; import RequestWarning from './request_warning.vue'; export default { @@ -7,6 +10,7 @@ export default { RequestWarning, GlButton, GlModal, + GlSegmentedControl, }, directives: { 'gl-modal': GlModalDirective, @@ -39,6 +43,7 @@ export default { data() { return { openedBacktraces: [], + sortOrder: sortOrders.DURATION, }; }, computed: { @@ -48,13 +53,37 @@ export default { metricDetails() { return this.currentRequest.details[this.metric]; }, + metricDetailsSummary() { + return { + [s__('Total')]: this.metricDetails.calls, + [s__('PerformanceBar|Total duration')]: this.metricDetails.duration, + ...(this.metricDetails.summary || {}), + }; + }, metricDetailsLabel() { - return this.metricDetails.duration - ? `${this.metricDetails.duration} / ${this.metricDetails.calls}` - : this.metricDetails.calls; + if (this.metricDetails.duration && this.metricDetails.calls) { + return `${this.metricDetails.duration} / ${this.metricDetails.calls}`; + } else if (this.metricDetails.calls) { + return this.metricDetails.calls; + } + + return '0'; + }, + displaySortOrder() { + return ( + this.metricDetails.details.length !== 0 && + this.metricDetails.details.every((item) => item.start) + ); }, detailsList() { - return this.metricDetails.details; + return this.metricDetails.details.map((item, index) => ({ ...item, id: index })); + }, + sortedList() { + if (this.sortOrder === sortOrders.CHRONOLOGICAL) { + return this.detailsList.slice().sort(this.sortDetailChronologically); + } + + return this.detailsList.slice().sort(this.sortDetailByDuration); }, warnings() { return this.metricDetails.warnings || []; @@ -82,7 +111,17 @@ export default { itemHasOpenedBacktrace(toggledIndex) { return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0; }, + changeSortOrder(order) { + this.sortOrder = order; + }, + sortDetailByDuration(a, b) { + return a.duration < b.duration ? 1 : -1; + }, + sortDetailChronologically(a, b) { + return a.start < b.start ? -1 : 1; + }, }, + sortOrderOptions, }; </script> <template> @@ -93,18 +132,41 @@ export default { data-qa-selector="detailed_metric_content" > <gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link"> - <span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span> + <span + class="gl-text-blue-300 gl-font-weight-bold" + data-testid="performance-bar-details-label" + > + {{ metricDetailsLabel }} + </span> </gl-button> <gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable> + <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> + <div class="gl-display-flex gl-align-items-center" data-testid="performance-bar-summary"> + <div v-for="(value, name) in metricDetailsSummary" :key="name" class="gl-pr-8"> + <div v-if="value" data-testid="performance-bar-summary-item"> + <div>{{ name }}</div> + <div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div> + </div> + </div> + </div> + <gl-segmented-control + v-if="displaySortOrder" + data-testid="performance-bar-sort-order" + :options="$options.sortOrderOptions" + :checked="sortOrder" + @input="changeSortOrder" + /> + </div> + <hr /> <table class="table gl-table"> - <template v-if="detailsList.length"> - <tr v-for="(item, index) in detailsList" :key="index"> - <td> + <template v-if="sortedList.length"> + <tr v-for="item in sortedList" :key="item.id"> + <td data-testid="performance-item-duration"> <span v-if="item.duration">{{ sprintf(__('%{duration}ms'), { duration: item.duration }) }}</span> </td> - <td> + <td data-testid="performance-item-content"> <div> <div v-for="(key, keyIndex) in keys" @@ -121,12 +183,12 @@ export default { variant="default" icon="ellipsis_h" size="small" - :selected="itemHasOpenedBacktrace(index)" + :selected="itemHasOpenedBacktrace(item.id)" :aria-label="__('Toggle backtrace')" - @click="toggleBacktrace(index)" + @click="toggleBacktrace(item.id)" /> </div> - <pre v-if="itemHasOpenedBacktrace(index)" class="backtrace-row mt-2">{{ + <pre v-if="itemHasOpenedBacktrace(item.id)" class="backtrace-row gl-mt-3">{{ item.backtrace }}</pre> </div> @@ -135,7 +197,7 @@ export default { </template> <template v-else> <tr> - <td> + <td data-testid="performance-bar-empty-detail-notice"> {{ sprintf(__('No %{header} for this request.'), { header: header.toLowerCase() }) }} </td> </tr> diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 6b446eb6073..4f79d99a49b 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -40,7 +40,7 @@ export default { metric: 'active-record', title: 'pg', header: s__('PerformanceBar|SQL queries'), - keys: ['sql', 'cached', 'db_role'], + keys: ['sql', 'cached', 'transaction', 'db_role'], }, { metric: 'bullet', @@ -69,6 +69,7 @@ export default { }, { metric: 'external-http', + title: 'external', header: s__('PerformanceBar|External Http calls'), keys: ['label', 'code', 'proxy', 'error'], }, @@ -157,15 +158,17 @@ export default { class="view" > <a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{ - s__('PerformanceBar|trace') + s__('PerformanceBar|Trace') }}</a> </div> - <add-request v-on="$listeners" /> <div v-if="currentRequest.details" id="peek-download" class="view"> <a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{ s__('PerformanceBar|Download') }}</a> </div> + <a v-if="statsUrl" class="gl-text-blue-300 view" :href="statsUrl">{{ + s__('PerformanceBar|Stats') + }}</a> <request-selector v-if="currentRequest" :current-request="currentRequest" @@ -173,9 +176,7 @@ export default { class="ml-auto" @change-current-request="changeCurrentRequest" /> - <div v-if="statsUrl" id="peek-stats" class="view"> - <a class="gl-text-blue-300" :href="statsUrl">{{ s__('PerformanceBar|Stats') }}</a> - </div> + <add-request v-on="$listeners" /> </div> </div> </template> diff --git a/app/assets/javascripts/performance_bar/constants.js b/app/assets/javascripts/performance_bar/constants.js new file mode 100644 index 00000000000..9659383edd9 --- /dev/null +++ b/app/assets/javascripts/performance_bar/constants.js @@ -0,0 +1,17 @@ +import { s__ } from '~/locale'; + +export const sortOrders = { + DURATION: 'duration', + CHRONOLOGICAL: 'chronological', +}; + +export const sortOrderOptions = [ + { + value: sortOrders.DURATION, + text: s__('PerformanceBar|Sort by duration'), + }, + { + value: sortOrders.CHRONOLOGICAL, + text: s__('PerformanceBar|Sort chronologically'), + }, +]; diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 524429bf12a..9b08ca3718e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -293,7 +293,8 @@ class CommitStatus < ApplicationRecord end def update_older_statuses_retried! - self.class + pipeline + .statuses .latest .where(name: name) .where.not(id: id) diff --git a/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml b/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml new file mode 100644 index 00000000000..6eb2d2fcbe3 --- /dev/null +++ b/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml @@ -0,0 +1,6 @@ +--- +title: Remove calls to jQuery animations to fix infinite scrolling on the Repository + commits page +merge_request: 57379 +author: +type: fixed diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ec126dc3ee7..5e74fc2027e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1983,6 +1983,10 @@ To disable directed acyclic graphs (DAG), set the limit to `0`. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14311) in GitLab v12.6. +When a job uses `needs`, it no longer downloads all artifacts from previous stages +by default, because jobs with `needs` can start before earlier stages complete. With +`needs` you can only download artifacts from the jobs listed in the `needs:` configuration. + Use `artifacts: true` (default) or `artifacts: false` to control when artifacts are downloaded in jobs that use `needs`. @@ -3072,6 +3076,13 @@ The artifacts are sent to GitLab after the job finishes. They are available for download in the GitLab UI if the size is not larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd). +By default, jobs in later stages automatically download all the artifacts created +by jobs in earlier stages. You can control artifact download behavior in jobs with +[`dependencies`](#dependencies). + +When using the [`needs`](#artifact-downloads-with-needs) keyword, jobs can only download +artifacts from the jobs defined in the `needs` configuration. + Job artifacts are only collected for successful jobs by default, and artifacts are restored after [caches](#cache). diff --git a/doc/user/application_security/dast/dast_troubleshooting.md b/doc/user/application_security/dast/dast_troubleshooting.md new file mode 100644 index 00000000000..b3f853ab2c0 --- /dev/null +++ b/doc/user/application_security/dast/dast_troubleshooting.md @@ -0,0 +1,64 @@ +--- +stage: Secure +group: Dynamic Analysis +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: reference, howto +--- + +# Dynamic Application Security Testing (DAST) Troubleshooting **(ULTIMATE)** + +The following troubleshooting scenarios have been collected from customer support cases. If you +experience a problem not addressed here, or the information here does not fix your problem, create a +support ticket. + +## Running out of memory + +By default, ZAProxy, which DAST relies on, is allocated memory that sums to 25% +of the total memory on the host. +Since it keeps most of its information in memory during a scan, +it's possible for DAST to run out of memory while scanning large applications. +This results in the following error: + +```plaintext +[zap.out] java.lang.OutOfMemoryError: Java heap space +``` + +Fortunately, it's straightforward to increase the amount of memory available +for DAST by using the `DAST_ZAP_CLI_OPTIONS` CI/CD variable: + +```yaml +include: + - template: DAST.gitlab-ci.yml + +variables: + DAST_ZAP_CLI_OPTIONS: "-Xmx3072m" +``` + +This example allocates 3072 MB to DAST. +Change the number after `-Xmx` to the required memory amount. + +## DAST job exceeding the job timeout + +If your DAST job exceeds the job timeout and you need to reduce the scan duration, we shared some +tips for optimizing DAST scans in a [blog post](https://about.gitlab.com/blog/2020/08/31/how-to-configure-dast-full-scans-for-complex-web-applications/). + +## Getting warning message `gl-dast-report.json: no matching files` + +For information on this, see the [general Application Security troubleshooting section](../../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload). + +## Getting error `dast job: chosen stage does not exist` when including DAST CI template + +To avoid overwriting stages from other CI files, newer versions of the DAST CI template do not +define stages. If you recently started using `DAST.latest.gitlab-ci.yml` or upgraded to a new major +release of GitLab and began receiving this error, you must define a `dast` stage with your other +stages. Note that you must have a running application for DAST to scan. If your application is set +up in your pipeline, it must be deployed in a stage _before_ the `dast` stage: + +```yaml +stages: + - deploy # DAST needs a running application to scan + - dast + +include: + - template: DAST.latest.gitlab-ci.yml +``` diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 77d906332dd..646e90b30c2 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -1193,70 +1193,3 @@ artifacts, add the following to your `gitlab-ci.yml` file: dast: dependencies: [] ``` - -## Troubleshooting - -### Running out of memory - -By default, ZAProxy, which DAST relies on, is allocated memory that sums to 25% -of the total memory on the host. -Since it keeps most of its information in memory during a scan, -it's possible for DAST to run out of memory while scanning large applications. -This results in the following error: - -```plaintext -[zap.out] java.lang.OutOfMemoryError: Java heap space -``` - -Fortunately, it's straightforward to increase the amount of memory available -for DAST by using the `DAST_ZAP_CLI_OPTIONS` CI/CD variable: - -```yaml -include: - - template: DAST.gitlab-ci.yml - -variables: - DAST_ZAP_CLI_OPTIONS: "-Xmx3072m" -``` - -Here, DAST is being allocated 3072 MB. -Change the number after `-Xmx` to the required memory amount. - -### DAST job exceeding the job timeout - -If your DAST job exceeds the job timeout and you need to reduce the scan duration, we shared some tips for optimizing DAST scans in a [blog post](https://about.gitlab.com/blog/2020/08/31/how-to-configure-dast-full-scans-for-complex-web-applications/). - -### Getting warning message `gl-dast-report.json: no matching files` - -For information on this, see the [general Application Security troubleshooting section](../../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload). - -### Getting error `dast job: chosen stage does not exist` when including DAST CI template - -Newer versions of the DAST CI template do not define stages in order to avoid -overwriting stages from other CI files. If you've recently started using -`DAST.latest.gitlab-ci.yml` or upgraded to a new major release of GitLab and -began receiving this error, you will need to define a `dast` stage with your -other stages. Please note that you must have a running application for DAST to -scan. If your application is set up in your pipeline, it must be deployed - in a stage _before_ the `dast` stage: - -```yaml -stages: - - deploy # DAST needs a running application to scan - - dast - -include: - - template: DAST.latest.gitlab-ci.yml -``` - -<!-- ## Troubleshooting - -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. - -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index a44f3286c12..4757e353dd5 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -124,7 +124,7 @@ npm config set @foo:registry https://gitlab.example.com/api/v4/projects/<your_pr # Add the token for the scoped packages URL. Replace <your_project_id> # with the project where your package is located. -npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>" +npm config set -- '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>" ``` - `<your_project_id>` is your project ID, found on the project's home page. @@ -147,7 +147,7 @@ npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/ # Add the token for the scoped packages URL. This will allow you to download # `@foo/` packages from private projects. -npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>" +npm config set -- '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>" ``` - `<your_token>` is your personal access token or deploy token. @@ -189,8 +189,8 @@ To use the [instance-level](#use-the-gitlab-endpoint-for-npm-packages) npm endpo To avoid hard-coding the `authToken` value, you may use a variable in its place: ```shell -npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}" -npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}" +npm config set -- '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}" +npm config set -- '//gitlab.example.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}" ``` Then, you can run `npm publish` either locally or by using GitLab CI/CD. @@ -482,7 +482,6 @@ NPM_TOKEN=<your_token> npm install If you get this error, ensure that: - Your token is not expired and has appropriate permissions. -- [Your token does not begin with `-`](https://gitlab.com/gitlab-org/gitlab/-/issues/235473). - A package with the same name or version doesn't already exist within the given scope. - Your NPM package name does not contain a dot `.`. This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/10248) in GitLab 11.9 and earlier. diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb index 523e673e9e1..4040bed50a9 100644 --- a/lib/peek/views/active_record.rb +++ b/lib/peek/views/active_record.rb @@ -17,22 +17,32 @@ module Peek } }.freeze - def results - super.merge(calls: detailed_calls) - end - def self.thresholds @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) end + def results + super.merge(summary: summary) + end + private - def detailed_calls - "#{calls} (#{cached_calls} cached)" + def summary + detail_store.each_with_object({}) do |item, count| + count_summary(item, count) + end end - def cached_calls - detail_store.count { |item| item[:cached] == 'cached' } + def count_summary(item, count) + if item[:cached].present? + count[item[:cached]] ||= 0 + count[item[:cached]] += 1 + end + + if item[:transaction].present? + count[item[:transaction]] ||= 0 + count[item[:transaction]] += 1 + end end def setup_subscribers @@ -45,10 +55,12 @@ module Peek def generate_detail(start, finish, data) { + start: start, duration: finish - start, sql: data[:sql].strip, backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller), - cached: data[:cached] ? 'cached' : '' + cached: data[:cached] ? 'Cached' : '', + transaction: data[:connection].transaction_open? ? 'In a transaction' : '' } end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 28c074290f5..9030bfa66b0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1365,9 +1365,6 @@ msgstr "" msgid "A job artifact is an archive of files and directories saved by a job when it finishes." msgstr "" -msgid "A label list displays issues with the selected label." -msgstr "" - msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies." msgstr "" @@ -1386,9 +1383,6 @@ msgstr "" msgid "A merge request hasn't yet been merged" msgstr "" -msgid "A milestone list displays issues in the selected milestone." -msgstr "" - msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details" msgstr "" @@ -3262,9 +3256,6 @@ msgstr "" msgid "An application called %{link_to_client} is requesting access to your GitLab account." msgstr "" -msgid "An assignee list displays issues assigned to the selected user" -msgstr "" - msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message." msgstr "" @@ -3652,9 +3643,6 @@ msgstr "" msgid "An issue title is required" msgstr "" -msgid "An iteration list displays issues in the selected iteration." -msgstr "" - msgid "An unauthenticated user" msgstr "" @@ -6446,9 +6434,6 @@ msgstr "" msgid "ClusterAgents|Configuration" msgstr "" -msgid "ClusterAgents|Connect your cluster with the GitLab Agent" -msgstr "" - msgid "ClusterAgents|Created by" msgstr "" @@ -6470,6 +6455,9 @@ msgstr "" msgid "ClusterAgents|Learn how to create an agent access token" msgstr "" +msgid "ClusterAgents|Learn more about installing the GitLab Agent" +msgstr "" + msgid "ClusterAgents|Name" msgstr "" @@ -17203,6 +17191,9 @@ msgstr "" msgid "Issues closed" msgstr "" +msgid "Issues must match this scope to appear in this list." +msgstr "" + msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgstr "" @@ -18419,9 +18410,6 @@ msgstr "" msgid "List the merge requests that must be merged before this one." msgstr "" -msgid "List type" -msgstr "" - msgid "List view" msgstr "" @@ -22327,10 +22315,19 @@ msgstr "" msgid "PerformanceBar|SQL queries" msgstr "" +msgid "PerformanceBar|Sort by duration" +msgstr "" + +msgid "PerformanceBar|Sort chronologically" +msgstr "" + msgid "PerformanceBar|Stats" msgstr "" -msgid "PerformanceBar|trace" +msgid "PerformanceBar|Total duration" +msgstr "" + +msgid "PerformanceBar|Trace" msgstr "" msgid "Permissions" @@ -27301,6 +27298,9 @@ msgstr "" msgid "Select a label" msgstr "" +msgid "Select a milestone" +msgstr "" + msgid "Select a new namespace" msgstr "" @@ -27328,9 +27328,15 @@ msgstr "" msgid "Select all" msgstr "" +msgid "Select an assignee" +msgstr "" + msgid "Select an existing Kubernetes cluster or create a new one." msgstr "" +msgid "Select an iteration" +msgstr "" + msgid "Select assignee" msgstr "" diff --git a/rubocop/cop/gitlab/delegate_predicate_methods.rb b/rubocop/cop/gitlab/delegate_predicate_methods.rb new file mode 100644 index 00000000000..43b5184faab --- /dev/null +++ b/rubocop/cop/gitlab/delegate_predicate_methods.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Gitlab + # This cop looks for delegations to predicate methods with `allow_nil: true` option. + # This construct results in three possible results: true, false and nil. + # In other words, it does not preserve the strict Boolean nature of predicate method return value. + # This cop suggests creating a method to handle `nil` delegator and ensure only Boolean type is returned. + # + # @example + # # bad + # delegate :is_foo?, to: :bar, allow_nil: true + # + # # good + # def is_foo? + # return false unless bar + # bar.is_foo? + # end + # + # def is_foo? + # !!bar&.is_foo? + # end + class DelegatePredicateMethods < RuboCop::Cop::Cop + MSG = "Using `delegate` with `allow_nil` on the following predicate methods is discouraged: %s." + RESTRICT_ON_SEND = %i[delegate].freeze + def_node_matcher :predicate_allow_nil_option, <<~PATTERN + (send nil? :delegate + (sym $_)* + (hash <$(pair (sym :allow_nil) true) ...>) + ) + PATTERN + + def on_send(node) + predicate_allow_nil_option(node) do |delegated_methods, _options| + offensive_methods = delegated_methods.select { |method| method.end_with?('?') } + next if offensive_methods.empty? + + add_offense(node, message: format(MSG, offensive_methods.join(', '))) + end + end + end + end + end +end diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb index b9945207bb2..5128fc4004e 100644 --- a/spec/features/boards/user_adds_lists_to_board_spec.rb +++ b/spec/features/boards/user_adds_lists_to_board_spec.rb @@ -71,10 +71,13 @@ RSpec.describe 'User adds lists', :js do def select_label(board_new_list_enabled, label) if board_new_list_enabled - page.within('.board-add-new-list') do - find('label', text: label.title).click - click_button 'Add' - end + click_button 'Select a label' + + find('label', text: label.title).click + + click_button 'Add to board' + + wait_for_all_requests else page.within('.dropdown-menu-issues-board-new') do click_link label.title diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 5a3c2fd6869..ac412d19974 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -7,12 +7,14 @@ RSpec.describe 'GFM autocomplete', :js do let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') } let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') } + let_it_be(:group) { create(:group, name: 'Ancestor') } let_it_be(:child_group) { create(:group, parent: group, name: 'My group') } let_it_be(:project) { create(:project, group: child_group) } - let_it_be(:label) { create(:label, project: project, title: 'special+') } - let(:issue) { create(:issue, project: project) } + let_it_be(:issue) { create(:issue, project: project, assignees: [user]) } + let_it_be(:label) { create(:label, project: project, title: 'special+') } + let_it_be(:snippet) { create(:project_snippet, project: project, title: 'code snippet') } before_all do project.add_maintainer(user) @@ -21,16 +23,35 @@ RSpec.describe 'GFM autocomplete', :js do end describe 'when tribute_autocomplete feature flag is off' do - before do - stub_feature_flags(tribute_autocomplete: false) + describe 'new issue page' do + before do + stub_feature_flags(tribute_autocomplete: false) + + sign_in(user) + visit new_project_issue_path(project) + + wait_for_requests + end - sign_in(user) - visit project_issue_path(project, issue) + it 'allows quick actions' do + fill_in 'Description', with: '/' - wait_for_requests + expect(find_autocomplete_menu).to be_visible + end end describe 'issue description' do + let(:issue_to_edit) { create(:issue, project: project) } + + before do + stub_feature_flags(tribute_autocomplete: false) + + sign_in(user) + visit project_issue_path(project, issue_to_edit) + + wait_for_requests + end + it 'updates with GFM reference' do click_button 'Edit title and description' @@ -58,367 +79,355 @@ RSpec.describe 'GFM autocomplete', :js do end end - describe 'triggering autocomplete' do - it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do - fill_in 'Comment', with: 'testing@' - expect(page).not_to have_css('.atwho-view') - - fill_in 'Comment', with: '@@' - expect(page).not_to have_css('.atwho-view') - - fill_in 'Comment', with: "@#{user.username[0..2]}!" - expect(page).not_to have_css('.atwho-view') - - fill_in 'Comment', with: "hello:#{user.username[0..2]}" - expect(page).not_to have_css('.atwho-view') - - fill_in 'Comment', with: '7:' - expect(page).not_to have_css('.atwho-view') - - fill_in 'Comment', with: 'w:' - expect(page).not_to have_css('.atwho-view') + describe 'issue comment' do + before do + stub_feature_flags(tribute_autocomplete: false) - fill_in 'Comment', with: 'Ё:' - expect(page).not_to have_css('.atwho-view') + sign_in(user) + visit project_issue_path(project, issue) - fill_in 'Comment', with: "test\n\n@" - expect(find_autocomplete_menu).to be_visible + wait_for_requests end - end - it 'opens autocomplete menu when field starts with text' do - fill_in 'Comment', with: '@' + describe 'triggering autocomplete' do + it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do + fill_in 'Comment', with: 'testing@' + expect(page).not_to have_css('.atwho-view') - expect(find_autocomplete_menu).to be_visible - end + fill_in 'Comment', with: '@@' + expect(page).not_to have_css('.atwho-view') - it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do - issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' - create(:issue, project: project, title: issue_xss_title) + fill_in 'Comment', with: "@#{user.username[0..2]}!" + expect(page).not_to have_css('.atwho-view') - fill_in 'Comment', with: '#' + fill_in 'Comment', with: "hello:#{user.username[0..2]}" + expect(page).not_to have_css('.atwho-view') - wait_for_requests + fill_in 'Comment', with: '7:' + expect(page).not_to have_css('.atwho-view') - expect(find_autocomplete_menu).to have_text(issue_xss_title) - end + fill_in 'Comment', with: 'w:' + expect(page).not_to have_css('.atwho-view') - it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do - fill_in 'Comment', with: '@ev' + fill_in 'Comment', with: 'Ё:' + expect(page).not_to have_css('.atwho-view') - wait_for_requests + fill_in 'Comment', with: "test\n\n@" + expect(find_autocomplete_menu).to be_visible + end + end - expect(find_highlighted_autocomplete_item).to have_text(user_xss.username) - end + it 'opens autocomplete menu when field starts with text' do + fill_in 'Comment', with: '@' - it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do - milestone_xss_title = 'alert milestone <img src=x onerror="alert(\'Hello xss\');" a' - create(:milestone, project: project, title: milestone_xss_title) + expect(find_autocomplete_menu).to be_visible + end - fill_in 'Comment', with: '%' + it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do + issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' + create(:issue, project: project, title: issue_xss_title) - wait_for_requests + fill_in 'Comment', with: '#' - expect(find_autocomplete_menu).to have_text('alert milestone') - end + wait_for_requests - it 'doesnt select the first item for non-assignee dropdowns' do - fill_in 'Comment', with: ':' + expect(find_autocomplete_menu).to have_text(issue_xss_title) + end - wait_for_requests + it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do + fill_in 'Comment', with: '@ev' - expect(find_autocomplete_menu).not_to have_css('.cur') - end + wait_for_requests - it 'selects the first item for assignee dropdowns' do - fill_in 'Comment', with: '@' + expect(find_highlighted_autocomplete_item).to have_text(user_xss.username) + end - wait_for_requests + it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do + milestone_xss_title = 'alert milestone <img src=x onerror="alert(\'Hello xss\');" a' + create(:milestone, project: project, title: milestone_xss_title) - expect(find_autocomplete_menu).to have_css('.cur:first-of-type') - end + fill_in 'Comment', with: '%' - it 'includes items for assignee dropdowns with non-ASCII characters in name' do - fill_in 'Comment', with: "@#{user.name[0...8]}" + wait_for_requests - wait_for_requests + expect(find_autocomplete_menu).to have_text('alert milestone') + end - expect(find_autocomplete_menu).to have_text(user.name) - end + it 'doesnt select the first item for non-assignee dropdowns' do + fill_in 'Comment', with: ':' - it 'searches across full name for assignees' do - fill_in 'Comment', with: '@speciąlsome' + wait_for_requests - wait_for_requests + expect(find_autocomplete_menu).not_to have_css('.cur') + end - expect(find_highlighted_autocomplete_item).to have_text(user.name) - end + it 'selects the first item for assignee dropdowns' do + fill_in 'Comment', with: '@' - it 'shows names that start with the query as the top result' do - fill_in 'Comment', with: '@mar' + wait_for_requests - wait_for_requests + expect(find_autocomplete_menu).to have_css('.cur:first-of-type') + end - expect(find_highlighted_autocomplete_item).to have_text(user2.name) - end + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + fill_in 'Comment', with: "@#{user.name[0...8]}" - it 'shows usernames that start with the query as the top result' do - fill_in 'Comment', with: '@msi' + wait_for_requests - wait_for_requests + expect(find_autocomplete_menu).to have_text(user.name) + end - expect(find_highlighted_autocomplete_item).to have_text(user2.name) - end + it 'searches across full name for assignees' do + fill_in 'Comment', with: '@speciąlsome' - # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925 - it 'shows username when pasting then pressing Enter' do - fill_in 'Comment', with: "@#{user.username}\n" + wait_for_requests - expect(find_field('Comment').value).to have_text "@#{user.username}" - end + expect(find_highlighted_autocomplete_item).to have_text(user.name) + end - it 'does not show `@undefined` when pressing `@` then Enter' do - fill_in 'Comment', with: "@\n" + it 'shows names that start with the query as the top result' do + fill_in 'Comment', with: '@mar' - expect(find_field('Comment').value).to have_text '@' - expect(find_field('Comment').value).not_to have_text '@undefined' - end + wait_for_requests - it 'selects the first item for non-assignee dropdowns if a query is entered' do - fill_in 'Comment', with: ':1' + expect(find_highlighted_autocomplete_item).to have_text(user2.name) + end - wait_for_requests + it 'shows usernames that start with the query as the top result' do + fill_in 'Comment', with: '@msi' - expect(find_autocomplete_menu).to have_css('.cur:first-of-type') - end + wait_for_requests - context 'if a selected value has special characters' do - it 'wraps the result in double quotes' do - fill_in 'Comment', with: "~#{label.title[0]}" + expect(find_highlighted_autocomplete_item).to have_text(user2.name) + end - find_highlighted_autocomplete_item.click + # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925 + it 'shows username when pasting then pressing Enter' do + fill_in 'Comment', with: "@#{user.username}\n" - expect(find_field('Comment').value).to have_text("~\"#{label.title}\"") + expect(find_field('Comment').value).to have_text "@#{user.username}" end - it 'doesn\'t wrap for assignee values' do - fill_in 'Comment', with: "@#{user.username[0]}" + it 'does not show `@undefined` when pressing `@` then Enter' do + fill_in 'Comment', with: "@\n" - find_highlighted_autocomplete_item.click - - expect(find_field('Comment').value).to have_text("@#{user.username}") + expect(find_field('Comment').value).to have_text '@' + expect(find_field('Comment').value).not_to have_text '@undefined' end - it 'doesn\'t wrap for emoji values' do - fill_in 'Comment', with: ':cartwheel_' + it 'selects the first item for non-assignee dropdowns if a query is entered' do + fill_in 'Comment', with: ':1' - find_highlighted_autocomplete_item.click + wait_for_requests - expect(find_field('Comment').value).to have_text('cartwheel_tone1') + expect(find_autocomplete_menu).to have_css('.cur:first-of-type') end - it 'triggers autocomplete after selecting a quick action' do - fill_in 'Comment', with: '/as' + context 'if a selected value has special characters' do + it 'wraps the result in double quotes' do + fill_in 'Comment', with: "~#{label.title[0]}" - find_highlighted_autocomplete_item.click + find_highlighted_autocomplete_item.click - expect(find_autocomplete_menu).to have_text(user.username) - end + expect(find_field('Comment').value).to have_text("~\"#{label.title}\"") + end - it 'does not limit quick actions autocomplete list to 5' do - fill_in 'Comment', with: '/' + it 'doesn\'t wrap for assignee values' do + fill_in 'Comment', with: "@#{user.username[0]}" - expect(find_autocomplete_menu).to have_css('li', minimum: 6) - end - end + find_highlighted_autocomplete_item.click - context 'assignees' do - let(:issue_assignee) { create(:issue, project: project) } - let(:unassigned_user) { create(:user) } + expect(find_field('Comment').value).to have_text("@#{user.username}") + end - before do - issue_assignee.update(assignees: [user]) + it 'doesn\'t wrap for emoji values' do + fill_in 'Comment', with: ':cartwheel_' - project.add_maintainer(unassigned_user) - end + find_highlighted_autocomplete_item.click - it 'lists users who are currently not assigned to the issue when using /assign' do - visit project_issue_path(project, issue_assignee) + expect(find_field('Comment').value).to have_text('cartwheel_tone1') + end - fill_in 'Comment', with: '/as' + it 'triggers autocomplete after selecting a quick action' do + fill_in 'Comment', with: '/as' - find_highlighted_autocomplete_item.click + find_highlighted_autocomplete_item.click - expect(find_autocomplete_menu).not_to have_text(user.username) - expect(find_autocomplete_menu).to have_text(unassigned_user.username) - end + expect(find_autocomplete_menu).to have_text(user2.username) + end - it 'shows dropdown on new issue form' do - visit new_project_issue_path(project) + it 'does not limit quick actions autocomplete list to 5' do + fill_in 'Comment', with: '/' - fill_in 'Description', with: '/ass' + expect(find_autocomplete_menu).to have_css('li', minimum: 6) + end + end - find_highlighted_autocomplete_item.click + context 'assignees' do + it 'lists users who are currently not assigned to the issue when using /assign' do + fill_in 'Comment', with: '/as' - expect(find_autocomplete_menu).to have_text(unassigned_user.username) - expect(find_autocomplete_menu).to have_text(user.username) + find_highlighted_autocomplete_item.click + + expect(find_autocomplete_menu).not_to have_text(user.username) + expect(find_autocomplete_menu).to have_text(user2.username) + end end - end - context 'labels' do - it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do - label_xss_title = 'alert label <img src=x onerror="alert(\'Hello xss\');" a' - create(:label, project: project, title: label_xss_title) + context 'labels' do + it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do + label_xss_title = 'alert label <img src=x onerror="alert(\'Hello xss\');" a' + create(:label, project: project, title: label_xss_title) - fill_in 'Comment', with: '~' + fill_in 'Comment', with: '~' - wait_for_requests + wait_for_requests - expect(find_autocomplete_menu).to have_text('alert label') - end + expect(find_autocomplete_menu).to have_text('alert label') + end - it 'allows colons when autocompleting scoped labels' do - create(:label, project: project, title: 'scoped:label') + it 'allows colons when autocompleting scoped labels' do + create(:label, project: project, title: 'scoped:label') - fill_in 'Comment', with: '~scoped:' + fill_in 'Comment', with: '~scoped:' - wait_for_requests + wait_for_requests - expect(find_autocomplete_menu).to have_text('scoped:label') - end + expect(find_autocomplete_menu).to have_text('scoped:label') + end - it 'allows colons when autocompleting scoped labels with double colons' do - create(:label, project: project, title: 'scoped::label') + it 'allows colons when autocompleting scoped labels with double colons' do + create(:label, project: project, title: 'scoped::label') - fill_in 'Comment', with: '~scoped::' + fill_in 'Comment', with: '~scoped::' - wait_for_requests + wait_for_requests - expect(find_autocomplete_menu).to have_text('scoped::label') - end + expect(find_autocomplete_menu).to have_text('scoped::label') + end - it 'allows spaces when autocompleting multi-word labels' do - create(:label, project: project, title: 'Accepting merge requests') + it 'allows spaces when autocompleting multi-word labels' do + create(:label, project: project, title: 'Accepting merge requests') - fill_in 'Comment', with: '~Accepting merge' + fill_in 'Comment', with: '~Accepting merge' - wait_for_requests + wait_for_requests - expect(find_autocomplete_menu).to have_text('Accepting merge requests') - end + expect(find_autocomplete_menu).to have_text('Accepting merge requests') + end - it 'only autocompletes the latest label' do - create(:label, project: project, title: 'Accepting merge requests') - create(:label, project: project, title: 'Accepting job applicants') + it 'only autocompletes the latest label' do + create(:label, project: project, title: 'Accepting merge requests') + create(:label, project: project, title: 'Accepting job applicants') - fill_in 'Comment', with: '~Accepting merge requests foo bar ~Accepting job' + fill_in 'Comment', with: '~Accepting merge requests foo bar ~Accepting job' - wait_for_requests + wait_for_requests - expect(find_autocomplete_menu).to have_text('Accepting job applicants') - end + expect(find_autocomplete_menu).to have_text('Accepting job applicants') + end - it 'does not autocomplete labels if no tilde is typed' do - create(:label, project: project, title: 'Accepting merge requests') + it 'does not autocomplete labels if no tilde is typed' do + create(:label, project: project, title: 'Accepting merge requests') - fill_in 'Comment', with: 'Accepting merge' + fill_in 'Comment', with: 'Accepting merge' - wait_for_requests + wait_for_requests - expect(page).not_to have_css('.atwho-view') + expect(page).not_to have_css('.atwho-view') + end end - end - context 'when other notes are destroyed' do - let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + context 'when other notes are destroyed' do + let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } - # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729 - it 'keeps autocomplete key listeners' do - visit project_issue_path(project, issue) - note = find_field('Comment') + # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729 + it 'keeps autocomplete key listeners' do + note = find_field('Comment') - start_comment_with_emoji(note, '.atwho-view li') + start_comment_with_emoji(note, '.atwho-view li') - start_and_cancel_discussion + start_and_cancel_discussion - note.fill_in(with: '') - start_comment_with_emoji(note, '.atwho-view li') - note.native.send_keys(:enter) + note.fill_in(with: '') + start_comment_with_emoji(note, '.atwho-view li') + note.native.send_keys(:enter) - expect(note.value).to eql('Hello :100: ') + expect(note.value).to eql('Hello :100: ') + end end - end - shared_examples 'autocomplete suggestions' do - it 'suggests objects correctly' do - fill_in 'Comment', with: object.class.reference_prefix + shared_examples 'autocomplete suggestions' do + it 'suggests objects correctly' do + fill_in 'Comment', with: object.class.reference_prefix - find_autocomplete_menu.find('li').click + find_autocomplete_menu.find('li').click - expect(find_field('Comment').value).to have_text(expected_body) + expect(find_field('Comment').value).to have_text(expected_body) + end end - end - context 'issues' do - let(:object) { issue } - let(:expected_body) { object.to_reference } + context 'issues' do + let(:object) { issue } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'merge requests' do - let(:object) { create(:merge_request, source_project: project) } - let(:expected_body) { object.to_reference } + context 'merge requests' do + let(:object) { create(:merge_request, source_project: project) } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'project snippets' do - let!(:object) { create(:project_snippet, project: project, title: 'code snippet') } - let(:expected_body) { object.to_reference } + context 'project snippets' do + let!(:object) { snippet } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'label' do - let!(:object) { label } - let(:expected_body) { object.title } + context 'label' do + let!(:object) { label } + let(:expected_body) { object.title } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'milestone' do - let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } - let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } - let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } - let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } - let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } + context 'milestone' do + let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } + let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } - before do - fill_in 'Comment', with: '/milestone %' + before do + fill_in 'Comment', with: '/milestone %' - wait_for_requests - end + wait_for_requests + end - it 'shows milestons list in the autocomplete menu' do - page.within(find_autocomplete_menu) do - expect(page).to have_selector('li', count: 5) + it 'shows milestons list in the autocomplete menu' do + page.within(find_autocomplete_menu) do + expect(page).to have_selector('li', count: 5) + end end - end - it 'shows expired milestone at the bottom of the list' do - page.within(find_autocomplete_menu) do - expect(page.find('li:last-child')).to have_content milestone_expired.title + it 'shows expired milestone at the bottom of the list' do + page.within(find_autocomplete_menu) do + expect(page.find('li:last-child')).to have_content milestone_expired.title + end end - end - it 'shows milestone due earliest at the top of the list' do - page.within(find_autocomplete_menu) do - aggregate_failures do - expect(page.all('li')[0]).to have_content milestone3.title - expect(page.all('li')[1]).to have_content milestone2.title - expect(page.all('li')[2]).to have_content milestone1.title - expect(page.all('li')[3]).to have_content milestone_no_duedate.title + it 'shows milestone due earliest at the top of the list' do + page.within(find_autocomplete_menu) do + aggregate_failures do + expect(page.all('li')[0]).to have_content milestone3.title + expect(page.all('li')[1]).to have_content milestone2.title + expect(page.all('li')[2]).to have_content milestone1.title + expect(page.all('li')[3]).to have_content milestone_no_duedate.title + end end end end @@ -426,16 +435,18 @@ RSpec.describe 'GFM autocomplete', :js do end describe 'when tribute_autocomplete feature flag is on' do - before do - stub_feature_flags(tribute_autocomplete: true) + describe 'issue description' do + let(:issue_to_edit) { create(:issue, project: project) } - sign_in(user) - visit project_issue_path(project, issue) + before do + stub_feature_flags(tribute_autocomplete: true) - wait_for_requests - end + sign_in(user) + visit project_issue_path(project, issue_to_edit) + + wait_for_requests + end - describe 'issue description' do it 'updates with GFM reference' do click_button 'Edit title and description' @@ -455,309 +466,306 @@ RSpec.describe 'GFM autocomplete', :js do end end - describe 'triggering autocomplete' do - it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do - fill_in 'Comment', with: 'testing@' - expect(page).not_to have_css('.tribute-container') - - fill_in 'Comment', with: "hello:#{user.username[0..2]}" - expect(page).not_to have_css('.tribute-container') - - fill_in 'Comment', with: '7:' - expect(page).not_to have_css('.tribute-container') - - fill_in 'Comment', with: 'w:' - expect(page).not_to have_css('.tribute-container') + describe 'issue comment' do + before do + stub_feature_flags(tribute_autocomplete: true) - fill_in 'Comment', with: 'Ё:' - expect(page).not_to have_css('.tribute-container') + sign_in(user) + visit project_issue_path(project, issue) - fill_in 'Comment', with: "test\n\n@" - expect(find_tribute_autocomplete_menu).to be_visible + wait_for_requests end - end - - it 'opens autocomplete menu when field starts with text' do - fill_in 'Comment', with: '@' - - expect(find_tribute_autocomplete_menu).to be_visible - end - it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do - issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' - create(:issue, project: project, title: issue_xss_title) + describe 'triggering autocomplete' do + it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do + fill_in 'Comment', with: 'testing@' + expect(page).not_to have_css('.tribute-container') - fill_in 'Comment', with: '#' + fill_in 'Comment', with: "hello:#{user.username[0..2]}" + expect(page).not_to have_css('.tribute-container') - wait_for_requests + fill_in 'Comment', with: '7:' + expect(page).not_to have_css('.tribute-container') - expect(find_tribute_autocomplete_menu).to have_text(issue_xss_title) - end + fill_in 'Comment', with: 'w:' + expect(page).not_to have_css('.tribute-container') - it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do - fill_in 'Comment', with: '@ev' + fill_in 'Comment', with: 'Ё:' + expect(page).not_to have_css('.tribute-container') - wait_for_requests + fill_in 'Comment', with: "test\n\n@" + expect(find_tribute_autocomplete_menu).to be_visible + end + end - expect(find_tribute_autocomplete_menu).to have_text(user_xss.username) - end + it 'opens autocomplete menu when field starts with text' do + fill_in 'Comment', with: '@' - it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do - milestone_xss_title = 'alert milestone <img src=x onerror="alert(\'Hello xss\');" a' - create(:milestone, project: project, title: milestone_xss_title) + expect(find_tribute_autocomplete_menu).to be_visible + end - fill_in 'Comment', with: '%' + it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do + issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' + create(:issue, project: project, title: issue_xss_title) - wait_for_requests + fill_in 'Comment', with: '#' - expect(find_tribute_autocomplete_menu).to have_text('alert milestone') - end + wait_for_requests - it 'selects the first item for assignee dropdowns' do - fill_in 'Comment', with: '@' + expect(find_tribute_autocomplete_menu).to have_text(issue_xss_title) + end - wait_for_requests + it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do + fill_in 'Comment', with: '@ev' - expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type') - end + wait_for_requests - it 'includes items for assignee dropdowns with non-ASCII characters in name' do - fill_in 'Comment', with: "@#{user.name[0...8]}" + expect(find_tribute_autocomplete_menu).to have_text(user_xss.username) + end - wait_for_requests + it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do + milestone_xss_title = 'alert milestone <img src=x onerror="alert(\'Hello xss\');" a' + create(:milestone, project: project, title: milestone_xss_title) - expect(find_tribute_autocomplete_menu).to have_text(user.name) - end + fill_in 'Comment', with: '%' - it 'selects the first item for non-assignee dropdowns if a query is entered' do - fill_in 'Comment', with: ':1' + wait_for_requests - wait_for_requests + expect(find_tribute_autocomplete_menu).to have_text('alert milestone') + end - expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type') - end + it 'selects the first item for assignee dropdowns' do + fill_in 'Comment', with: '@' - context 'when autocompleting for groups' do - it 'shows the group when searching for the name of the group' do - fill_in 'Comment', with: '@mygroup' + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('My group') + expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type') end - it 'does not show the group when searching for the name of the parent of the group' do - fill_in 'Comment', with: '@ancestor' + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + fill_in 'Comment', with: "@#{user.name[0...8]}" - expect(find_tribute_autocomplete_menu).not_to have_text('My group') + wait_for_requests + + expect(find_tribute_autocomplete_menu).to have_text(user.name) end - end - context 'if a selected value has special characters' do - it 'wraps the result in double quotes' do - fill_in 'Comment', with: "~#{label.title[0]}" + it 'selects the first item for non-assignee dropdowns if a query is entered' do + fill_in 'Comment', with: ':1' - find_highlighted_tribute_autocomplete_menu.click + wait_for_requests - expect(find_field('Comment').value).to have_text("~\"#{label.title}\"") + expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type') end - it 'doesn\'t wrap for assignee values' do - fill_in 'Comment', with: "@#{user.username[0..2]}" + context 'when autocompleting for groups' do + it 'shows the group when searching for the name of the group' do + fill_in 'Comment', with: '@mygroup' - find_highlighted_tribute_autocomplete_menu.click + expect(find_tribute_autocomplete_menu).to have_text('My group') + end + + it 'does not show the group when searching for the name of the parent of the group' do + fill_in 'Comment', with: '@ancestor' - expect(find_field('Comment').value).to have_text("@#{user.username}") + expect(find_tribute_autocomplete_menu).not_to have_text('My group') + end end - it 'does not wrap for emoji values' do - fill_in 'Comment', with: ':cartwheel_' + context 'if a selected value has special characters' do + it 'wraps the result in double quotes' do + fill_in 'Comment', with: "~#{label.title[0]}" - find_highlighted_tribute_autocomplete_menu.click + find_highlighted_tribute_autocomplete_menu.click - expect(find_field('Comment').value).to have_text('cartwheel_tone1') - end + expect(find_field('Comment').value).to have_text("~\"#{label.title}\"") + end - it 'autocompletes for quick actions' do - fill_in 'Comment', with: '/as' + it 'doesn\'t wrap for assignee values' do + fill_in 'Comment', with: "@#{user.username[0..2]}" - find_highlighted_tribute_autocomplete_menu.click + find_highlighted_tribute_autocomplete_menu.click - expect(find_field('Comment').value).to have_text('/assign') - end - end + expect(find_field('Comment').value).to have_text("@#{user.username}") + end - context 'assignees' do - let(:issue_assignee) { create(:issue, project: project) } - let(:unassigned_user) { create(:user) } + it 'does not wrap for emoji values' do + fill_in 'Comment', with: ':cartwheel_' - before do - issue_assignee.update(assignees: [user]) + find_highlighted_tribute_autocomplete_menu.click - project.add_maintainer(unassigned_user) - end + expect(find_field('Comment').value).to have_text('cartwheel_tone1') + end - it 'lists users who are currently not assigned to the issue when using /assign' do - visit project_issue_path(project, issue_assignee) + it 'autocompletes for quick actions' do + fill_in 'Comment', with: '/as' - note = find_field('Comment') - note.native.send_keys('/assign ') - # The `/assign` ajax response might replace the one by `@` below causing a failed test - # so we need to wait for the `/assign` ajax request to finish first - wait_for_requests - note.native.send_keys('@') - wait_for_requests + find_highlighted_tribute_autocomplete_menu.click - expect(find_tribute_autocomplete_menu).not_to have_text(user.username) - expect(find_tribute_autocomplete_menu).to have_text(unassigned_user.username) + expect(find_field('Comment').value).to have_text('/assign') + end end - it 'lists users who are currently not assigned to the issue when using /assign on the second line' do - visit project_issue_path(project, issue_assignee) + context 'assignees' do + it 'lists users who are currently not assigned to the issue when using /assign' do + note = find_field('Comment') + note.native.send_keys('/assign ') + # The `/assign` ajax response might replace the one by `@` below causing a failed test + # so we need to wait for the `/assign` ajax request to finish first + wait_for_requests + note.native.send_keys('@') + wait_for_requests - note = find_field('Comment') - note.native.send_keys('/assign @user2') - note.native.send_keys(:enter) - note.native.send_keys('/assign ') - # The `/assign` ajax response might replace the one by `@` below causing a failed test - # so we need to wait for the `/assign` ajax request to finish first - wait_for_requests - note.native.send_keys('@') - wait_for_requests + expect(find_tribute_autocomplete_menu).not_to have_text(user.username) + expect(find_tribute_autocomplete_menu).to have_text(user2.username) + end - expect(find_tribute_autocomplete_menu).not_to have_text(user.username) - expect(find_tribute_autocomplete_menu).to have_text(unassigned_user.username) + it 'lists users who are currently not assigned to the issue when using /assign on the second line' do + note = find_field('Comment') + note.native.send_keys('/assign @user2') + note.native.send_keys(:enter) + note.native.send_keys('/assign ') + # The `/assign` ajax response might replace the one by `@` below causing a failed test + # so we need to wait for the `/assign` ajax request to finish first + wait_for_requests + note.native.send_keys('@') + wait_for_requests + + expect(find_tribute_autocomplete_menu).not_to have_text(user.username) + expect(find_tribute_autocomplete_menu).to have_text(user2.username) + end end - end - context 'labels' do - it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do - label_xss_title = 'alert label <img src=x onerror="alert(\'Hello xss\');" a' - create(:label, project: project, title: label_xss_title) + context 'labels' do + it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do + label_xss_title = 'alert label <img src=x onerror="alert(\'Hello xss\');" a' + create(:label, project: project, title: label_xss_title) - fill_in 'Comment', with: '~' + fill_in 'Comment', with: '~' - wait_for_requests + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('alert label') - end + expect(find_tribute_autocomplete_menu).to have_text('alert label') + end - it 'allows colons when autocompleting scoped labels' do - create(:label, project: project, title: 'scoped:label') + it 'allows colons when autocompleting scoped labels' do + create(:label, project: project, title: 'scoped:label') - fill_in 'Comment', with: '~scoped:' + fill_in 'Comment', with: '~scoped:' - wait_for_requests + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('scoped:label') - end + expect(find_tribute_autocomplete_menu).to have_text('scoped:label') + end - it 'allows colons when autocompleting scoped labels with double colons' do - create(:label, project: project, title: 'scoped::label') + it 'allows colons when autocompleting scoped labels with double colons' do + create(:label, project: project, title: 'scoped::label') - fill_in 'Comment', with: '~scoped::' + fill_in 'Comment', with: '~scoped::' - wait_for_requests + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('scoped::label') - end + expect(find_tribute_autocomplete_menu).to have_text('scoped::label') + end - it 'autocompletes multi-word labels' do - create(:label, project: project, title: 'Accepting merge requests') + it 'autocompletes multi-word labels' do + create(:label, project: project, title: 'Accepting merge requests') - fill_in 'Comment', with: '~Acceptingmerge' + fill_in 'Comment', with: '~Acceptingmerge' - wait_for_requests + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests') - end + expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests') + end - it 'only autocompletes the latest label' do - create(:label, project: project, title: 'documentation') - create(:label, project: project, title: 'feature') + it 'only autocompletes the latest label' do + create(:label, project: project, title: 'documentation') + create(:label, project: project, title: 'feature') - fill_in 'Comment', with: '~documentation foo bar ~feat' - # Invoke autocompletion - find_field('Comment').native.send_keys(:right) + fill_in 'Comment', with: '~documentation foo bar ~feat' + # Invoke autocompletion + find_field('Comment').native.send_keys(:right) - wait_for_requests + wait_for_requests - expect(find_tribute_autocomplete_menu).to have_text('feature') - expect(find_tribute_autocomplete_menu).not_to have_text('documentation') - end + expect(find_tribute_autocomplete_menu).to have_text('feature') + expect(find_tribute_autocomplete_menu).not_to have_text('documentation') + end - it 'does not autocomplete labels if no tilde is typed' do - create(:label, project: project, title: 'documentation') + it 'does not autocomplete labels if no tilde is typed' do + create(:label, project: project, title: 'documentation') - fill_in 'Comment', with: 'document' + fill_in 'Comment', with: 'document' - wait_for_requests + wait_for_requests - expect(page).not_to have_css('.tribute-container') + expect(page).not_to have_css('.tribute-container') + end end - end - context 'when other notes are destroyed' do - let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + context 'when other notes are destroyed' do + let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } - # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729 - it 'keeps autocomplete key listeners' do - visit project_issue_path(project, issue) - note = find_field('Comment') + # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729 + it 'keeps autocomplete key listeners' do + note = find_field('Comment') - start_comment_with_emoji(note, '.tribute-container li') + start_comment_with_emoji(note, '.tribute-container li') - start_and_cancel_discussion + start_and_cancel_discussion - note.fill_in(with: '') - start_comment_with_emoji(note, '.tribute-container li') - note.native.send_keys(:enter) + note.fill_in(with: '') + start_comment_with_emoji(note, '.tribute-container li') + note.native.send_keys(:enter) - expect(note.value).to eql('Hello :100: ') + expect(note.value).to eql('Hello :100: ') + end end - end - shared_examples 'autocomplete suggestions' do - it 'suggests objects correctly' do - fill_in 'Comment', with: object.class.reference_prefix + shared_examples 'autocomplete suggestions' do + it 'suggests objects correctly' do + fill_in 'Comment', with: object.class.reference_prefix - find_tribute_autocomplete_menu.find('li').click + find_tribute_autocomplete_menu.find('li').click - expect(find_field('Comment').value).to have_text(expected_body) + expect(find_field('Comment').value).to have_text(expected_body) + end end - end - context 'issues' do - let(:object) { issue } - let(:expected_body) { object.to_reference } + context 'issues' do + let(:object) { issue } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'merge requests' do - let(:object) { create(:merge_request, source_project: project) } - let(:expected_body) { object.to_reference } + context 'merge requests' do + let(:object) { create(:merge_request, source_project: project) } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'project snippets' do - let!(:object) { create(:project_snippet, project: project, title: 'code snippet') } - let(:expected_body) { object.to_reference } + context 'project snippets' do + let!(:object) { snippet } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'label' do - let!(:object) { label } - let(:expected_body) { object.title } + context 'label' do + let!(:object) { label } + let(:expected_body) { object.title } - it_behaves_like 'autocomplete suggestions' - end + it_behaves_like 'autocomplete suggestions' + end - context 'milestone' do - let!(:object) { create(:milestone, project: project) } - let(:expected_body) { object.to_reference } + context 'milestone' do + let!(:object) { create(:milestone, project: project) } + let(:expected_body) { object.to_reference } - it_behaves_like 'autocomplete suggestions' + it_behaves_like 'autocomplete suggestions' + end end end diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js index 3702f55f17b..3b26ca57d6f 100644 --- a/spec/frontend/boards/components/board_add_new_column_form_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -1,6 +1,6 @@ -import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; @@ -25,7 +25,7 @@ describe('Board card layout', () => { const mountComponent = ({ loading = false, - formDescription = '', + noneSelected = '', searchLabel = '', searchPlaceholder = '', selectedId, @@ -34,12 +34,9 @@ describe('Board card layout', () => { } = {}) => { wrapper = extendedWrapper( shallowMount(BoardAddNewColumnForm, { - stubs: { - GlFormGroup: true, - }, propsData: { loading, - formDescription, + noneSelected, searchLabel, searchPlaceholder, selectedId, @@ -51,13 +48,15 @@ describe('Board card layout', () => { ...actions, }, }), + stubs: { + GlDropdown, + }, }), ); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); @@ -65,10 +64,13 @@ describe('Board card layout', () => { const findSearchLabel = () => wrapper.find(GlFormGroup); const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); const submitButton = () => wrapper.findByTestId('addNewColumnButton'); + const findDropdown = () => wrapper.findComponent(GlDropdown); it('shows form title & search input', () => { mountComponent(); + findDropdown().vm.$emit('show'); + expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); expect(findSearchInput().exists()).toBe(true); }); @@ -86,16 +88,6 @@ describe('Board card layout', () => { expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); }); - it('sets placeholder and description from props', () => { - const props = { - formDescription: 'Some description of a list', - }; - - mountComponent(props); - - expect(wrapper.html()).toHaveText(props.formDescription); - }); - describe('items', () => { const mountWithItems = (loading) => mountComponent({ @@ -151,13 +143,11 @@ describe('Board card layout', () => { expect(submitButton().props('disabled')).toBe(true); }); - it('emits add-list event on click', async () => { + it('emits add-list event on click', () => { mountComponent({ selectedId: mockLabelList.label.id, }); - await nextTick(); - submitButton().vm.$emit('click'); expect(wrapper.emitted('add-list')).toEqual([[]]); diff --git a/spec/frontend/cycle_analytics/total_time_component_spec.js b/spec/frontend/cycle_analytics/total_time_component_spec.js index 0f7f2628aca..e831bc311ed 100644 --- a/spec/frontend/cycle_analytics/total_time_component_spec.js +++ b/spec/frontend/cycle_analytics/total_time_component_spec.js @@ -1,58 +1,58 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import component from '~/cycle_analytics/components/total_time_component.vue'; +import { shallowMount } from '@vue/test-utils'; +import TotalTime from '~/cycle_analytics/components/total_time_component.vue'; describe('Total time component', () => { - let vm; - let Component; + let wrapper; - beforeEach(() => { - Component = Vue.extend(component); - }); + const createComponent = (propsData) => { + wrapper = shallowMount(TotalTime, { + propsData, + }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('With data', () => { it('should render information for days and hours', () => { - vm = mountComponent(Component, { + createComponent({ time: { days: 3, hours: 4, }, }); - expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs'); + expect(wrapper.text()).toMatchInterpolatedText('3 days 4 hrs'); }); it('should render information for hours and minutes', () => { - vm = mountComponent(Component, { + createComponent({ time: { hours: 4, mins: 35, }, }); - expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins'); + expect(wrapper.text()).toMatchInterpolatedText('4 hrs 35 mins'); }); it('should render information for seconds', () => { - vm = mountComponent(Component, { + createComponent({ time: { seconds: 45, }, }); - expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s'); + expect(wrapper.text()).toMatchInterpolatedText('45 s'); }); }); describe('Without data', () => { it('should render no information', () => { - vm = mountComponent(Component); + createComponent(); - expect(vm.$el.textContent.trim()).toEqual('--'); + expect(wrapper.text()).toBe('--'); }); }); }); diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js index 6ddd047d549..a58712f2fec 100644 --- a/spec/frontend/performance_bar/components/detailed_metric_spec.js +++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js @@ -1,24 +1,40 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import DetailedMetric from '~/performance_bar/components/detailed_metric.vue'; import RequestWarning from '~/performance_bar/components/request_warning.vue'; +import { sortOrders } from '~/performance_bar/constants'; describe('detailedMetric', () => { let wrapper; - const createComponent = (props) => { - wrapper = shallowMount(DetailedMetric, { - propsData: { - ...props, - }, - }); + const defaultProps = { + currentRequest: {}, + metric: 'gitaly', + header: 'Gitaly calls', + keys: ['feature', 'request'], + }; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + shallowMount(DetailedMetric, { + propsData: { ...defaultProps, ...props }, + }), + ); }; const findAllTraceBlocks = () => wrapper.findAll('pre'); const findTraceBlockAtIndex = (index) => findAllTraceBlocks().at(index); - const findExpandBacktraceBtns = () => wrapper.findAll('[data-testid="backtrace-expand-btn"]'); + const findExpandBacktraceBtns = () => wrapper.findAllByTestId('backtrace-expand-btn'); const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index); + const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label'); + const findSortOrderSwitcher = () => wrapper.findByTestId('performance-bar-sort-order'); + const findEmptyDetailNotice = () => wrapper.findByTestId('performance-bar-empty-detail-notice'); + const findAllDetailDurations = () => + wrapper.findAllByTestId('performance-item-duration').wrappers.map((w) => w.text()); + const findAllSummaryItems = () => + wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text()); afterEach(() => { wrapper.destroy(); @@ -26,13 +42,7 @@ describe('detailedMetric', () => { describe('when the current request has no details', () => { beforeEach(() => { - createComponent({ - currentRequest: {}, - metric: 'gitaly', - header: 'Gitaly calls', - details: 'details', - keys: ['feature', 'request'], - }); + createComponent(); }); it('does not render the element', () => { @@ -42,36 +52,104 @@ describe('detailedMetric', () => { describe('when the current request has details', () => { const requestDetails = [ - { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] }, { - duration: '23', + duration: 23, feature: 'rebase_in_progress', request: '', backtrace: ['other', 'example'], }, + { duration: 100, feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] }, ]; - describe('with a default metric name', () => { + describe('with an empty detail', () => { + beforeEach(() => { + createComponent({ + currentRequest: { + details: { + gitaly: { + duration: '0ms', + calls: 0, + details: [], + warnings: [], + }, + }, + }, + }); + }); + + it('displays an empty title', () => { + expect(findDetailsLabel().text()).toBe('0'); + }); + + it('displays an empty modal', () => { + expect(findEmptyDetailNotice().text()).toContain('No gitaly calls for this request'); + }); + + it('does not display sort by switcher', () => { + expect(findSortOrderSwitcher().exists()).toBe(false); + }); + }); + + describe('when the details have a summary field', () => { beforeEach(() => { createComponent({ currentRequest: { details: { gitaly: { duration: '123ms', - calls: '456', + calls: 456, details: requestDetails, warnings: ['gitaly calls: 456 over 30'], + summary: { + 'In controllers': 100, + 'In middlewares': 20, + }, }, }, }, - metric: 'gitaly', - header: 'Gitaly calls', - keys: ['feature', 'request'], }); }); - it('displays details', () => { - expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456'); + it('displays a summary section', () => { + expect(findAllSummaryItems()).toEqual([ + 'Total 456', + 'Total duration 123ms', + 'In controllers 100', + 'In middlewares 20', + ]); + }); + }); + + describe("when the details don't have a start field", () => { + beforeEach(() => { + createComponent({ + currentRequest: { + details: { + gitaly: { + duration: '123ms', + calls: 456, + details: requestDetails, + warnings: ['gitaly calls: 456 over 30'], + }, + }, + }, + }); + }); + + it('displays details header', () => { + expect(findDetailsLabel().text()).toBe('123ms / 456'); + }); + + it('displays a basic summary section', () => { + expect(findAllSummaryItems()).toEqual(['Total 456', 'Total duration 123ms']); + }); + + it('sorts the details by descending duration order', () => { + expect(findAllDetailDurations()).toEqual(['100ms', '23ms']); + }); + + it('does not display sort by switcher', () => { + expect(findSortOrderSwitcher().exists()).toBe(false); }); it('adds a modal with a table of the details', () => { @@ -119,17 +197,75 @@ describe('detailedMetric', () => { findExpandedBacktraceBtnAtIndex(0).vm.$emit('click'); await nextTick(); expect(findAllTraceBlocks()).toHaveLength(1); - expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]); + expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]); secondExpandButton.vm.$emit('click'); await nextTick(); expect(findAllTraceBlocks()).toHaveLength(2); - expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[1].backtrace[0]); + expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[0].backtrace[0]); secondExpandButton.vm.$emit('click'); await nextTick(); expect(findAllTraceBlocks()).toHaveLength(1); - expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]); + expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]); + }); + }); + + describe('when the details have a start field', () => { + const requestDetailsWithStart = [ + { + start: '2021-03-18 11:41:49.846356 +0700', + duration: 23, + feature: 'rebase_in_progress', + request: '', + }, + { + start: '2021-03-18 11:42:11.645711 +0700', + duration: 75, + feature: 'find_commit', + request: 'abcdef', + }, + { + start: '2021-03-18 11:42:10.645711 +0700', + duration: 100, + feature: 'find_commit', + request: 'abcdef', + }, + ]; + + beforeEach(() => { + createComponent({ + currentRequest: { + details: { + gitaly: { + duration: '123ms', + calls: 456, + details: requestDetailsWithStart, + warnings: ['gitaly calls: 456 over 30'], + }, + }, + }, + metric: 'gitaly', + header: 'Gitaly calls', + keys: ['feature', 'request'], + }); + }); + + it('sorts the details by descending duration order', () => { + expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']); + }); + + it('displays sort by switcher', () => { + expect(findSortOrderSwitcher().exists()).toBe(true); + }); + + it('allows switch sorting orders', async () => { + findSortOrderSwitcher().vm.$emit('input', sortOrders.CHRONOLOGICAL); + await nextTick(); + expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']); + findSortOrderSwitcher().vm.$emit('input', sortOrders.DURATION); + await nextTick(); + expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']); }); }); @@ -145,10 +281,7 @@ describe('detailedMetric', () => { }, }, }, - metric: 'gitaly', title: 'custom', - header: 'Gitaly calls', - keys: ['feature', 'request'], }); }); @@ -156,31 +289,39 @@ describe('detailedMetric', () => { expect(wrapper.text()).toContain('custom'); }); }); - }); - describe('when the details has no duration', () => { - beforeEach(() => { - createComponent({ - currentRequest: { - details: { - bullet: { - calls: '456', - details: [{ notification: 'notification', backtrace: 'backtrace' }], + describe('when the details has no duration', () => { + beforeEach(() => { + createComponent({ + currentRequest: { + details: { + bullet: { + calls: '456', + details: [{ notification: 'notification', backtrace: 'backtrace' }], + }, }, }, - }, - metric: 'bullet', - header: 'Bullet notifications', - keys: ['notification'], + metric: 'bullet', + header: 'Bullet notifications', + keys: ['notification'], + }); + }); + + it('displays calls in the label', () => { + expect(findDetailsLabel().text()).toBe('456'); + }); + + it('displays a basic summary section', () => { + expect(findAllSummaryItems()).toEqual(['Total 456']); }); - }); - it('renders only the number of calls', async () => { - expect(trimText(wrapper.text())).toEqual('456 notification bullet'); + it('renders only the number of calls', async () => { + expect(trimText(wrapper.text())).toContain('notification bullet'); - findExpandedBacktraceBtnAtIndex(0).vm.$emit('click'); - await nextTick(); - expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet'); + findExpandedBacktraceBtnAtIndex(0).vm.$emit('click'); + await nextTick(); + expect(trimText(wrapper.text())).toContain('notification backtrace bullet'); + }); }); }); }); diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb index dad5a2bf461..9eeeca4de61 100644 --- a/spec/lib/peek/views/active_record_spec.rb +++ b/spec/lib/peek/views/active_record_spec.rb @@ -5,14 +5,16 @@ require 'spec_helper' RSpec.describe Peek::Views::ActiveRecord, :request_store do subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } } - let(:connection) { double(:connection) } + let(:connection_1) { double(:connection) } + let(:connection_2) { double(:connection) } + let(:connection_3) { double(:connection) } let(:event_1) do { name: 'SQL', sql: 'SELECT * FROM users WHERE id = 10', cached: false, - connection: connection + connection: connection_1 } end @@ -21,7 +23,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do name: 'SQL', sql: 'SELECT * FROM users WHERE id = 10', cached: true, - connection: connection + connection: connection_2 } end @@ -30,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do name: 'SQL', sql: 'UPDATE users SET admin = true WHERE id = 10', cached: false, - connection: connection + connection: connection_3 } end before do allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) + allow(connection_1).to receive(:transaction_open?).and_return(false) + allow(connection_2).to receive(:transaction_open?).and_return(false) + allow(connection_3).to receive(:transaction_open?).and_return(true) end it 'subscribes and store data into peek views' do @@ -46,22 +51,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do end expect(subject.results).to match( - calls: '3 (1 cached)', + calls: 3, + summary: { + "Cached" => 1, + "In a transaction" => 1 + }, duration: '6000.00ms', warnings: ["active-record duration: 6000.0 over 3000"], details: contain_exactly( a_hash_including( + start: be_a(Time), cached: '', + transaction: '', duration: 1000.0, sql: 'SELECT * FROM users WHERE id = 10' ), a_hash_including( - cached: 'cached', + start: be_a(Time), + cached: 'Cached', + transaction: '', duration: 2000.0, sql: 'SELECT * FROM users WHERE id = 10' ), a_hash_including( + start: be_a(Time), cached: '', + transaction: 'In a transaction', duration: 3000.0, sql: 'UPDATE users SET admin = true WHERE id = 10' ) diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 01da379e001..88496c74e44 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -870,4 +870,23 @@ RSpec.describe CommitStatus do it { is_expected.to eq(false) } end end + + describe '#update_older_statuses_retried!' do + let!(:build_old) { create_status(name: 'build') } + let!(:build_new) { create_status(name: 'build') } + let!(:test) { create_status(name: 'test') } + let!(:build_from_other_pipeline) do + new_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id) + create_status(name: 'build', pipeline: new_pipeline) + end + + it "updates 'retried' and 'status' columns of the latest status with the same name in the same pipeline" do + build_new.update_older_statuses_retried! + + expect(build_new.reload).to have_attributes(retried: false, processed: false) + expect(build_old.reload).to have_attributes(retried: true, processed: true) + expect(test.reload).to have_attributes(retried: false, processed: false) + expect(build_from_other_pipeline.reload).to have_attributes(retried: false, processed: false) + end + end end diff --git a/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb new file mode 100644 index 00000000000..1ceff0dd681 --- /dev/null +++ b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_relative '../../../../rubocop/cop/gitlab/delegate_predicate_methods' + +RSpec.describe RuboCop::Cop::Gitlab::DelegatePredicateMethods do + subject(:cop) { described_class.new } + + it 'registers offense for single predicate method with allow_nil:true' do + expect_offense(<<~SOURCE) + delegate :is_foo?, :do_foo, to: :bar, allow_nil: true + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using `delegate` with `allow_nil` on the following predicate methods is discouraged: is_foo?. + SOURCE + end + + it 'registers offense for multiple predicate methods with allow_nil:true' do + expect_offense(<<~SOURCE) + delegate :is_foo?, :is_bar?, to: :bar, allow_nil: true + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using `delegate` with `allow_nil` on the following predicate methods is discouraged: is_foo?, is_bar?. + SOURCE + end + + it 'registers no offense for non-predicate method with allow_nil:true' do + expect_no_offenses(<<~SOURCE) + delegate :do_foo, to: :bar, allow_nil: true + SOURCE + end + + it 'registers no offense with predicate method with allow_nil:false' do + expect_no_offenses(<<~SOURCE) + delegate :is_foo?, to: :bar, allow_nil: false + SOURCE + end + + it 'registers no offense with predicate method without allow_nil' do + expect_no_offenses(<<~SOURCE) + delegate :is_foo?, to: :bar + SOURCE + end +end diff --git a/vendor/assets/javascripts/jquery.endless-scroll.js b/vendor/assets/javascripts/jquery.endless-scroll.js index f022d9a5d06..fe0fe5b5a8e 100644 --- a/vendor/assets/javascripts/jquery.endless-scroll.js +++ b/vendor/assets/javascripts/jquery.endless-scroll.js @@ -19,7 +19,6 @@ * // using some custom options * $(document).endlessScroll({ * fireOnce: false, - * fireDelay: false, * loader: "<div class=\"loading\"><div>", * callback: function(){ * alert("test"); @@ -30,7 +29,6 @@ * * bottomPixels integer the number of pixels from the bottom of the page that triggers the event * fireOnce boolean only fire once until the execution of the current event is completed - * fireDelay integer delay the subsequent firing, in milliseconds, 0 or false to disable delay * loader string the HTML to be displayed during loading * data string|function plain HTML data, can be either a string or a function that returns a string, * when passed as a function it accepts one argument: fire sequence (the number @@ -55,7 +53,6 @@ var defaults = { bottomPixels : 50, fireOnce : true, - fireDelay : 150, loader : "<br />Loading...<br />", data : "", insertAfter : "div:last", @@ -102,21 +99,11 @@ data = typeof options.data == 'function' ? options.data.apply(this, [fireSequence]) : options.data; if (data !== false) { - $(options.insertAfter).after("<div id=\"endless_scroll_data\">" + data + "</div>"); - $("#endless_scroll_data").hide().fadeIn(250, function() {$(this).removeAttr("id");}); + $(options.insertAfter).after("<div>" + data + "</div>"); options.callback.apply(this, [fireSequence]); - if (options.fireDelay !== false || options.fireDelay !== 0) { - $("body").after("<div id=\"endless_scroll_marker\"></div>"); - // slight delay for preventing event firing twice - $("#endless_scroll_marker").fadeTo(options.fireDelay, 1, function() { - $(this).remove(); - fired = false; - }); - } - else - fired = false; + fired = false; } $("#endless_scroll_loader").remove(); |