summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-25 00:08:34 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-25 00:08:34 +0000
commit1e6730a4e32f6cbf4b84aa9fc13204778783f33c (patch)
tree6dd6c9ed98ec836432cf431397a4ef45dd78deb8
parent95a48f11db963bc55ab918e3eb24f8576dca4a81 (diff)
downloadgitlab-ce-1e6730a4e32f6cbf4b84aa9fc13204778783f33c.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.eslintrc.yml8
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue8
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue36
-rw-r--r--app/assets/javascripts/frequent_items/utils.js10
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue21
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue2
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue20
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue34
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue12
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue25
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue8
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue43
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue12
-rw-r--r--app/assets/javascripts/terraform/components/terraform_list.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss6
-rw-r--r--app/assets/stylesheets/utilities.scss32
-rw-r--r--app/graphql/mutations/packages/destroy.rb37
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/service_desk_setting.rb5
-rw-r--r--app/services/packages/destroy_package_service.rb33
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/layouts/minimal.html.haml3
-rw-r--r--app/views/projects/branches/new.html.haml8
-rw-r--r--app/views/projects/issues/_alert_moved_from_service_desk.html.haml9
-rw-r--r--app/views/registrations/experience_levels/show.html.haml1
-rw-r--r--doc/api/graphql/reference/index.md18
-rw-r--r--doc/user/application_security/dast/index.md1
-rw-r--r--doc/user/project/merge_requests/code_quality.md14
-rw-r--r--lib/api/project_packages.rb4
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js12
-rw-r--r--spec/frontend/frequent_items/utils_spec.js47
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js3
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js7
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js14
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js2
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap6
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js3
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap30
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js10
-rw-r--r--spec/models/service_desk_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_package_spec.rb93
-rw-r--r--spec/services/packages/destroy_package_service_spec.rb61
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue6
62 files changed, 602 insertions, 229 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b6abb574e19..b1847fd9c6b 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -17,6 +17,14 @@ settings:
webpack:
config: './config/webpack.config.js'
rules:
+ # BEGIN Disallow deprecated slot syntax
+ # TODO: Remove once
+ # https://gitlab.com/gitlab-org/frontend/eslint-plugin/-/issues/31 is closed
+ # and consumed by GitLab.
+ vue/no-deprecated-scope-attribute: error
+ vue/no-deprecated-slot-attribute: error
+ vue/no-deprecated-slot-scope-attribute: error
+ # END Disallow deprecated slot syntax
import/no-commonjs: error
import/no-default-export: off
no-underscore-dangle:
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index e6de724512f..96c3b8276ee 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -94,9 +94,11 @@ export default {
@handleUpdateNote="update"
@toggleResolveStatus="toggleResolveDiscussion(draft.id)"
>
- <strong slot="note-header-info" class="badge draft-pending-label gl-mr-2">
- {{ __('Pending') }}
- </strong>
+ <template #note-header-info>
+ <strong class="badge draft-pending-label gl-mr-2">
+ {{ __('Pending') }}
+ </strong>
+ </template>
</noteable-note>
</ul>
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 25ce6500094..512f060e2ea 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -204,15 +204,16 @@ export default {
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- class="gl-mb-5"
- :width="width"
- :data="masterChartData"
- :option="masterChartOptions"
- :height="masterChartHeight"
- @created="onMasterChartCreated"
- />
+ <template #default="{ width }">
+ <gl-area-chart
+ class="gl-mb-5"
+ :width="width"
+ :data="masterChartData"
+ :option="masterChartOptions"
+ :height="masterChartHeight"
+ @created="onMasterChartCreated"
+ />
+ </template>
</resizable-chart-container>
<div class="row">
@@ -226,14 +227,15 @@ export default {
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
</p>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- :width="width"
- :data="contributor.dates"
- :option="individualChartOptions"
- :height="individualChartHeight"
- @created="onIndividualChartCreated"
- />
+ <template #default="{ width }">
+ <gl-area-chart
+ :width="width"
+ :data="contributor.dates"
+ :option="individualChartOptions"
+ :height="individualChartHeight"
+ @created="onIndividualChartCreated"
+ />
+ </template>
</resizable-chart-container>
</div>
</div>
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index 88519d934cb..27ef47df8c8 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -35,13 +35,15 @@ export const getTopFrequentItems = (items) => {
};
export const updateExistingFrequentItem = (frequentItem, item) => {
- const accessedOverHourAgo =
- Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
+ // `frequentItem` comes from localStorage and it's possible it doesn't have a `lastAccessedOn`
+ const neverAccessed = !frequentItem.lastAccessedOn;
+ const shouldUpdate =
+ neverAccessed || Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
return {
...item,
- frequency: accessedOverHourAgo ? frequentItem.frequency + 1 : frequentItem.frequency,
- lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
+ frequency: shouldUpdate ? frequentItem.frequency + 1 : frequentItem.frequency,
+ lastAccessedOn: shouldUpdate ? Date.now() : frequentItem.lastAccessedOn,
};
};
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
index 3cbd5620063..c216a05bdb0 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_form.vue
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -72,16 +72,17 @@ export default {
:show-suggest-popover="true"
:textarea-value="issuableDescription"
>
- <textarea
- id="issuable-description"
- ref="textarea"
- slot="textarea"
- v-model="issuableDescription"
- dir="auto"
- class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- ></textarea>
+ <template #textarea>
+ <textarea
+ id="issuable-description"
+ ref="textarea"
+ v-model="issuableDescription"
+ dir="auto"
+ class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ ></textarea>
+ </template>
</markdown-field>
</div>
</div>
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index a4bd8ffd708..7c21e33d892 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -116,7 +116,7 @@ export default {
:title-link-attributes="tab.attrs"
:query-param-value="tab.queryParamValue"
>
- <template slot="title">
+ <template #title>
<span>{{ tab.title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ getTabCount(tab) }}</gl-badge>
</template>
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index 1db2d10db20..7cdfdba6f27 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -171,7 +171,7 @@ export default {
<template>
<gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
- <template slot="button-content">
+ <template #button-content>
<span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
selectedMilestonesLabel
}}</span>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 99008d047af..12f5e7efc96 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -402,22 +402,20 @@ export default {
@created="onChartCreated"
@updated="onChartUpdated"
>
- <template v-if="tooltip.type === 'deployments'">
- <template slot="tooltip-title">
+ <template #tooltip-title>
+ <template v-if="tooltip.type === 'deployments'">
{{ __('Deployed') }}
</template>
- <div slot="tooltip-content" class="d-flex align-items-center">
+ <div v-else class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template #tooltip-content>
+ <div v-if="tooltip.type === 'deployments'" class="d-flex align-items-center">
<gl-icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
- </template>
- <template v-else>
- <template slot="tooltip-title">
- <div class="text-nowrap">
- {{ tooltip.title }}
- </div>
- </template>
- <template slot="tooltip-content" :tooltip="tooltip">
+ <template v-else>
<div
v-for="(content, key) in tooltip.content"
:key="key"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 94cfb562ce3..8e5a0b5cda2 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -138,10 +138,10 @@ export default {
</script>
<template>
- <!--
+ <!--
This component should be replaced with a variant developed
as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
- The variant will create a dropdown with an icon, no text and no caret
+ The variant will create a dropdown with an icon, no text and no caret
-->
<gl-dropdown
v-gl-tooltip
@@ -177,20 +177,22 @@ export default {
@formValidation="setFormValidity"
/>
</form>
- <div slot="modal-footer">
- <gl-button @click="hideAddMetricModal">
- {{ __('Cancel') }}
- </gl-button>
- <gl-button
- v-track-event="getAddMetricTrackingOptions()"
- data-testid="add-metric-modal-submit-button"
- :disabled="!customMetricsFormIsValid"
- variant="success"
- @click="submitCustomMetricsForm"
- >
- {{ __('Save changes') }}
- </gl-button>
- </div>
+ <template #modal-footer>
+ <div>
+ <gl-button @click="hideAddMetricModal">
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ v-track-event="getAddMetricTrackingOptions()"
+ data-testid="add-metric-modal-submit-button"
+ :disabled="!customMetricsFormIsValid"
+ variant="success"
+ @click="submitCustomMetricsForm"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
+ </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
index 6f98f85ff90..36e4a278da9 100644
--- a/app/assets/javascripts/nav/components/top_nav_container_view.vue
+++ b/app/assets/javascripts/nav/components/top_nav_container_view.vue
@@ -20,6 +20,10 @@ export default {
type: String,
required: true,
},
+ currentItem: {
+ type: Object,
+ required: true,
+ },
containerClass: {
type: String,
required: false,
@@ -43,6 +47,12 @@ export default {
{ id: 'secondary', menuItems: this.linksSecondary },
].filter((x) => x.menuItems?.length);
},
+ currentItemTimestamped() {
+ return {
+ ...this.currentItem,
+ lastAccessedOn: Date.now(),
+ };
+ },
},
mounted() {
// For historic reasons, the frequent-items-app component requires this too start up.
@@ -62,7 +72,7 @@ export default {
>
<div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
<vuex-module-provider :vuex-module="frequentItemsVuexModule">
- <frequent-items-app v-bind="$attrs" />
+ <frequent-items-app :current-item="currentItemTimestamped" v-bind="$attrs" />
</vuex-module-provider>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index dfe2763d8bd..0892276ff3b 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -130,15 +130,18 @@ export default {
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
- <note-edited-text
- v-if="discussion.resolved"
- slot="discussion-resolved-text"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #discussion-resolved-text>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ </template>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
</component>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
@@ -175,7 +178,9 @@ export default {
:discussion-resolve-path="discussion.resolve_path"
@handleDeleteNote="$emit('deleteNote')"
>
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot v-if="index === 0" name="avatar-badge"></slot>
+ </template>
</component>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index a66b8be652f..c503fb561ee 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -265,7 +265,9 @@ export default {
@startReplying="showReplyForm"
@deleteNote="deleteNoteHandler"
>
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
<template #footer="{ showReplies }">
<draft-note
v-if="showDraft(discussion.reply_id)"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 83942499f4e..c91ae1ed14e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -392,7 +392,9 @@ export default {
:img-alt="author.name"
:img-size="40"
>
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
</user-avatar-link>
</div>
<div class="timeline-content">
@@ -403,7 +405,9 @@ export default {
:note-id="note.id"
:is-confidential="note.confidential"
>
- <slot slot="note-header-info" name="note-header-info"></slot>
+ <template #note-header-info>
+ <slot name="note-header-info"></slot>
+ </template>
<span v-if="commit" v-safe-html="actionText"></span>
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 75c3b6d564c..ccdcd3c94e7 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -301,7 +301,7 @@ export default {
:state="form.fields.namespace.state"
required
>
- <template slot="first">
+ <template #first>
<option :value="null" disabled>{{ s__('ForkProject|Select a namespace') }}</option>
</template>
<option v-for="namespace in namespaces" :key="namespace.id" :value="namespace">
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index aaa9bb906b2..e708cd32fff 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -85,28 +85,29 @@ export default {
:action-cancel="$options.cancelProps"
@primary="onSubmit"
>
- <div slot="modal-title" class="modal-title-with-label">
- <gl-sprintf
- :message="
- s__(
- 'Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}',
- )
- "
- >
- <template #labelTitle>
- <span
- class="label color-label"
- :style="`background-color: ${labelColor}; color: ${labelTextColor};`"
- >
- {{ labelTitle }}
- </span>
- </template>
- <template #span="{ content }"
- ><span>{{ content }}</span></template
+ <template #modal-title>
+ <div class="modal-title-with-label">
+ <gl-sprintf
+ :message="
+ s__(
+ 'Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}',
+ )
+ "
>
- </gl-sprintf>
- </div>
-
+ <template #labelTitle>
+ <span
+ class="label color-label"
+ :style="`background-color: ${labelColor}; color: ${labelTextColor};`"
+ >
+ {{ labelTitle }}
+ </span>
+ </template>
+ <template #span="{ content }"
+ ><span>{{ content }}</span></template
+ >
+ </gl-sprintf>
+ </div>
+ </template>
{{ text }}
</gl-modal>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index fb00f58abae..4c083ed5496 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
@@ -9,6 +9,9 @@ export default {
GlAlert,
ServiceDeskSetting,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
inject: {
initialIsEnabled: {
default: false,
@@ -121,7 +124,7 @@ export default {
<template>
<div>
<gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
- {{ alertMessage }}
+ <span v-safe-html="alertMessage"></span>
</gl-alert>
<service-desk-setting
:is-enabled="isEnabled"
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
index 99bb2080610..5ce2c17f8de 100644
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
@@ -81,11 +81,13 @@ export default {
:invalid-feedback="urlError"
>
<gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" />
- <gl-sprintf slot="description" :message="description" class="text-gl-muted">
- <template #id>
- <strong>{{ __('0t1DgySidms') }}</strong>
- </template>
- </gl-sprintf>
+ <template #description>
+ <gl-sprintf :message="description" class="text-gl-muted">
+ <template #id>
+ <strong>{{ __('0t1DgySidms') }}</strong>
+ </template>
+ </gl-sprintf>
+ </template>
</gl-form-group>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue
index a18f33ebb1f..7eb79120fb8 100644
--- a/app/assets/javascripts/terraform/components/terraform_list.vue
+++ b/app/assets/javascripts/terraform/components/terraform_list.vue
@@ -98,7 +98,7 @@ export default {
<section>
<gl-tabs>
<gl-tab>
- <template slot="title">
+ <template #title>
<p class="gl-m-0">
{{ s__('Terraform|States') }}
<gl-badge v-if="statesCount">{{ statesCount }}</gl-badge>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 07de525b1fa..7ab8da93a57 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -629,11 +629,9 @@ export default {
input-id="squash-message-edit"
squash
>
- <commit-message-dropdown
- slot="header"
- v-model="squashCommitMessage"
- :commits="commits"
- />
+ <template #header>
+ <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ </template>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
@@ -641,14 +639,16 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
- <label slot="checkbox">
- <input
- id="include-description"
- type="checkbox"
- @change="updateMergeCommitMessage($event.target.checked)"
- />
- {{ __('Include merge request description') }}
- </label>
+ <template #checkbox>
+ <label>
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
+ />
+ {{ __('Include merge request description') }}
+ </label>
+ </template>
</commit-edit>
</ul>
</commits-header>
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index 13472b48e84..bab13fe7c75 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -68,7 +68,7 @@ export default {
split
@click="handleClick(selectedAction, $event)"
>
- <template slot="button-content">
+ <template #button-content>
<span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
{{ selectedAction.text }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
index 2552236a073..fb7105bd416 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
@@ -28,18 +28,23 @@ export default {
<slot></slot>
</p>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- v-bind="$attrs"
- :width="width"
- :height="$options.chartContainerHeight"
- :data="chartData"
- :include-legend-avg-max="false"
- :option="areaChartOptions"
- >
- <slot slot="tooltip-title" name="tooltip-title"></slot>
- <slot slot="tooltip-content" name="tooltip-content"></slot>
- </gl-area-chart>
+ <template #default="{ width }">
+ <gl-area-chart
+ v-bind="$attrs"
+ :width="width"
+ :height="$options.chartContainerHeight"
+ :data="chartData"
+ :include-legend-avg-max="false"
+ :option="areaChartOptions"
+ >
+ <template #tooltip-title>
+ <slot name="tooltip-title"></slot>
+ </template>
+ <template #tooltip-content>
+ <slot name="tooltip-content"></slot>
+ </template>
+ </gl-area-chart>
+ </template>
</resizable-chart-container>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index f4fd57e4cdc..0575d7f6404 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
@@ -46,9 +46,12 @@ export default {
:area-chart-options="chartOptions"
>
{{ dateRange }}
-
- <slot slot="tooltip-title" name="tooltip-title"></slot>
- <slot slot="tooltip-content" name="tooltip-content"></slot>
+ <template #tooltip-title>
+ <slot name="tooltip-title"></slot>
+ </template>
+ <template #tooltip-content>
+ <slot name="tooltip-content"></slot>
+ </template>
</ci-cd-analytics-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 546ee56355f..0b92c947fc7 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -7,7 +7,7 @@ import { __ } from '~/locale';
*
* @example
* <expand-button>
- * <template slot="expanded">
+ * <template #expanded>
* Text goes here.
* </template>
* </expand-button>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
index 96d99faa952..dd0c0358ef6 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -74,6 +74,8 @@ export default {
@hidden="syncHide"
>
<slot></slot>
- <slot slot="modal-footer" name="modal-footer" :ok="ok" :cancel="cancel"></slot>
+ <template #modal-footer>
+ <slot name="modal-footer" :ok="ok" :cancel="cancel"></slot>
+ </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 149909d263e..c3d861d74bc 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -111,7 +111,7 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-safe-html="actionTextHtml"></span>
- <template v-if="canSeeDescriptionVersion" slot="extra-controls">
+ <template v-if="canSeeDescriptionVersion" #extra-controls>
&middot;
<gl-button
variant="link"
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index c25b5b53070..6902a67fbb7 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -796,9 +796,15 @@ svg {
.gl-display-flex {
display: flex;
}
+.gl-display-block {
+ display: block;
+}
.gl-align-items-center {
align-items: center;
}
+.gl-w-full {
+ width: 100%;
+}
.gl-p-2 {
padding: 0.25rem;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 965e1806416..1d70698fb76 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -85,10 +85,6 @@
padding-bottom: $gl-spacing-scale-8;
}
-.gl-pt-11 {
- padding-top: $gl-spacing-scale-11;
-}
-
.gl-transition-property-stroke-opacity {
transition-property: stroke-opacity;
}
@@ -121,6 +117,20 @@
}
}
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1462
+.gl-md-mt-11 {
+ @media (min-width: $breakpoint-md) {
+ margin-top: $gl-spacing-scale-11;
+ }
+}
+
+// Same as above
+.gl-md-pt-11 {
+ @media (min-width: $breakpoint-md) {
+ padding-top: $gl-spacing-scale-11;
+ }
+}
+
// This is used to help prevent issues with margin collapsing.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing.
.gl-force-block-formatting-context::after {
@@ -142,6 +152,13 @@
}
}
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1463
+.gl-xs-mt-6 {
+ @media (max-width: $breakpoint-sm) {
+ margin-top: $gl-spacing-scale-6;
+ }
+}
+
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1165
.gl-xs-mb-4 {
@media (max-width: $breakpoint-sm) {
@@ -156,6 +173,13 @@
}
}
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1463
+.gl-xs-pt-6 {
+ @media (max-width: $breakpoint-sm) {
+ padding-top: $gl-spacing-scale-6;
+ }
+}
+
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
.gl-sm-pr-3 {
@media (min-width: $breakpoint-sm) {
diff --git a/app/graphql/mutations/packages/destroy.rb b/app/graphql/mutations/packages/destroy.rb
new file mode 100644
index 00000000000..979a54da6bd
--- /dev/null
+++ b/app/graphql/mutations/packages/destroy.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ class Destroy < ::Mutations::BaseMutation
+ graphql_name 'DestroyPackage'
+
+ authorize :destroy_package
+
+ argument :id,
+ ::Types::GlobalIDType[::Packages::Package],
+ required: true,
+ description: 'ID of the Package.'
+
+ def resolve(id:)
+ package = authorized_find!(id: id)
+
+ result = ::Packages::DestroyPackageService.new(container: package, current_user: current_user).execute
+
+ errors = result.error? ? Array.wrap(result[:message]) : []
+
+ {
+ errors: errors
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 6b1146f8f09..d321efecb1b 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -104,6 +104,7 @@ module Types
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create
+ mount_mutation Mutations::Packages::Destroy
end
end
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index c5203354b9d..1c854cc9941 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -8,7 +8,10 @@ class ServiceDeskSetting < ApplicationRecord
validate :valid_issue_template
validate :valid_project_key
validates :outgoing_name, length: { maximum: 255 }, allow_blank: true
- validates :project_key, length: { maximum: 255 }, allow_blank: true, format: { with: /\A[a-z0-9_]+\z/ }
+ validates :project_key,
+ length: { maximum: 255 },
+ allow_blank: true,
+ format: { with: /\A[a-z0-9_]+\z/, message: -> (setting, data) { _("can contain only lowercase letters, digits, and '_'.") } }
scope :with_project_key, ->(key) { where(project_key: key) }
diff --git a/app/services/packages/destroy_package_service.rb b/app/services/packages/destroy_package_service.rb
new file mode 100644
index 00000000000..697f1fa3ac8
--- /dev/null
+++ b/app/services/packages/destroy_package_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Packages
+ class DestroyPackageService < BaseContainerService
+ alias_method :package, :container
+
+ def execute
+ return service_response_error("You don't have access to this package", 403) unless user_can_delete_package?
+
+ package.destroy!
+
+ package.sync_maven_metadata(current_user)
+
+ service_response_success('Package was successfully deleted')
+ rescue StandardError
+ service_response_error('Failed to remove the package', 400)
+ end
+
+ private
+
+ def service_response_error(message, http_status)
+ ServiceResponse.error(message: message, http_status: http_status)
+ end
+
+ def service_response_success(message)
+ ServiceResponse.success(message: message)
+ end
+
+ def user_can_delete_package?
+ can?(current_user, :destroy_package, package.project)
+ end
+ end
+end
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 09b7f247450..a313ad7d23c 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -61,7 +61,7 @@
- if show_recaptcha_sign_up?
= recaptcha_tags nonce: content_security_policy_nonce
.submit-container
- = f.submit button_text, class: 'btn gl-button btn-confirm', data: { qa_selector: 'new_user_register_button' }
+ = f.submit button_text, class: 'btn gl-button btn-confirm gl-display-block gl-w-full', data: { qa_selector: 'new_user_register_button' }
= render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom
= render 'devise/shared/signup_omniauth_providers'
diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml
index c9b208de477..ec909fcc279 100644
--- a/app/views/layouts/minimal.html.haml
+++ b/app/views/layouts/minimal.html.haml
@@ -8,10 +8,11 @@
= render 'peek/bar'
= render "layouts/header/empty"
.layout-page
- .content-wrapper.content-wrapper-margin.gl-pt-11
+ .content-wrapper.content-wrapper-margin.gl-md-pt-11.gl-xs-pt-6
.alert-wrapper.gl-force-block-formatting-context
= render "layouts/broadcast"
.limit-container-width{ class: container_class }
%main#content-body.content
+ = render "layouts/flash" unless @hide_flash
= yield
= footer_message
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 6cb2c435a30..6de50d48721 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -2,10 +2,10 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- .gl-alert.gl-alert-danger
- = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = render 'shared/global_alert',
+ variant: :danger,
+ close_button_class: 'js-close',
+ is_contained: true do
.gl-alert-body
= @error
%h3.page-title
diff --git a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
index 9b142b08574..662270fb8e1 100644
--- a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
+++ b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
@@ -2,9 +2,10 @@
- service_desk_link_url = help_page_path('user/project/service_desk')
- service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url }
-.hide.gl-alert.gl-alert-warning.js-alert-moved-from-service-desk-warning.gl-mt-5{ role: 'alert' }
- = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', css_class: 'gl-icon')
+= render 'shared/global_alert',
+ variant: :warning,
+ is_contained: true,
+ close_button_class: 'js-close',
+ alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5' do
.gl-alert-body.gl-mr-3
= s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe }
diff --git a/app/views/registrations/experience_levels/show.html.haml b/app/views/registrations/experience_levels/show.html.haml
index f878245a48c..16e59757147 100644
--- a/app/views/registrations/experience_levels/show.html.haml
+++ b/app/views/registrations/experience_levels/show.html.haml
@@ -1,4 +1,5 @@
- page_title _('What’s your experience level?')
+- @hide_flash = true
.gl-display-flex.gl-flex-direction-column.gl-align-items-center
= image_tag 'learn-gitlab-avatar.jpg', width: '90'
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e7ee6d2b01d..420744d2f73 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1835,6 +1835,24 @@ Input type: `DestroyNoteInput`
| <a id="mutationdestroynoteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdestroynotenote"></a>`note` | [`Note`](#note) | The note after mutation. |
+### `Mutation.destroyPackage`
+
+Input type: `DestroyPackageInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdestroypackageclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdestroypackageid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the Package. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdestroypackageclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdestroypackageerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.destroySnippet`
Input type: `DestroySnippetInput`
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 4a96efc3d72..27311d4b55e 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -702,6 +702,7 @@ dast:
DAST_AUTH_REPORT: "true"
artifacts:
paths: [gl-dast-debug-auth-report.html]
+ when: always
```
### Available CI/CD variables
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 27642a9bd5d..876f1e9ed79 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -342,13 +342,13 @@ do this:
The Code Quality report artifact JSON file must contain an array of objects
with the following properties:
-| Name | Description |
-| ---------------------- | -------------------------------------------------------------------------------------- |
-| `description` | A description of the code quality violation. |
-| `fingerprint` | A unique fingerprint to identify the code quality violation. For example, an MD5 hash. |
-| `severity` | A severity string (can be `info`, `minor`, `major`, `critical`, or `blocker`). |
-| `location.path` | The relative path to the file containing the code quality violation. |
-| `location.lines.begin` | The line on which the code quality violation occurred. |
+| Name | Description |
+| ---------------------- | ----------------------------------------------------------------------------------------- |
+| `description` | A description of the code quality violation. |
+| `fingerprint` | A unique fingerprint to identify the code quality violation. For example, an MD5 hash. |
+| `severity` | A severity string (can be `info`, `minor`, `major`, `critical`, or `blocker`). |
+| `location.path` | The relative path to the file containing the code quality violation. |
+| `location.lines.begin` or `location.positions.begin.line` | The line on which the code quality violation occurred. |
Example:
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
index 276cbe50e42..c3918f71427 100644
--- a/lib/api/project_packages.rb
+++ b/lib/api/project_packages.rb
@@ -71,9 +71,7 @@ module API
.new(user_project, params[:package_id]).execute
destroy_conditionally!(package) do |package|
- if package.destroy
- package.sync_maven_metadata(current_user)
- end
+ ::Packages::DestroyPackageService.new(container: package, current_user: current_user).execute
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 81ec7baa924..acb0bfafedd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -37898,6 +37898,9 @@ msgstr ""
msgid "can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'"
msgstr ""
+msgid "can contain only lowercase letters, digits, and '_'."
+msgstr ""
+
msgid "can only be changed by a group admin."
msgstr ""
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index c2d488a465e..5d22823e974 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,5 +1,6 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -8,6 +9,14 @@ import { createDraft } from '../mock_data';
const localVue = createLocalVue();
+const NoteableNoteStub = stubComponent(NoteableNote, {
+ template: `
+ <div>
+ <slot name="note-header-info">Test</slot>
+ </div>
+ `,
+});
+
describe('Batch comments draft note component', () => {
let store;
let wrapper;
@@ -26,6 +35,9 @@ describe('Batch comments draft note component', () => {
store,
propsData,
localVue,
+ stubs: {
+ NoteableNote: NoteableNoteStub,
+ },
});
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js
index a7ab18b0d10..8c3841558f4 100644
--- a/spec/frontend/frequent_items/utils_spec.js
+++ b/spec/frontend/frequent_items/utils_spec.js
@@ -66,35 +66,36 @@ describe('Frequent Items utils spec', () => {
});
describe('updateExistingFrequentItem', () => {
- let mockedProject;
-
- beforeEach(() => {
- mockedProject = {
- ...mockProject,
- frequency: 1,
- lastAccessedOn: 1497979281815,
- };
+ const LAST_ACCESSED = 1497979281815;
+ const WITHIN_AN_HOUR = LAST_ACCESSED + HOUR_IN_MS;
+ const OVER_AN_HOUR = WITHIN_AN_HOUR + 1;
+ const EXISTING_ITEM = Object.freeze({
+ ...mockProject,
+ frequency: 1,
+ lastAccessedOn: 1497979281815,
});
- it('updates item if accessed over an hour ago', () => {
- const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+ it.each`
+ desc | existingProps | newProps | expected
+ ${'updates item if accessed over an hour ago'} | ${{}} | ${{ lastAccessedOn: OVER_AN_HOUR }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
+ ${'does not update is accessed with an hour'} | ${{}} | ${{ lastAccessedOn: WITHIN_AN_HOUR }} | ${{ lastAccessedOn: EXISTING_ITEM.lastAccessedOn, frequency: 1 }}
+ ${'updates if lastAccessedOn not found'} | ${{ lastAccessedOn: undefined }} | ${{ lastAccessedOn: WITHIN_AN_HOUR }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
+ `('$desc', ({ existingProps, newProps, expected }) => {
const newItem = {
- ...mockedProject,
- lastAccessedOn: newTimestamp,
+ ...EXISTING_ITEM,
+ ...newProps,
};
- const result = updateExistingFrequentItem(mockedProject, newItem);
-
- expect(result.frequency).toBe(mockedProject.frequency + 1);
- });
-
- it('does not update item if accessed within the hour', () => {
- const newItem = {
- ...mockedProject,
- lastAccessedOn: mockedProject.lastAccessedOn + HOUR_IN_MS,
+ const existingItem = {
+ ...EXISTING_ITEM,
+ ...existingProps,
};
- const result = updateExistingFrequentItem(mockedProject, newItem);
- expect(result.frequency).toBe(mockedProject.frequency);
+ const result = updateExistingFrequentItem(existingItem, newItem);
+
+ expect(result).toEqual({
+ ...newItem,
+ ...expected,
+ });
});
});
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
index a074fddf091..30b116bc35c 100644
--- a/spec/frontend/issuable_create/components/issuable_form_spec.js
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -23,6 +23,9 @@ const createComponent = ({
<button class="js-issuable-save">Submit issuable</button>
`,
},
+ stubs: {
+ MarkdownField,
+ },
});
};
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 754ddd96c9b..ea6e4f4a5ed 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -51,6 +51,8 @@ describe('Time series component', () => {
},
stubs: {
GlPopover: true,
+ GlLineChart,
+ GlAreaChart,
},
attachTo: document.body,
});
@@ -202,7 +204,7 @@ describe('Time series component', () => {
describe('when series is of line type', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper({}, mount);
wrapper.vm.formatTooltipText(mockLineSeriesData());
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index 6e98ca28071..dbb9fd5f603 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -43,6 +43,9 @@ describe('Actions menu', () => {
wrapper = shallowMount(ActionsMenu, {
propsData: { ...dashboardActionsMenuProps, ...props },
store,
+ stubs: {
+ GlModal,
+ },
...options,
});
};
@@ -82,7 +85,7 @@ describe('Actions menu', () => {
it('modal for custom metrics form is rendered', () => {
expect(findAddMetricModal().exists()).toBe(true);
- expect(findAddMetricModal().attributes().modalid).toBe('addMetric');
+ expect(findAddMetricModal().props('modalId')).toBe('addMetric');
});
it('add metric modal submit button exists', () => {
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
index 7221ea2c5cd..e1b443745e3 100644
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -111,6 +111,7 @@ describe('~/nav/components/responsive_app.vue', () => {
containerClass: 'gl-px-3',
frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace,
frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule,
+ currentItem: {},
linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary,
linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary,
};
@@ -118,6 +119,7 @@ describe('~/nav/components/responsive_app.vue', () => {
containerClass: 'gl-px-3',
frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace,
frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule,
+ currentItem: {},
linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary,
linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary,
};
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
index 06d2179b859..0218f09af0a 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { merge } from 'lodash';
import { nextTick } from 'vue';
import FrequentItemsApp from '~/frequent_items/components/app.vue';
import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants';
@@ -82,7 +83,9 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
it('renders frequent items app', () => {
expect(findFrequentItemsApp()).toEqual({
vuexModule: DEFAULT_PROPS.frequentItemsVuexModule,
- props: expect.objectContaining(TEST_OTHER_PROPS),
+ props: expect.objectContaining(
+ merge({ currentItem: { lastAccessedOn: Date.now() } }, TEST_OTHER_PROPS),
+ ),
attributes: expect.objectContaining(EXTRA_ATTRS),
});
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index cd24b9afbdf..59ac75f00e6 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,5 +1,5 @@
import { getByRole } from '@testing-library/dom';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -23,8 +23,8 @@ describe('DiscussionNotes', () => {
let wrapper;
const getList = () => getByRole(wrapper.element, 'list');
- const createComponent = (props) => {
- wrapper = shallowMount(DiscussionNotes, {
+ const createComponent = (props, mountingMethod = shallowMount) => {
+ wrapper = mountingMethod(DiscussionNotes, {
store,
propsData: {
discussion: discussionMock,
@@ -33,7 +33,11 @@ describe('DiscussionNotes', () => {
...props,
},
scopedSlots: {
- footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ footer: `
+ <template #default="{ showReplies }">
+ <p>showReplies:{{ showReplies }}</p>,
+ </template>
+ `,
},
slots: {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
@@ -112,7 +116,7 @@ describe('DiscussionNotes', () => {
});
it('passes down avatar-badge slot content', () => {
- createComponent();
+ createComponent({}, mount);
expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index c80ccfa8256..f9be0796546 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -155,7 +155,7 @@ describe('ForkForm component', () => {
describe('forks namespaces', () => {
beforeEach(() => {
mockGetRequest({ namespaces: MOCK_NAMESPACES_RESPONSE });
- createComponent();
+ createFullComponent();
});
it('make GET request from endpoint', async () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
index c37f6415898..fc51825f15b 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
@@ -21,11 +21,7 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
option="[object Object]"
thresholds=""
width="0"
- >
- <template />
-
- <template />
- </glareachart-stub>
+ />
</div>
</div>
`;
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index 882b7b55b3e..c622f86072d 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -47,6 +47,9 @@ describe('TerraformList', () => {
localVue,
apolloProvider,
propsData,
+ stubs: {
+ GlTab,
+ },
});
};
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index 5d09af50420..8214cedc4a1 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
@@ -63,7 +63,7 @@ describe('Commits edit component', () => {
beforeEach(() => {
createComponent({
header: `<div class="test-header">${testCommitMessage}</div>`,
- checkbox: `<label slot="checkbox" class="test-checkbox">${testLabel}</label >`,
+ checkbox: `<label class="test-checkbox">${testLabel}</label >`,
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 2d00cd8e8d4..cd77d442cbf 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -70,6 +70,9 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) =
mergeRequestWidgetGraphql,
},
},
+ stubs: {
+ CommitEdit,
+ },
});
};
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index eacc41ccdad..01fc6210d3f 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -121,7 +121,9 @@ describe('ImageDiffViewer', () => {
:new-size="newSize"
:old-size="oldSize"
>
- <span slot="image-overlay" class="overlay">test</span>
+ <template #image-overlay>
+ <span class="overlay">test</span>
+ </template>
</image-diff-viewer>
`),
}).$mount();
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index c0ee49f194f..9f819cc4e94 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -7,9 +7,11 @@ describe('Pagination links component', () => {
let glPaginatedList;
const template = `
- <div class="slot" slot-scope="{ listItem }">
- <span class="item">Item Name: {{listItem.id}}</span>
- </div>
+ <template #default="{ listItem }">
+ <div class="slot">
+ <span class="item">Item Name: {{ listItem.id }}</span>
+ </div>
+ </template>
`;
const props = {
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
index add0c36a120..cdfe311acd9 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
@@ -2,20 +2,22 @@
exports[`Resizable Chart Container renders the component 1`] = `
<div>
- <div
- class="slot"
- >
- <span
- class="width"
+ <template>
+ <div
+ class="slot"
>
- 0
- </span>
-
- <span
- class="height"
- >
- 0
- </span>
- </div>
+ <span
+ class="width"
+ >
+ 0
+ </span>
+
+ <span
+ class="height"
+ >
+ 0
+ </span>
+ </div>
+ </template>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
index 1fce3c5d0b0..40f0c0f29f2 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
@@ -16,10 +16,12 @@ describe('Resizable Chart Container', () => {
wrapper = mount(ResizableChartContainer, {
scopedSlots: {
default: `
- <div class="slot" slot-scope="{ width, height }">
- <span class="width">{{width}}</span>
- <span class="height">{{height}}</span>
- </div>
+ <template #default="{ width, height }">
+ <div class="slot">
+ <span class="width">{{width}}</span>
+ <span class="height">{{height}}</span>
+ </div>
+ </template>
`,
},
});
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index 8ccbd983ba1..f99ac84175c 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe ServiceDeskSetting do
it { is_expected.to validate_length_of(:outgoing_name).is_at_most(255) }
it { is_expected.to validate_length_of(:project_key).is_at_most(255) }
it { is_expected.to allow_value('abc123_').for(:project_key) }
- it { is_expected.not_to allow_value('abc 12').for(:project_key) }
+ it { is_expected.not_to allow_value('abc 12').for(:project_key).with_message("can contain only lowercase letters, digits, and '_'.") }
it { is_expected.not_to allow_value('Big val').for(:project_key) }
describe '.valid_issue_template' do
diff --git a/spec/requests/api/graphql/mutations/packages/destroy_package_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_package_spec.rb
new file mode 100644
index 00000000000..e5ced419ecf
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/packages/destroy_package_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroying a package' do
+ using RSpec::Parameterized::TableSyntax
+
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:package) { create(:package) }
+ let_it_be(:user) { create(:user) }
+
+ let(:project) { package.project }
+ let(:id) { package.to_global_id.to_s }
+
+ let(:query) do
+ <<~GQL
+ errors
+ GQL
+ end
+
+ let(:params) { { id: id } }
+ let(:mutation) { graphql_mutation(:destroy_package, params, query) }
+ let(:mutation_response) { graphql_mutation_response(:destroyPackage) }
+
+ shared_examples 'destroying the package' do
+ it 'destroy the package' do
+ expect(::Packages::DestroyPackageService)
+ .to receive(:new).with(container: package, current_user: user).and_call_original
+
+ expect { mutation_request }.to change { ::Packages::Package.count }.by(-1)
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ shared_examples 'denying the mutation request' do
+ it 'does not destroy the package' do
+ expect(::Packages::DestroyPackageService)
+ .not_to receive(:new).with(container: package, current_user: user)
+
+ expect { mutation_request }.not_to change { ::Packages::Package.count }
+
+ expect(mutation_response).to be_nil
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ describe 'post graphql mutation' do
+ subject(:mutation_request) { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying the package'
+ :developer | 'denying the mutation request'
+ :reporter | 'denying the mutation request'
+ :guest | 'denying the mutation request'
+ :anonymous | 'denying the mutation request'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'with invalid id' do
+ let(:params) { { id: 'gid://gitlab/Packages::Package/5555' } }
+
+ it_behaves_like 'denying the mutation request'
+ end
+
+ context 'when an error occures' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns the errors in the response' do
+ allow_next_found_instance_of(::Packages::Package) do |package|
+ allow(package).to receive(:destroy!).and_raise(StandardError)
+ end
+
+ mutation_request
+
+ expect(mutation_response['errors']).to eq(['Failed to remove the package'])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/destroy_package_service_spec.rb b/spec/services/packages/destroy_package_service_spec.rb
new file mode 100644
index 00000000000..92db8da968c
--- /dev/null
+++ b/spec/services/packages/destroy_package_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::DestroyPackageService do
+ let_it_be(:user) { create(:user) }
+
+ let!(:package) { create(:npm_package) }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(container: package, current_user: user) }
+
+ context 'when the user is authorized' do
+ before do
+ package.project.add_maintainer(user)
+ end
+
+ context 'when the destroy is successfull' do
+ it 'destroy the package' do
+ expect(package).to receive(:sync_maven_metadata).and_call_original
+ expect { service.execute }.to change { Packages::Package.count }.by(-1)
+ end
+
+ it 'returns a success ServiceResponse' do
+ response = service.execute
+
+ expect(response).to be_a(ServiceResponse)
+ expect(response).to be_success
+ expect(response.message).to eq("Package was successfully deleted")
+ end
+ end
+
+ context 'when the destroy is not successful' do
+ before do
+ allow(package).to receive(:destroy!).and_raise(StandardError, "test")
+ end
+
+ it 'returns an error ServiceResponse' do
+ response = service.execute
+
+ expect(package).not_to receive(:sync_maven_metadata)
+ expect(response).to be_a(ServiceResponse)
+ expect(response).to be_error
+ expect(response.message).to eq("Failed to remove the package")
+ expect(response.status).to eq(:error)
+ end
+ end
+ end
+
+ context 'when the user is not authorized' do
+ it 'returns an error ServiceResponse' do
+ response = service.execute
+
+ expect(response).to be_a(ServiceResponse)
+ expect(response).to be_error
+ expect(response.message).to eq("You don't have access to this package")
+ expect(response.status).to eq(:error)
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue
index e9f3acea9d8..2d354d605f8 100644
--- a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue
+++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue
@@ -10,7 +10,7 @@
@visible="onScrollerVisible"
v-on="listeners"
>
- <template slot-scope="{ item: itemWithSize, index, active }">
+ <template #default="{ item: itemWithSize, index, active }">
<slot
v-bind="{
item: itemWithSize.item,
@@ -20,10 +20,10 @@
}"
/>
</template>
- <template slot="before">
+ <template #before>
<slot name="before" />
</template>
- <template slot="after">
+ <template #after>
<slot name="after" />
</template>
</RecycleScroller>