diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 18:09:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 18:09:51 +0000 |
commit | bd979acf95124119d41f75d34cab231229f4dd81 (patch) | |
tree | a7ce5127c8ab6d42b27aa8342889c54d2f0090b2 | |
parent | 4bdfcf93f224edb9c4daff90d95b0c6c92766ea3 (diff) | |
download | gitlab-ce-bd979acf95124119d41f75d34cab231229f4dd81.tar.gz |
Add latest changes from gitlab-org/gitlab@master
143 files changed, 1596 insertions, 1032 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 2da26513452..fb0c3383fc5 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1365,6 +1365,84 @@ lib/gitlab/checks/** /**/javascripts/admin/application_settings/runner_token_expiration/ @gitlab-org/ci-cd/verify/frontend /**/javascripts/usage_quotas/pipelines/ @gitlab-org/ci-cd/verify/frontend @sheldonled @aalakkad @kpalchyk +## Verify:Runner Fleet Backend + +/app/controllers/admin/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/controllers/concerns/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/controllers/groups/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/controllers/projects/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/controllers/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/finders/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/mutations/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/resolvers/ci/*_runners_resolver.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/resolvers/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/types/ci/runner_*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/types/namespace/shared_runners_setting_enum.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/graphql/types/permission_types/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/models/ci/build_runner_session.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/models/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/models/concerns/ci/has_runner_executor.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/models/concerns/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/models/preloaders/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/policies/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/presenters/ci/runner_*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/serializers/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/services/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/app/workers/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/db/docs/ci_runner*.yml @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/controllers/ee/admin/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/controllers/ee/groups/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/graphql/ee/mutations/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/graphql/ee/types/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/graphql/resolvers/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/models/ee/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/policies/ee/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/services/audit_events/*runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/services/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/services/ee/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/app/workers/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/graphql/ee/mutations/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/graphql/resolvers/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/graphql/types/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/models/ee/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/policies/ee/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/requests/api/graphql/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/services/audit_events/*runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/services/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/ee/spec/workers/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/api/ci/helpers/runner.rb @gitlab-org/maintainers/cicd-verify @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/api/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/api/ci/runner.rb @gitlab-org/maintainers/cicd-verify @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/api/entities/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/gitlab/audit/ci_runner_token_author.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/gitlab/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/gitlab/seeders/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/lib/tasks/gitlab/seed/runner_fleet.rake @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/factories/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/finders/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/graphql/mutations/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/graphql/resolvers/ci/*runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/graphql/types/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/graphql/types/permission_types/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/lib/api/ci/helpers/runner_*_spec.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/lib/api/ci/helpers/runner_spec.rb @gitlab-org/maintainers/cicd-verify @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/lib/gitlab/ci/runner_*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/lib/gitlab/seeders/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/models/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/models/concerns/runners*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/models/preloaders/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/requests/api/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/requests/api/graphql/ci/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/requests/api/graphql/mutations/ci/runner/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/requests/api/graphql/mutations/ci/runners_registration_token/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/requests/api/graphql/project/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/services/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/support/shared_contexts/graphql/resolvers/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/support/shared_examples/features/runner*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/tasks/gitlab/seed/runner_fleet_*.rb @gitlab-org/ci-cd/runner-fleet-team/backend-approvers +/spec/workers/ci/runners/ @gitlab-org/ci-cd/runner-fleet-team/backend-approvers + # CI/CD templates require approval from specific owners. /lib/gitlab/ci/templates/ @gitlab-org/maintainers/cicd-templates /lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah diff --git a/.rubocop_todo/rspec/variable_name.yml b/.rubocop_todo/rspec/variable_name.yml deleted file mode 100644 index 8858fbd9eb7..00000000000 --- a/.rubocop_todo/rspec/variable_name.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -RSpec/VariableName: - Exclude: - - 'spec/models/user_spec.rb' diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js index c72a913aacd..f69bb8ad7cb 100644 --- a/app/assets/javascripts/api/projects_api.js +++ b/app/assets/javascripts/api/projects_api.js @@ -8,6 +8,7 @@ const PROJECT_ALL_MEMBERS_PATH = '/api/:version/projects/:id/members/all'; const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id'; const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size'; const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_locations'; +const PROJECT_SHARE_LOCATIONS = 'api/:version/projects/:id/share_locations'; export function getProjects(query, options, callback = () => {}) { const url = buildApiUrl(PROJECTS_PATH); @@ -70,3 +71,10 @@ export const getProjectMembers = (projectId, inherited = false) => { return axios.get(url); }; + +export const getProjectShareLocations = (projectId, params = {}) => { + const url = buildApiUrl(PROJECT_SHARE_LOCATIONS).replace(':id', projectId); + const defaultParams = { per_page: DEFAULT_PER_PAGE }; + + return axios.get(url, { params: { ...defaultParams, ...params } }); +}; diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index 9005c1b1220..6aa5bb715b2 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -117,9 +117,7 @@ export default { {{ __('Summary comment (optional)') }} </template> <div class="common-note-form gfm-form"> - <div - class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100" - > + <div class="comment-warning-wrapper-large gl-border-0 gl-bg-white"> <markdown-field :is-submitting="isSubmitting" :add-spacing-classes="false" diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index dc408f5a950..bcd92d09033 100644 --- a/app/assets/javascripts/behaviors/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js @@ -121,9 +121,7 @@ MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) const markdownPreview = new MarkdownPreview(); const previewButtonSelector = '.js-md-preview-button'; -const writeButtonSelector = '.js-md-write-button'; lastTextareaPreviewed = null; -const markdownToolbar = $('.md-header-toolbar'); $(document).on('markdown-preview:show', (e, $form) => { if (!$form) { @@ -134,13 +132,15 @@ $(document).on('markdown-preview:show', (e, $form) => { lastTextareaHeight = lastTextareaPreviewed.height(); // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).val('edit'); + $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Continue editing')); + $form.find(previewButtonSelector).addClass('gl-shadow-none! gl-bg-transparent!'); // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); - markdownToolbar.removeClass('active'); + $form.find('.md-header-toolbar, .js-zen-enter').addClass('gl-display-none!'); + markdownPreview.showPreview($form); }); @@ -155,14 +155,14 @@ $(document).on('markdown-preview:hide', (e, $form) => { } // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).val('preview'); + $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Preview')); // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); $form.find('.md-preview-holder').hide(); - markdownToolbar.addClass('active'); + $form.find('.md-header-toolbar, .js-zen-enter').removeClass('gl-display-none!'); markdownPreview.hideReferencedCommands($form); }); @@ -183,13 +183,26 @@ $(document).on('markdown-preview:toggle', (e, keyboardEvent) => { $(document).on('click', previewButtonSelector, function (e) { e.preventDefault(); const $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:show', [$form]); + const eventName = e.currentTarget.getAttribute('value') === 'preview' ? 'show' : 'hide'; + $(document).triggerHandler(`markdown-preview:${eventName}`, [$form]); +}); + +$(document).on('mousedown', previewButtonSelector, function (e) { + e.preventDefault(); + const $form = $(this).closest('form'); + $form.find(previewButtonSelector).removeClass('gl-shadow-none! gl-bg-transparent!'); +}); + +$(document).on('mouseenter', previewButtonSelector, function (e) { + e.preventDefault(); + const $form = $(this).closest('form'); + $form.find(previewButtonSelector).removeClass('gl-bg-transparent!'); }); -$(document).on('click', writeButtonSelector, function (e) { +$(document).on('mouseleave', previewButtonSelector, function (e) { e.preventDefault(); const $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:hide', [$form]); + $form.find(previewButtonSelector).addClass('gl-bg-transparent!'); }); export default MarkdownPreview; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 5c6e4665a5c..efb462f4778 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -186,13 +186,14 @@ export default class Shortcuts { } static toggleMarkdownPreview(e) { - // Check if short-cut was triggered while in Write Mode - const $target = $(e.target); - const $form = $target.closest('form'); + const $form = $(e.target).closest('form'); + const toggle = $('.js-md-preview-button', $form).get(0); + + if (!toggle) return; + + toggle.focus(); + toggle.click(); - if ($target.hasClass('js-note-text')) { - $('.js-md-preview-button', $form).focus(); - } $(document).triggerHandler('markdown-preview:toggle', [e]); } diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue index 5715635fd13..8fd3f03ff71 100644 --- a/app/assets/javascripts/blob/components/blob_edit_header.vue +++ b/app/assets/javascripts/blob/components/blob_edit_header.vue @@ -35,9 +35,7 @@ export default { <div class="gl-display-flex gl-align-items-center gl-w-full"> <gl-form-input v-model="name" - :placeholder=" - s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby') - " + :placeholder="s__('Snippets|File name (e.g. test.rb)')" name="snippet_file_name" class="form-control js-snippet-file-name" type="text" diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/index.js b/app/assets/javascripts/ci/runner/admin_runner_show/index.js index cbd25819303..f34cd5508ce 100644 --- a/app/assets/javascripts/ci/runner/admin_runner_show/index.js +++ b/app/assets/javascripts/ci/runner/admin_runner_show/index.js @@ -17,7 +17,7 @@ export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => { return null; } - const { runnerId, runnersPath } = el.dataset; + const { runnerId, runnersPath, emptyStateImage } = el.dataset; const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -26,6 +26,9 @@ export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => { return new Vue({ el, apolloProvider, + provide: { + emptyStateImage, + }, render(h) { return h(AdminRunnerShowApp, { props: { diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs.vue b/app/assets/javascripts/ci/runner/components/runner_jobs.vue index f5287f597ab..62e09346c2c 100644 --- a/app/assets/javascripts/ci/runner/components/runner_jobs.vue +++ b/app/assets/javascripts/ci/runner/components/runner_jobs.vue @@ -7,6 +7,7 @@ import { captureException } from '../sentry_utils'; import { getPaginationVariables } from '../utils'; import RunnerJobsTable from './runner_jobs_table.vue'; import RunnerPagination from './runner_pagination.vue'; +import RunnerJobsEmptyState from './runner_jobs_empty_state.vue'; export default { name: 'RunnerJobs', @@ -14,7 +15,9 @@ export default { GlSkeletonLoader, RunnerJobsTable, RunnerPagination, + RunnerJobsEmptyState, }, + props: { runner: { type: Object, @@ -75,7 +78,7 @@ export default { <gl-skeleton-loader /> </div> <runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" /> - <p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p> + <runner-jobs-empty-state v-else /> <runner-pagination :disabled="loading" :page-info="jobs.pageInfo" @input="onPaginationInput" /> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue new file mode 100644 index 00000000000..68fb38c1a29 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue @@ -0,0 +1,25 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + i18n: { + title: s__('Runners|This runner has not run any jobs'), + description: s__( + 'Runners|Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up.', + ), + }, + components: { + GlEmptyState, + }, + inject: ['emptyStateImage'], +}; +</script> + +<template> + <gl-empty-state :svg-path="emptyStateImage" :title="$options.i18n.title"> + <template #description> + <p>{{ $options.i18n.description }}</p> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index f9d48708473..2b2c4a5ac1c 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -3,7 +3,6 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2'; import { GlSprintf, GlLink } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { VARIANT_DANGER } from '~/alert'; -import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue'; import { createContentEditor } from '../services/create_content_editor'; import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants'; import ContentEditorAlert from './content_editor_alert.vue'; @@ -30,7 +29,6 @@ export default { LinkBubbleMenu, MediaBubbleMenu, EditorStateObserver, - EditorModeDropdown, }, props: { renderMarkdown: { @@ -100,6 +98,11 @@ export default { latestMarkdown: null, }; }, + computed: { + showPlaceholder() { + return this.placeholder && !this.markdown && !this.focused; + }, + }, watch: { markdown(markdown) { if (markdown !== this.latestMarkdown) { @@ -196,11 +199,6 @@ export default { markdown: this.latestMarkdown, }); }, - handleEditorModeChanged(mode) { - if (mode === 'markdown') { - this.$emit('enableMarkdownEditor'); - } - }, }, i18n: { quickActionsText: s__( @@ -226,34 +224,36 @@ export default { :class="{ 'is-focused': focused }" > <formatting-toolbar ref="toolbar" @enableMarkdownEditor="$emit('enableMarkdownEditor')" /> - <div class="gl-relative gl-mt-4"> + <div class="gl-relative"> <formatting-bubble-menu /> <code-block-bubble-menu /> <link-bubble-menu /> <media-bubble-menu /> - <div v-if="placeholder && !markdown && !focused" class="gl-absolute gl-text-gray-400"> + <div v-if="showPlaceholder" class="gl-absolute gl-text-gray-400 gl-px-5 gl-pt-4"> {{ placeholder }} </div> <tiptap-editor-content - class="md" + class="md gl-px-5" data-testid="content_editor_editablebox" :editor="contentEditor.tiptapEditor" /> <loading-indicator v-if="isLoading" /> - <div class="gl-display-flex gl-border-t gl-py-2 gl-text-secondary"> - <div class="gl-w-full"> - <template v-if="quickActionsDocsPath"> - <gl-sprintf :message="$options.i18n.quickActionsText"> - <template #keyboard="{ content }"> - <kbd>{{ content }}</kbd> - </template> - <template #quickActionsDocsLink="{ content }"> - <gl-link :href="quickActionsDocsPath" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </template> + <div + v-if="quickActionsDocsPath" + class="gl-display-flex gl-align-items-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-4 gl-mx-2 gl-mb-2 gl-bg-gray-10 gl-text-secondary" + > + <div class="gl-w-full gl-line-height-32 gl-font-sm"> + <gl-sprintf :message="$options.i18n.quickActionsText"> + <template #keyboard="{ content }"> + <kbd>{{ content }}</kbd> + </template> + <template #quickActionsDocsLink="{ content }"> + <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{ + content + }}</gl-link> + </template> + </gl-sprintf> </div> - <editor-mode-dropdown size="small" value="richText" @input="handleEditorModeChanged" /> </div> </div> </div> diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue index cd9fdeeca46..e7e520a55da 100644 --- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue @@ -1,4 +1,5 @@ <script> +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; import trackUIControl from '../services/track_ui_control'; import ToolbarButton from './toolbar_button.vue'; import ToolbarAttachmentButton from './toolbar_attachment_button.vue'; @@ -13,94 +14,106 @@ export default { ToolbarTableButton, ToolbarAttachmentButton, ToolbarMoreDropdown, + EditorModeSwitcher, }, methods: { trackToolbarControlExecution({ contentType, value }) { trackUIControl({ property: contentType, value }); }, + handleEditorModeChanged() { + this.$emit('enableMarkdownEditor'); + }, }, }; </script> <template> - <div - class="gl-w-full gl-border-b gl-display-flex gl-justify-content-end" - data-testid="formatting-toolbar" - > - <div class="gl-py-2 gl-display-flex gl-flex-wrap gl-align-items-end"> - <toolbar-text-style-dropdown - data-testid="text-styles" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="bold" - content-type="bold" - icon-name="bold" - editor-command="toggleBold" - :label="__('Bold text')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="italic" - content-type="italic" - icon-name="italic" - editor-command="toggleItalic" - :label="__('Italic text')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="blockquote" - content-type="blockquote" - icon-name="quote" - editor-command="toggleBlockquote" - :label="__('Insert a quote')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="code" - content-type="code" - icon-name="code" - editor-command="toggleCode" - :label="__('Code')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="link" - content-type="link" - icon-name="link" - editor-command="editLink" - :label="__('Insert link')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="bullet-list" - content-type="bulletList" - icon-name="list-bulleted" - class="gl-display-none gl-sm-display-inline" - editor-command="toggleBulletList" - :label="__('Add a bullet list')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="ordered-list" - content-type="orderedList" - icon-name="list-numbered" - class="gl-display-none gl-sm-display-inline" - editor-command="toggleOrderedList" - :label="__('Add a numbered list')" - @execute="trackToolbarControlExecution" - /> - <toolbar-button - data-testid="task-list" - content-type="taskList" - icon-name="list-task" - class="gl-display-none gl-sm-display-inline" - editor-command="toggleTaskList" - :label="__('Add a checklist')" - @execute="trackToolbarControlExecution" - /> - <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" /> - <toolbar-attachment-button data-testid="attachment" @execute="trackToolbarControlExecution" /> - <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" /> + <div class="gl-mx-2 gl-mt-2"> + <div + class="gl-w-full gl-display-flex gl-align-items-center gl-flex-wrap gl-bg-gray-50 gl-px-2 gl-rounded-base gl-justify-content-space-between" + data-testid="formatting-toolbar" + > + <div class="gl-py-2 gl-display-flex gl-flex-wrap-wrap"> + <toolbar-text-style-dropdown + data-testid="text-styles" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="bold" + content-type="bold" + icon-name="bold" + editor-command="toggleBold" + :label="__('Bold text')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="italic" + content-type="italic" + icon-name="italic" + editor-command="toggleItalic" + :label="__('Italic text')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="blockquote" + content-type="blockquote" + icon-name="quote" + editor-command="toggleBlockquote" + :label="__('Insert a quote')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="code" + content-type="code" + icon-name="code" + editor-command="toggleCode" + :label="__('Code')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="link" + content-type="link" + icon-name="link" + editor-command="editLink" + :label="__('Insert link')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="bullet-list" + content-type="bulletList" + icon-name="list-bulleted" + class="gl-display-none gl-sm-display-inline" + editor-command="toggleBulletList" + :label="__('Add a bullet list')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="ordered-list" + content-type="orderedList" + icon-name="list-numbered" + class="gl-display-none gl-sm-display-inline" + editor-command="toggleOrderedList" + :label="__('Add a numbered list')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button + data-testid="task-list" + content-type="taskList" + icon-name="list-task" + class="gl-display-none gl-sm-display-inline" + editor-command="toggleTaskList" + :label="__('Add a checklist')" + @execute="trackToolbarControlExecution" + /> + <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" /> + <toolbar-attachment-button + data-testid="attachment" + @execute="trackToolbarControlExecution" + /> + <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" /> + </div> + <div class="content-editor-switcher gl-display-flex gl-align-items-center gl-ml-auto"> + <editor-mode-switcher size="small" value="richText" @input="handleEditorModeChanged" /> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue index efb9a5b07b5..1f18090e7d7 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue @@ -46,6 +46,7 @@ export default { :title="__('Attach a file or image')" category="tertiary" icon="paperclip" + size="small" lazy @click="openFileUpload" /> diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue index cef026c5bc6..1f3c7062b67 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue @@ -47,7 +47,7 @@ export default { size: { type: String, required: false, - default: 'medium', + default: 'small', }, }, data() { @@ -82,6 +82,7 @@ export default { :aria-label="label" :title="label" :icon="iconName" + class="gl-mr-3" @click="execute" /> </editor-state-observer> diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js index 537c810bcff..a46a8d4affa 100644 --- a/app/assets/javascripts/deprecated_notes.js +++ b/app/assets/javascripts/deprecated_notes.js @@ -599,7 +599,10 @@ export default class Notes { // remove validation errors form.find('.js-errors').remove(); // reset text and preview - form.find('.js-md-write-button').click(); + if (form.find('.js-md-preview-button').val() === 'edit') { + form.find('.js-md-preview-button').click(); + } + form.find('.js-note-text').val('').trigger('input'); form.find('.js-note-text').each(function reset() { this.$autosave.reset(); @@ -939,6 +942,7 @@ export default class Notes { const replyLink = $(target).closest('.js-discussion-reply-button'); // insert the form after the button replyLink.closest('.discussion-reply-holder').hide().after(form); + // show the form return this.setupDiscussionNoteForm(replyLink, form); } @@ -1241,7 +1245,10 @@ export default class Notes { $editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-type').val(targetType); $editForm.find('.js-note-text').focus().val(originalContent); - $editForm.find('.js-md-write-button').trigger('click'); + // reset preview + if ($editForm.find('.js-md-preview-button').val() === 'edit') { + $editForm.find('.js-md-preview-button').click(); + } $editForm.find('.referenced-users').hide(); } diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue index 1b7320df674..ff2dd9935ae 100644 --- a/app/assets/javascripts/environments/environment_details/index.vue +++ b/app/assets/javascripts/environments/environment_details/index.vue @@ -1,5 +1,6 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { logError } from '~/lib/logger'; import { toggleQueryPollingByVisibility, etagQueryHeaders } from '~/graphql_shared/utils'; import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue'; @@ -147,6 +148,13 @@ export default { this.isPrefetchingPages = false; }, }, + errorCaptured(error) { + Sentry.withScope((scope) => { + scope.setTag('vue_component', 'EnvironmentDetailsIndex'); + + Sentry.captureException(error); + }); + }, mounted() { if (this.graphqlEtagKey) { toggleQueryPollingByVisibility( diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue index dee1d239c9f..37a0c679287 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle } from '@gitlab/ui'; +import { GlBadge, GlButton, GlTooltipDirective, GlIcon, GlModal, GlToggle } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { labelForStrategy } from '../utils'; @@ -15,6 +15,7 @@ export default { components: { GlBadge, GlButton, + GlIcon, GlModal, GlToggle, StrategyLabel, @@ -116,7 +117,12 @@ export default { </div> <template v-for="featureFlag in featureFlags"> - <div :key="featureFlag.id" class="gl-responsive-table-row" role="row"> + <div + :key="featureFlag.id" + :data-testid="featureFlag.id" + class="gl-responsive-table-row" + role="row" + > <div class="table-section section-10" role="gridcell"> <div class="table-mobile-header" role="rowheader">{{ s__('FeatureFlags|ID') }}</div> <div class="table-mobile-content js-feature-flag-id"> @@ -155,9 +161,14 @@ export default { <div class="feature-flag-name text-monospace text-truncate"> {{ featureFlag.name }} </div> - </div> - <div class="feature-flag-description text-secondary text-truncate"> - {{ featureFlag.description }} + <div class="feature-flag-description"> + <gl-icon + v-if="featureFlag.description" + v-gl-tooltip.hover="featureFlag.description" + class="gl-ml-3 gl-mr-3" + name="information-o" + /> + </div> </div> </div> </div> diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue index 0e9781d77fe..5bb3b6b98e6 100644 --- a/app/assets/javascripts/invite_members/components/group_select.vue +++ b/app/assets/javascripts/invite_members/components/group_select.vue @@ -8,7 +8,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; import { s__ } from '~/locale'; -import { getGroups, getDescendentGroups } from '~/rest_api'; +import { getGroups, getDescendentGroups, getProjectShareLocations } from '~/rest_api'; import { SEARCH_DELAY, GROUP_FILTERS } from '../constants'; export default { @@ -29,6 +29,10 @@ export default { required: false, default: GROUP_FILTERS.ALL, }, + sourceId: { + type: String, + required: true, + }, parentGroupId: { type: Number, required: false, @@ -38,6 +42,10 @@ export default { type: Array, required: true, }, + isProject: { + type: Boolean, + required: true, + }, }, data() { return { @@ -79,7 +87,7 @@ export default { const rawGroups = response.map((group) => ({ id: group.id, name: group.full_name, - path: group.path, + path: group.full_path, avatarUrl: group.avatar_url, })); @@ -94,6 +102,14 @@ export default { this.$emit('input', this.selectedGroup); }, fetchGroups() { + if (this.isProject) { + return new Promise((resolve, reject) => { + getProjectShareLocations(this.sourceId, { search: this.searchTerm }) + .then(({ data }) => resolve(data)) + .catch(reject); + }); + } + switch (this.groupsFilter) { case GROUP_FILTERS.DESCENDANT_GROUPS: return getDescendentGroups( diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue index 51355baef99..03513f74aa6 100644 --- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue @@ -203,8 +203,10 @@ export default { <group-select v-model="groupToBeSharedWith" :groups-filter="groupSelectFilter" + :source-id="id" :parent-group-id="groupSelectParentId" :invalid-groups="invalidGroups" + :is-project="isProject" @input="clearValidation" /> </template> diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue index 02d128eb119..cfe4baaa1f9 100644 --- a/app/assets/javascripts/notes/components/comment_field_layout.vue +++ b/app/assets/javascripts/notes/components/comment_field_layout.vue @@ -67,7 +67,7 @@ export default { </script> <template> <div - class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100" + class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-lg gl-border-gray-100 gl-bg-white" > <div v-if="withAlertContainer" diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue index 1dd07fe90d2..a0d2b47c89c 100644 --- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue +++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue @@ -21,7 +21,7 @@ export default { 'li', { class: - 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base clearfix', + 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base clearfix gl-pt-5', }, [h('ul', { class: 'notes' }, children)], ); diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index bce17aebd64..27fb116d213 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,11 +1,16 @@ <script> -import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui'; +import { + GlTooltipDirective, + GlIcon, + GlButton, + GlDisclosureDropdown, + GlDisclosureDropdownItem, +} from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import Api from '~/api'; import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import eventHub from '~/sidebar/event_hub'; import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; @@ -29,7 +34,8 @@ export default { ReplyButton, TimelineEventButton, GlButton, - GlDropdownItem, + GlDisclosureDropdown, + GlDisclosureDropdownItem, UserAccessRoleBadge, EmojiPicker: () => import('~/emoji/components/picker.vue'), AbuseCategorySelector, @@ -208,18 +214,23 @@ export default { methods: { ...mapActions(['toggleAwardRequest', 'promoteCommentToTimelineEvent']), onEdit() { + this.closeMoreActionsDropdown(); this.$emit('handleEdit'); }, onDelete() { + this.closeMoreActionsDropdown(); this.$emit('handleDelete'); }, onResolve() { this.$emit('handleResolve'); }, - closeTooltip() { - this.$nextTick(() => { - this.$root.$emit(BV_HIDE_TOOLTIP); - }); + onAbuse() { + this.closeMoreActionsDropdown(); + this.toggleReportAbuseDrawer(true); + }, + onCopyUrl() { + this.closeMoreActionsDropdown(); + this.$toast.show(__('Link copied to clipboard.')); }, handleAssigneeUpdate(assignees) { this.$emit('updateAssignees', assignees); @@ -230,6 +241,8 @@ export default { let { assignees } = this; const { project_id, iid } = this.getNoteableData; + this.closeMoreActionsDropdown(); + if (this.isUserAssigned) { assignees = assignees.filter((assignee) => assignee.id !== this.author.id); } else { @@ -258,6 +271,11 @@ export default { toggleReportAbuseDrawer(isOpen) { this.isReportAbuseDrawerOpen = isOpen; }, + closeMoreActionsDropdown() { + if (this.shouldShowActionsDropdown && this.$refs.moreActionsDropdown) { + this.$refs.moreActionsDropdown.close(); + } + }, }, }; </script> @@ -354,48 +372,61 @@ export default { class="note-action-button js-note-delete" @click="onDelete" /> - <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions"> - <!-- eslint-disable @gitlab/vue-no-data-toggle --> - <gl-button + <div v-else-if="shouldShowActionsDropdown" class="more-actions dropdown"> + <gl-disclosure-dropdown + ref="moreActionsDropdown" v-gl-tooltip :title="$options.i18n.moreActionsLabel" :aria-label="$options.i18n.moreActionsLabel" icon="ellipsis_v" category="tertiary" + placement="right" class="note-action-button more-actions-toggle" - data-toggle="dropdown" - @click="closeTooltip" - /> - <!-- eslint-enable @gitlab/vue-no-data-toggle --> - <ul class="dropdown-menu more-actions-dropdown dropdown-menu-right"> - <gl-dropdown-item + no-caret + > + <gl-disclosure-dropdown-item v-if="canEdit" class="js-note-edit gl-sm-display-none!" - @click.prevent="onEdit" + @action="onEdit" > - {{ __('Edit comment') }} - </gl-dropdown-item> - <gl-dropdown-item + <template #list-item> + {{ __('Edit comment') }} + </template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="canReportAsAbuse" data-testid="report-abuse-button" - @click="toggleReportAbuseDrawer(true)" + @action="onAbuse" > - {{ $options.i18n.reportAbuse }} - </gl-dropdown-item> - <gl-dropdown-item + <template #list-item> + {{ $options.i18n.reportAbuse }} + </template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="noteUrl" class="js-btn-copy-note-link" :data-clipboard-text="noteUrl" + @action="onCopyUrl" + > + <template #list-item> + {{ __('Copy link') }} + </template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item + v-if="canAssign" + data-testid="assign-user" + @action="assignUser" > - {{ __('Copy link') }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canAssign" data-testid="assign-user" @click="assignUser"> - {{ displayAssignUserText }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canEdit" class="js-note-delete" @click.prevent="onDelete"> - <span class="text-danger">{{ __('Delete comment') }}</span> - </gl-dropdown-item> - </ul> + <template #list-item> + {{ displayAssignUserText }} + </template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="canEdit" class="js-note-delete" @action="onDelete"> + <template #list-item> + <span class="text-danger">{{ __('Delete comment') }}</span> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown> </div> <!-- IMPORTANT: show this component lazily because it causes layout thrashing --> <!-- https://gitlab.com/gitlab-org/gitlab/-/issues/331172#note_1269378396 --> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 3375e366ecf..375b16f6ce2 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -319,7 +319,7 @@ export default { /> <li v-else-if="canShowReplyActions && showReplies" - :class="{ 'is-replying': isReplying }" + :class="{ 'is-replying gl-bg-white! gl-pt-0!': isReplying }" class="discussion-reply-holder gl-border-t-0! clearfix" > <discussion-actions diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 151c38d01dc..1678e51a29d 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -256,7 +256,7 @@ export default { <form-footer-actions> <template #prepend> <gl-button - class="js-no-auto-disable" + class="js-no-auto-disable gl-mr-2" category="primary" type="submit" variant="confirm" diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue index bac423f6838..3ce7ea231ff 100644 --- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue @@ -35,11 +35,7 @@ export default { <div class="js-collapsed" :class="{ 'd-none': value }"> <gl-form-input class="form-control" - :placeholder=" - s__( - 'Snippets|Optionally add a description about what your snippet does or how to use it…', - ) - " + :placeholder="s__('Snippets|Describe what your snippet does or how to use it…')" data-qa-selector="description_placeholder" /> </div> diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js index 8e4250d0e39..4bbef67742c 100644 --- a/app/assets/javascripts/super_sidebar/utils.js +++ b/app/assets/javascripts/super_sidebar/utils.js @@ -26,7 +26,9 @@ const sortItemsByFrequencyAndLastAccess = (items) => // This imitates getTopFrequentItems from app/assets/javascripts/frequent_items/utils.js, but // adjusts the rules to accommodate for the context switcher's designs. -export const getTopFrequentItems = (items = [], maxCount) => { +export const getTopFrequentItems = (items, maxCount) => { + if (!Array.isArray(items)) return []; + const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY); sortItemsByFrequencyAndLastAccess(frequentItems); diff --git a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue index fe221d2fefa..f22a17b4603 100644 --- a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue +++ b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue @@ -1,7 +1,7 @@ <template> - <footer class="form-actions d-flex justify-content-between"> - <div><slot name="prepend"></slot></div> - <div><slot></slot></div> - <div><slot name="append"></slot></div> + <footer class="gl-mt-5 footer-block"> + <slot name="prepend"></slot> + <slot></slot> + <slot name="append"></slot> </footer> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue index feee132629f..1377a40fcf0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue @@ -86,6 +86,7 @@ export default { category="tertiary" placement="right" searchable + size="small" class="comment-template-dropdown" :searching="$apollo.queries.savedReplies.loading" @shown="fetchCommentTemplates" diff --git a/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue index a66becb5c92..e88c7f75745 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue @@ -43,6 +43,7 @@ export default { :aria-label="__('Insert or edit diagram')" category="tertiary" icon="diagram" + size="small" @click="launchDrawioEditor" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue deleted file mode 100644 index 7803d6f53e0..00000000000 --- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue +++ /dev/null @@ -1,58 +0,0 @@ -<script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default { - components: { - GlDropdown, - GlDropdownItem, - }, - props: { - size: { - type: String, - required: false, - default: 'medium', - }, - value: { - type: String, - required: true, - }, - }, - computed: { - markdownEditorSelected() { - return this.value === 'markdown'; - }, - text() { - return this.markdownEditorSelected ? __('Editing markdown') : __('Editing rich text'); - }, - }, -}; -</script> -<template> - <gl-dropdown - category="tertiary" - data-qa-selector="editing_mode_switcher" - :size="size" - :text="text" - right - > - <gl-dropdown-item - is-check-item - :is-checked="!markdownEditorSelected" - @click="$emit('input', 'richText')" - ><div class="gl-font-weight-bold">{{ __('Rich text') }}</div> - <div class="gl-text-secondary"> - {{ __('View the formatted output in real-time as you edit.') }} - </div> - </gl-dropdown-item> - <gl-dropdown-item - is-check-item - :is-checked="markdownEditorSelected" - @click="$emit('input', 'markdown')" - ><div class="gl-font-weight-bold">{{ __('Markdown') }}</div> - <div class="gl-text-secondary"> - {{ __('View and edit markdown, with the option to preview the formatted output.') }} - </div></gl-dropdown-item - > - </gl-dropdown> -</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue new file mode 100644 index 00000000000..645975ca565 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue @@ -0,0 +1,32 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlButton, + }, + props: { + value: { + type: String, + required: true, + }, + }, + computed: { + markdownEditorSelected() { + return this.value === 'markdown'; + }, + text() { + return this.markdownEditorSelected ? __('Switch to rich text') : __('Switch to Markdown'); + }, + }, +}; +</script> +<template> + <gl-button + class="btn btn-default btn-sm gl-button btn-default-tertiary" + data-qa-selector="editing_mode_switcher" + @click="$emit('input')" + >{{ text }}</gl-button + > +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index cc153747765..3c4070105d1 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import $ from 'jquery'; import { debounce, unescape } from 'lodash'; import { createAlert } from '~/alert'; @@ -27,6 +27,7 @@ export default { }, directives: { SafeHtml, + GlTooltip: GlTooltipDirective, }, mixins: [glFeatureFlagsMixin()], props: { @@ -245,7 +246,7 @@ export default { immediate: true, handler(newVal) { if (!newVal) { - this.showWriteTab(); + this.hidePreview(); } }, }, @@ -277,7 +278,7 @@ export default { } }, methods: { - showPreviewTab() { + showPreview() { if (this.previewMarkdown) return; this.previewMarkdown = true; @@ -297,7 +298,7 @@ export default { this.renderMarkdown(); } }, - showWriteTab() { + hidePreview() { this.markdownPreview = ''; this.previewMarkdown = false; }, @@ -365,9 +366,11 @@ export default { :drawio-enabled="drawioEnabled" data-testid="markdownHeader" :restricted-tool-bar-items="restrictedToolBarItems" - @preview-markdown="showPreviewTab" - @write-markdown="showWriteTab" + :show-content-editor-switcher="showContentEditorSwitcher" + @showPreview="showPreview" + @hidePreview="hidePreview" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" + @enableContentEditor="$emit('enableContentEditor')" /> <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> @@ -384,8 +387,6 @@ export default { :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" :show-comment-tool-bar="showCommentToolBar" - :show-content-editor-switcher="showContentEditorSwitcher" - @enableContentEditor="$emit('enableContentEditor')" /> </div> </div> @@ -393,7 +394,7 @@ export default { <div v-show="previewMarkdown" ref="markdown-preview" - class="js-vue-md-preview md-preview-holder" + class="js-vue-md-preview md-preview-holder gl-px-5" > <suggestions v-if="hasSuggestion" @@ -410,13 +411,13 @@ export default { v-show="previewMarkdown" ref="markdown-preview" v-safe-html:[$options.safeHtmlConfig]="markdownPreview" - class="js-vue-md-preview md md-preview-holder" + class="js-vue-md-preview md md-preview-holder gl-px-5" ></div> </template> <div v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading" v-safe-html:[$options.safeHtmlConfig]="referencedCommands" - class="referenced-commands gl-mx-n5" + class="referenced-commands gl-mx-2 gl-mb-2 gl-px-4 gl-rounded-bottom-left-base gl-rounded-bottom-right-base" data-testid="referenced-commands" ></div> <div v-if="shouldShowReferencedUsers" class="referenced-users"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 53a3913ebda..17d9a2daf0b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,5 +1,5 @@ <script> -import { GlPopover, GlButton, GlTooltipDirective, GlTabs, GlTab } from '@gitlab/ui'; +import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; import $ from 'jquery'; import { keysFor, @@ -19,17 +19,17 @@ import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; import DrawioToolbarButton from './drawio_toolbar_button.vue'; import CommentTemplatesDropdown from './comment_templates_dropdown.vue'; +import EditorModeSwitcher from './editor_mode_switcher.vue'; export default { components: { ToolbarButton, GlPopover, GlButton, - GlTabs, - GlTab, DrawioToolbarButton, CommentTemplatesDropdown, AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'), + EditorModeSwitcher, }, directives: { GlTooltip: GlTooltipDirective, @@ -91,6 +91,11 @@ export default { required: false, default: false, }, + showContentEditorSwitcher: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -121,6 +126,9 @@ export default { const expandText = s__('MarkdownEditor|Click to expand'); return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, + showEditorModeSwitcher() { + return this.showContentEditorSwitcher && !this.previewMarkdown; + }, }, watch: { showSuggestPopover() { @@ -128,14 +136,14 @@ export default { }, }, mounted() { - $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); + $(document).on('markdown-preview:show.vue', this.showMarkdownPreview); + $(document).on('markdown-preview:hide.vue', this.hideMarkdownPreview); this.updateSuggestPopoverVisibility(); }, beforeDestroy() { - $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); + $(document).off('markdown-preview:show.vue', this.showMarkdownPreview); + $(document).off('markdown-preview:hide.vue', this.hideMarkdownPreview); }, methods: { async updateSuggestPopoverVisibility() { @@ -149,19 +157,15 @@ export default { (form.find('.js-vue-markdown-field').length && $(this.$el).closest('form')[0] === form[0]) ); }, - - previewMarkdownTab(event, form) { - if (event.target.blur) event.target.blur(); + showMarkdownPreview(_, form) { if (!this.isValid(form)) return; - this.$emit('preview-markdown'); + this.$emit('showPreview'); }, - - writeMarkdownTab(event, form) { - if (event.target.blur) event.target.blur(); + hideMarkdownPreview(_, form) { if (!this.isValid(form)) return; - this.$emit('write-markdown'); + this.$emit('hidePreview'); }, handleSuggestDismissed() { this.$emit('handleSuggestDismissed'); @@ -204,6 +208,16 @@ export default { }); } }, + handleEditorModeChanged() { + this.$emit('enableContentEditor'); + }, + switchPreview() { + if (this.previewMarkdown) { + this.hideMarkdownPreview(); + } else { + this.showMarkdownPreview(); + } + }, }, shortcuts: { bold: keysFor(BOLD_TEXT), @@ -214,225 +228,240 @@ export default { outdent: keysFor(OUTDENT_LINE), }, i18n: { - writeTabTitle: __('Write'), - previewTabTitle: __('Preview'), + preview: __('Preview'), + hidePreview: __('Continue editing'), }, }; </script> <template> - <div class="md-header"> - <gl-tabs content-class="gl-display-none"> - <gl-tab - title-link-class="gl-py-4 gl-px-3 js-md-write-button" - :title="$options.i18n.writeTabTitle" - :active="!previewMarkdown" - data-testid="write-tab" - @click="writeMarkdownTab($event)" - /> - <gl-tab - v-if="enablePreview" - title-link-class="gl-py-4 gl-px-3 js-md-preview-button" - :title="$options.i18n.previewTabTitle" - :active="previewMarkdown" - data-testid="preview-tab" - @click="previewMarkdownTab($event)" - /> - - <template #tabs-end> - <div - data-testid="md-header-toolbar" - :class="{ 'gl-display-none!': previewMarkdown }" - class="md-header-toolbar gl-ml-auto gl-py-2 gl-justify-content-center" - > - <template v-if="canSuggest"> - <toolbar-button - ref="suggestButton" - :tag="mdSuggestion" - :prepend="true" - :button-title="__('Insert suggestion')" - :cursor-offset="4" - :tag-content="lineContent" - icon="doc-code" - data-qa-selector="suggestion_button" - class="js-suggestion-btn" - @click="handleSuggestDismissed" - /> - <gl-popover - v-if="suggestPopoverVisible" - :target="$refs.suggestButton.$el" - :css-classes="['diff-suggest-popover']" - placement="bottom" - :show="suggestPopoverVisible" - > - <strong>{{ __('New! Suggest changes directly') }}</strong> - <p class="mb-2"> - {{ - __( - 'Suggest code changes which can be immediately applied in one click. Try it out!', - ) - }} - </p> - <gl-button - variant="confirm" - category="primary" - size="small" - data-qa-selector="dismiss_suggestion_popover_button" - @click="handleSuggestDismissed" - > - {{ __('Got it') }} - </gl-button> - </gl-popover> - </template> - <ai-actions-dropdown - v-if="editorAiActions.length" - :actions="editorAiActions" - @input="insertIntoTextarea" - /> - <toolbar-button - tag="**" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.bold" - icon="bold" - /> - <toolbar-button - tag="_" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.italic" - icon="italic" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('strikethrough')" - tag="~~" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.strikethrough" - icon="strikethrough" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('quote')" - :prepend="true" - :tag="tag" - :button-title="__('Insert a quote')" - icon="quote" - @click="handleQuote" - /> - <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> - <toolbar-button - tag="[{text}](url)" - tag-select="url" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.link" - icon="link" - /> + <div class="md-header gl-bg-gray-50 gl-px-2 gl-rounded-base gl-mx-2 gl-mt-2"> + <div + class="gl-display-flex gl-align-items-center gl-flex-wrap" + :class="{ + 'gl-justify-content-end': previewMarkdown, + 'gl-justify-content-space-between': !previewMarkdown, + }" + > + <div + data-testid="md-header-toolbar" + class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap" + :class="{ 'gl-display-none!': previewMarkdown }" + > + <template v-if="canSuggest"> <toolbar-button - v-if="!restrictedToolBarItems.includes('bullet-list')" + ref="suggestButton" + :tag="mdSuggestion" :prepend="true" - tag="- " - :button-title="__('Add a bullet list')" - icon="list-bulleted" + :button-title="__('Insert suggestion')" + :cursor-offset="4" + :tag-content="lineContent" + icon="doc-code" + data-qa-selector="suggestion_button" + class="js-suggestion-btn" + @click="handleSuggestDismissed" /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('numbered-list')" - :prepend="true" - tag="1. " - :button-title="__('Add a numbered list')" - icon="list-numbered" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('task-list')" - :prepend="true" - tag="- [ ] " - :button-title="__('Add a checklist')" - icon="list-task" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('indent')" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.indent" - command="indentLines" - icon="list-indent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('outdent')" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.outdent" - command="outdentLines" - icon="list-outdent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('collapsible-section')" - :tag="mdCollapsibleSection" - :prepend="true" - tag-select="Click to expand" - :button-title="__('Add a collapsible section')" - icon="details-block" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('table')" - :tag="mdTable" - :prepend="true" - :button-title="__('Add a table')" - icon="table" - /> - <gl-button - v-if="!restrictedToolBarItems.includes('attach-file')" - v-gl-tooltip - :title="__('Attach a file or image')" - data-testid="button-attach-file" - category="tertiary" - icon="paperclip" - @click="handleAttachFile" - /> - <drawio-toolbar-button - v-if="drawioEnabled" - :uploads-path="uploadsPath" - :markdown-preview-path="markdownPreviewPath" - /> - <comment-templates-dropdown - v-if="newCommentTemplatePath && glFeatures.savedReplies" - :new-comment-template-path="newCommentTemplatePath" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('full-screen')" - class="js-zen-enter" - :prepend="true" - :button-title="__('Go full screen')" - icon="maximize" - /> - </div> - </template> - </gl-tabs> + <gl-popover + v-if="suggestPopoverVisible" + :target="$refs.suggestButton.$el" + :css-classes="['diff-suggest-popover']" + placement="bottom" + :show="suggestPopoverVisible" + > + <strong>{{ __('New! Suggest changes directly') }}</strong> + <p class="mb-2"> + {{ + __( + 'Suggest code changes which can be immediately applied in one click. Try it out!', + ) + }} + </p> + <gl-button + variant="confirm" + category="primary" + size="small" + data-qa-selector="dismiss_suggestion_popover_button" + @click="handleSuggestDismissed" + > + {{ __('Got it') }} + </gl-button> + </gl-popover> + </template> + <ai-actions-dropdown + v-if="editorAiActions.length" + :actions="editorAiActions" + @input="insertIntoTextarea" + /> + <toolbar-button + tag="**" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.bold" + icon="bold" + /> + <toolbar-button + tag="_" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.italic" + icon="italic" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('strikethrough')" + tag="~~" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.strikethrough" + icon="strikethrough" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('quote')" + :prepend="true" + :tag="tag" + :button-title="__('Insert a quote')" + icon="quote" + @click="handleQuote" + /> + <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> + <toolbar-button + tag="[{text}](url)" + tag-select="url" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.link" + icon="link" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('bullet-list')" + :prepend="true" + tag="- " + :button-title="__('Add a bullet list')" + icon="list-bulleted" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('numbered-list')" + :prepend="true" + tag="1. " + :button-title="__('Add a numbered list')" + icon="list-numbered" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('task-list')" + :prepend="true" + tag="- [ ] " + :button-title="__('Add a checklist')" + icon="list-task" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('indent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.indent" + command="indentLines" + icon="list-indent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('outdent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.outdent" + command="outdentLines" + icon="list-outdent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('collapsible-section')" + :tag="mdCollapsibleSection" + :prepend="true" + tag-select="Click to expand" + :button-title="__('Add a collapsible section')" + icon="details-block" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('table')" + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + /> + <gl-button + v-if="!restrictedToolBarItems.includes('attach-file')" + v-gl-tooltip + :title="__('Attach a file or image')" + class="gl-mr-2" + data-testid="button-attach-file" + category="tertiary" + icon="paperclip" + size="small" + @click="handleAttachFile" + /> + <drawio-toolbar-button + v-if="drawioEnabled" + :uploads-path="uploadsPath" + :markdown-preview-path="markdownPreviewPath" + /> + <comment-templates-dropdown + v-if="newCommentTemplatePath && glFeatures.savedReplies" + :new-comment-template-path="newCommentTemplatePath" + /> + </div> + <div class="switch-preview gl-py-2 gl-display-flex gl-align-items-center gl-ml-auto"> + <editor-mode-switcher + v-if="showEditorModeSwitcher" + size="small" + class="gl-mr-2" + value="markdown" + @input="handleEditorModeChanged" + /> + <gl-button + v-if="enablePreview" + data-testid="preview-toggle" + value="preview" + :label="$options.i18n.previewTabTitle" + class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!" + size="small" + category="tertiary" + @click="switchPreview" + >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button + > + <gl-button + v-if="!restrictedToolBarItems.includes('full-screen')" + v-gl-tooltip + :class="{ 'gl-display-none!': previewMarkdown }" + class="js-zen-enter gl-ml-2" + category="tertiary" + icon="maximize" + size="small" + :title="__('Go full screen')" + :prepend="true" + :button-title="__('Go full screen')" + /> + </div> + </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index e8be242f660..4733afb7504 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; -import EditorModeDropdown from './editor_mode_dropdown.vue'; export default { components: { @@ -9,7 +8,6 @@ export default { GlLoadingIcon, GlSprintf, GlIcon, - EditorModeDropdown, }, props: { markdownDocsPath: { @@ -31,30 +29,21 @@ export default { required: false, default: true, }, - showContentEditorSwitcher: { - type: Boolean, - required: false, - default: false, - }, }, computed: { hasQuickActionsDocsPath() { return this.quickActionsDocsPath !== ''; }, }, - methods: { - handleEditorModeChanged(mode) { - if (mode === 'richText') { - this.$emit('enableContentEditor'); - } - }, - }, }; </script> <template> - <div v-if="showCommentToolBar" class="comment-toolbar clearfix"> - <div class="toolbar-text"> + <div + v-if="showCommentToolBar" + class="comment-toolbar gl-mx-2 gl-mb-2 gl-px-4 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base clearfix" + > + <div class="toolbar-text gl-font-sm"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <gl-sprintf :message=" @@ -62,7 +51,9 @@ export default { " > <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link> + <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ + content + }}</gl-link> </template> </gl-sprintf> </template> @@ -75,18 +66,22 @@ export default { " > <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link> + <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ + content + }}</gl-link> </template> <template #keyboard="{ content }"> <kbd>{{ content }}</kbd> </template> <template #quickActionsDocsLink="{ content }"> - <gl-link :href="quickActionsDocsPath" target="_blank">{{ content }}</gl-link> + <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{ + content + }}</gl-link> </template> </gl-sprintf> </template> </div> - <span v-if="canAttachFile" class="uploading-container gl-line-height-32"> + <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32"> <span class="uploading-progress-container hide"> <gl-icon name="paperclip" /> <span class="attaching-file-message"></span> @@ -111,7 +106,7 @@ export default { <gl-button variant="link" category="primary" - class="retry-uploading-link gl-vertical-align-baseline" + class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!" > {{ content }} </gl-button> @@ -120,7 +115,7 @@ export default { <gl-button variant="link" category="primary" - class="markdown-selector attach-new-file gl-vertical-align-baseline" + class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!" > {{ content }} </gl-button> @@ -130,17 +125,10 @@ export default { <gl-button variant="link" category="primary" - class="button-cancel-uploading-files gl-vertical-align-baseline hide" + class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!" > {{ __('Cancel') }} </gl-button> </span> - <editor-mode-dropdown - v-if="showContentEditorSwitcher" - size="small" - class="gl-float-right gl-line-height-28 gl-display-block" - value="markdown" - @input="handleEditorModeChanged" - /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 5ca21522d33..636c89c99d4 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -92,7 +92,8 @@ export default { :icon="icon" type="button" category="tertiary" - class="js-md" + size="small" + class="js-md gl-mr-3" data-container="body" @click="$emit('click', $event)" /> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index cb682e8b944..32ac5daf5de 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -145,7 +145,7 @@ export default { return this.isNewDiscussion ? 'timeline-entry note-form' : // eslint-disable-next-line @gitlab/require-i18n-strings - 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix'; + 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix gl-bg-white! gl-pt-0!'; }, }, watch: { diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index 44429b439d9..a0cbf4fcd43 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -1,5 +1,6 @@ .ProseMirror { - min-height: 128px; + padding-top: $gl-spacing-scale-4; + min-height: 140px; max-height: 55vh; overflow-y: auto; @@ -116,14 +117,8 @@ } } -.content-editor-dropdown .dropdown-menu { - width: auto !important; - - @include gl-min-w-0; - - button { - @include gl-white-space-nowrap; - } +.content-editor-switcher { + min-height: 32px; } diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index 9ec87b4f304..54a4769f66d 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -946,7 +946,7 @@ table.code { &.popover { width: 250px; min-width: 250px; - z-index: 210; + z-index: 610; } .popover-header { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index e4025eb8b8d..503e22742ba 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -4,7 +4,6 @@ */ .file-holder { border: 1px solid $border-color; - border-top: 0; border-radius: $border-radius-default; &.file-holder-top-border { @@ -21,10 +20,6 @@ border: 0; } - &.file-holder-bottom-radius { - border-radius: 0 0 $border-radius-small $border-radius-small; - } - &.readme-holder { margin: $gl-padding 0; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 48aacc9606e..e57dad9e4cb 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -44,46 +44,6 @@ } } -.md-header { - .nav-links { - a { - width: 100%; - padding-top: 0; - line-height: 19px; - - &.btn.btn-sm { - padding: 2px 5px; - } - - &:focus { - margin-top: -10px; - padding-top: 10px; - } - } - } - - .gl-tabs-nav { - @include media-breakpoint-down(xs) { - .nav-item { - flex: 1; - } - - .gl-tab-nav-item { - padding-top: $gl-padding-4; - padding-bottom: $gl-padding-8; - } - - .md-header-toolbar { - width: 100%; - display: flex; - flex-wrap: wrap; - padding-top: $gl-padding-8; - border-top: 1px solid $border-color; - } - } - } -} - .md-header-tab { @include media-breakpoint-down(xs) { flex: 1; @@ -131,7 +91,7 @@ } .md-preview-holder { - min-height: 172px; + min-height: 176px; padding: 10px 0; overflow-x: auto; } diff --git a/app/assets/stylesheets/framework/source_editor.scss b/app/assets/stylesheets/framework/source_editor.scss index f1ee4c94942..a09ab7ed64c 100644 --- a/app/assets/stylesheets/framework/source_editor.scss +++ b/app/assets/stylesheets/framework/source_editor.scss @@ -37,6 +37,7 @@ .gl-source-editor { @include gl-order-n1; + border-radius: 0 0 $border-radius-default $border-radius-default; } } diff --git a/app/assets/stylesheets/page_bundles/editor.scss b/app/assets/stylesheets/page_bundles/editor.scss index 36da979ba1f..9e9723d2e5a 100644 --- a/app/assets/stylesheets/page_bundles/editor.scss +++ b/app/assets/stylesheets/page_bundles/editor.scss @@ -88,6 +88,10 @@ } } } + + .overflow-guard { + border-radius: 0 0 $border-radius-default $border-radius-default; + } } diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index 00ef659dcf4..d3ebc06a1dd 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -1027,7 +1027,7 @@ $tabs-holder-z-index: 250; } .md-preview-holder { - max-height: 172px; + max-height: 182px; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index adeab227670..d029aa01e37 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -31,7 +31,7 @@ .note-textarea { display: block; - padding: 10px 1px; + padding: 10px 16px; color: $gl-text-color; font-family: $regular-font; border: 0; @@ -48,9 +48,8 @@ .common-note-form { .md-area { - padding: 0 $gl-padding; border: 1px solid $border-color; - border-radius: $border-radius-base; + border-radius: $border-radius-large; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; background-color: $white; @@ -81,6 +80,10 @@ @include gl-focus; } +.md-header { + min-height: 32px; +} + .md-header .nav-links { display: flex; flex-flow: row wrap; @@ -92,6 +95,11 @@ } } + +.md-header .gl-tabs-nav { + border-bottom: 0; +} + .issuable-note-warning { color: $orange-600; background-color: $orange-50; @@ -305,7 +313,6 @@ table { .comment-toolbar { color: $gl-text-color-secondary; - border-top: 1px solid $border-color; } .toolbar-button { @@ -394,6 +401,10 @@ table { float: left; margin-top: 5px; } + + button { + font-size: $gl-font-size-sm !important; + } } .uploading-error-icon, diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index bec6cccb977..91fce6d6820 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -192,7 +192,7 @@ module MarkupHelper def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: 'body' }) - css_classes = %w[gl-button btn btn-default-tertiary btn-icon js-md has-tooltip] << options[:css_class].to_s + css_classes = %w[gl-button btn btn-default-tertiary btn-icon btn-sm js-md has-tooltip] << options[:css_class].to_s content_tag :button, type: 'button', class: css_classes.join(' '), diff --git a/app/models/resource_events/issue_assignment_event.rb b/app/models/resource_events/issue_assignment_event.rb index b24f181bc48..393e2aa8942 100644 --- a/app/models/resource_events/issue_assignment_event.rb +++ b/app/models/resource_events/issue_assignment_event.rb @@ -10,5 +10,9 @@ module ResourceEvents validates :issue, presence: true enum action: { add: 1, remove: 2 } + + def self.issuable_id_column + :issue_id + end end end diff --git a/app/models/resource_events/merge_request_assignment_event.rb b/app/models/resource_events/merge_request_assignment_event.rb index 898594b7008..778b9101858 100644 --- a/app/models/resource_events/merge_request_assignment_event.rb +++ b/app/models/resource_events/merge_request_assignment_event.rb @@ -10,5 +10,9 @@ module ResourceEvents validates :merge_request, presence: true enum action: { add: 1, remove: 2 } + + def self.issuable_id_column + :merge_request_id + end end end diff --git a/app/services/google_cloud/generate_pipeline_service.rb b/app/services/google_cloud/generate_pipeline_service.rb index b6438d6f501..791be69f4d4 100644 --- a/app/services/google_cloud/generate_pipeline_service.rb +++ b/app/services/google_cloud/generate_pipeline_service.rb @@ -61,7 +61,7 @@ module GoogleCloud end def pipeline_content(include_path) - gitlab_ci_yml = Gitlab::Config::Loader::Yaml.new(default_branch_gitlab_ci_yml || '{}').load! + gitlab_ci_yml = ::Gitlab::Ci::Config::Yaml.load!(default_branch_gitlab_ci_yml || '{}') append_remote_include(gitlab_ci_yml, "https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/#{include_path}") end diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 3942c427cc4..f65b2f4915c 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -6,4 +6,4 @@ - page_title runner_name - add_to_breadcrumbs _('Runners'), admin_runners_path -#js-admin-runner-show{ data: {runner_id: @runner.id, runners_path: admin_runners_path} } +#js-admin-runner-show{ data: {runner_id: @runner.id, runners_path: admin_runners_path, empty_state_image: image_path('illustrations/pipelines_empty.svg')} } diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 9962e03995b..19943aa68a3 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -1,4 +1,4 @@ -.form-actions.gl-display-flex +.gl-display-flex.gl-mt-7 - submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { qa_selector: 'commit_button' } } } = render Pajamas::ButtonComponent.new(**submit_button_options) do = _('Commit changes') diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index ff95e9a1088..621cd251bdf 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -2,8 +2,8 @@ - file_name = params[:id].split("/").last ||= "" - is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) -.file-holder-bottom-radius.file-holder.file.gl-mb-3 - .js-file-title.file-title.gl-display-flex.gl-align-items-center.clearfix{ data: { current_action: action } } +.file-holder.file.gl-mb-3 + .js-file-title.file-title.gl-display-flex.gl-align-items-center.gl-rounded-top-base{ data: { current_action: action } } .editor-ref.block-truncated.has-tooltip{ title: ref } = sprite_icon('branch', size: 12) = ref @@ -29,7 +29,10 @@ - unless Feature.enabled?(:source_editor_toolbar, current_user) .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end - if is_markdown - = render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false + .md-header.gl-display-flex.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2 + .gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-justify-content-space-between + .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap{ class: "gl-m-0!" } + = render 'shared/blob/markdown_buttons', supports_file_upload: false %span.soft-wrap-toggle = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do = _("No wrap") diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 59b2536c5d0..fd74ffef425 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -4,5 +4,4 @@ %h1.page-title.gl-font-size-h-display = _("New Snippet") -%hr = render "shared/snippets/form", url: project_snippets_path(@project, @snippet) diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 904854c3fb7..2b55d35cf1f 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,19 +1,18 @@ -.form-group.row.commit_message-group +.form-group.commit_message-group.gl-mt-5 - nonce = SecureRandom.hex - descriptions = local_assigns.slice(:message_with_description, :message_without_description) - = label_tag "commit_message-#{nonce}", class: 'col-form-label col-sm-2' do + = label_tag "commit_message-#{nonce}" do #{ _('Commit message') } - .col-sm-10 - .commit-message-container - .max-width-marker - = text_area_tag 'commit_message', - (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), - class: 'form-control gl-form-input js-commit-message', - placeholder: local_assigns[:placeholder], - data: descriptions, - 'data-qa-selector': 'commit_message_field', - required: true, rows: (local_assigns[:rows] || 3), - id: "commit_message-#{nonce}" + .commit-message-container + .max-width-marker + = text_area_tag 'commit_message', + (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), + class: 'form-control gl-form-input js-commit-message', + placeholder: local_assigns[:placeholder], + data: descriptions, + 'data-qa-selector': 'commit_message_field', + required: true, rows: (local_assigns[:rows] || 3), + id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint = _('Try to keep the first line under 52 characters and the others under 72.') diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml index 2fff70cdc74..dd3a31f5a59 100644 --- a/app/views/shared/_md_preview.html.haml +++ b/app/views/shared/_md_preview.html.haml @@ -8,21 +8,18 @@ = _('Only project members can comment.') .md-area.position-relative - .md-header - = gl_tabs_nav({ class: 'clearfix nav-links'}) do - %li.md-header-tab.active - %button.js-md-write-button{ class: 'gl-py-3!' } - = _("Write") - %li.md-header-tab - %button.js-md-preview-button{ class: 'gl-py-3!' } - = _("Preview") - - %li.md-header-toolbar.active.gl-py-2 - = render 'shared/blob/markdown_buttons', show_fullscreen_button: true + .md-header.gl-bg-gray-50.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2 + .gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-justify-content-space-between + .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap + = render 'shared/blob/markdown_buttons' + .switch-preview.gl-py-2.gl-display-flex.gl-align-items-center.gl-ml-auto + = render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, button_options: { class: 'js-md-preview-button', value: 'preview' }) do + = _('Preview') + = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, size: :small, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter gl-ml-2', data: { container: 'body' } }) .md-write-holder = yield - .md.md-preview-holder.js-md-preview.hide{ data: { url: url } } + .md.md-preview-holder.gl-px-5.js-md-preview.hide{ data: { url: url } } .referenced-commands.hide - if referenced_users diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 14ea96f9669..bdc7156242d 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -8,13 +8,12 @@ = hidden_field_tag 'branch_name', ref, class: 'js-branch-name' - else - if can?(current_user, :push_code, @project) - .form-group.row.branch - = label_tag 'branch_name', _('Target Branch'), class: 'col-form-label col-sm-2' - .col-sm-10 - = text_field_tag 'branch_name', branch_name, required: true, class: "form-control gl-form-input js-branch-name ref-name" + .form-group.branch + = label_tag 'branch_name', _('Target Branch') + = text_field_tag 'branch_name', branch_name, required: true, class: "form-control gl-form-input js-branch-name ref-name" - .js-create-merge-request-container - = render 'shared/new_merge_request_checkbox' + .js-create-merge-request-container + = render 'shared/new_merge_request_checkbox' - elsif project.can_current_user_push_to_branch?(branch_name) = hidden_field_tag 'branch_name', branch_name - else diff --git a/app/views/shared/_zen.html.haml b/app/views/shared/_zen.html.haml index 5a4efe7fe7f..05bee9e4d42 100644 --- a/app/views/shared/_zen.html.haml +++ b/app/views/shared/_zen.html.haml @@ -4,6 +4,7 @@ - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - qa_selector = local_assigns.fetch(:qa_selector, '') - autofocus = local_assigns.fetch(:autofocus, false) + .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml index db53d78dadb..a3d3c1c8231 100644 --- a/app/views/shared/blob/_markdown_buttons.html.haml +++ b/app/views/shared/blob/_markdown_buttons.html.haml @@ -1,42 +1,44 @@ - modifier_key = client_js_flags[:isMac] ? '⌘' : s_('KeyboardKey|Ctrl+') - supports_file_upload = local_assigns.fetch(:supports_file_upload, true) -.md-header-toolbar.active - = markdown_toolbar_button({ icon: "bold", - data: { "md-tag" => "**", "md-shortcuts": '["mod+b"]' }, - title: sprintf(s_("MarkdownEditor|Add bold text (%{modifier_key}B)") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "bold", + css_class: 'gl-mr-3', + data: { "md-tag" => "**", "md-shortcuts": '["mod+b"]' }, + title: sprintf(s_("MarkdownEditor|Add bold text (%{modifier_key}B)") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "italic", - data: { "md-tag" => "_", "md-shortcuts": '["mod+i"]' }, - title: sprintf(s_("MarkdownEditor|Add italic text (%{modifier_key}I)") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "italic", + css_class: 'gl-mr-3', + data: { "md-tag" => "_", "md-shortcuts": '["mod+i"]' }, + title: sprintf(s_("MarkdownEditor|Add italic text (%{modifier_key}I)") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "strikethrough", - data: { "md-tag" => "~~", "md-shortcuts": '["mod+shift+x"]' }, - title: sprintf(s_("MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "strikethrough", + css_class: 'gl-mr-3', + data: { "md-tag" => "~~", "md-shortcuts": '["mod+shift+x"]' }, + title: sprintf(s_("MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") }) - = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") }) += markdown_toolbar_button({ icon: "quote", css_class: 'gl-mr-3', data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") }) += markdown_toolbar_button({ icon: "code", css_class: 'gl-mr-3', data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") }) - = markdown_toolbar_button({ icon: "link", - data: { "md-tag" => "[{text}](url)", "md-select" => "url", "md-shortcuts": '["mod+k"]' }, - title: sprintf(s_("MarkdownEditor|Add a link (%{modifier_key}K)") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "link", + css_class: 'gl-mr-3', + data: { "md-tag" => "[{text}](url)", "md-select" => "url", "md-shortcuts": '["mod+k"]' }, + title: sprintf(s_("MarkdownEditor|Add a link (%{modifier_key}K)") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") }) - = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") }) - = markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") }) - = markdown_toolbar_button({ icon: "list-indent", - data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' }, - css_class: 'gl-display-none', - title: sprintf(s_("MarkdownEditor|Indent line (%{modifier_key}])") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "list-outdent", - data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' }, - css_class: 'gl-display-none', - title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) }) - = markdown_toolbar_button({ icon: "details-block", - data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" }, - title: _("Add a collapsible section") }) - = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") }) - - if supports_file_upload - = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button', data: { testid: 'button-attach-file', container: 'body' } }) - - if show_fullscreen_button - = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter', data: { container: 'body' } }) += markdown_toolbar_button({ icon: "list-bulleted", css_class: 'gl-mr-3', data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") }) += markdown_toolbar_button({ icon: "list-numbered", css_class: 'gl-mr-3', data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") }) += markdown_toolbar_button({ icon: "list-task", css_class: 'gl-mr-3', data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") }) += markdown_toolbar_button({ icon: "list-indent", + css_class: 'gl-display-none gl-mr-3', + data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' }, + title: sprintf(s_("MarkdownEditor|Indent line (%{modifier_key}])") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "list-outdent", + css_class: 'gl-display-none gl-mr-3', + data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' }, + title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) }) += markdown_toolbar_button({ icon: "details-block", + css_class: 'gl-mr-3', + data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" }, + title: _("Add a collapsible section") }) += markdown_toolbar_button({ icon: "table", css_class: 'gl-mr-3', data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") }) +- if supports_file_upload + = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, size: :small, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button gl-mr-3', data: { testid: 'button-attach-file', container: 'body' } }) diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index fb000b9aab1..d7d6e477ab1 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,7 +1,7 @@ - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - supports_file_upload = local_assigns.fetch(:supports_file_upload, true) -.comment-toolbar.clearfix - .toolbar-text +.comment-toolbar.gl-mx-2.gl-mb-2.gl-px-4.gl-bg-gray-10.gl-rounded-bottom-left-base.gl-rounded-bottom-right-base.clearfix + .toolbar-text.gl-font-sm - markdownLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') } - quickActionsLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/quick_actions') } - if supports_quick_actions @@ -9,7 +9,7 @@ - else = html_escape(s_('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe } - if supports_file_upload - %span.uploading-container.gl-line-height-32 + %span.uploading-container.gl-line-height-32.gl-font-sm %span.uploading-progress-container.hide = sprite_icon('paperclip', css_class: 'gl-icon gl-vertical-align-text-bottom') %span.attaching-file-message diff --git a/config/metrics/counts_all/20210216175520_ci_runners.yml b/config/metrics/counts_all/20210216175520_ci_runners.yml index 68d8a3ee190..f701a312446 100644 --- a/config/metrics/counts_all/20210216175520_ci_runners.yml +++ b/config/metrics/counts_all/20210216175520_ci_runners.yml @@ -18,3 +18,4 @@ tier: - ultimate performance_indicator_type: [] milestone: "<13.9" +instrumentation_class: CountCiRunnersMetric diff --git a/config/metrics/counts_all/20210502045402_ci_runners_instance_type_active.yml b/config/metrics/counts_all/20210502045402_ci_runners_instance_type_active.yml index fac597f7a9f..ef969f31496 100644 --- a/config/metrics/counts_all/20210502045402_ci_runners_instance_type_active.yml +++ b/config/metrics/counts_all/20210502045402_ci_runners_instance_type_active.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersInstanceTypeActiveMetric distribution: - ce - ee diff --git a/config/metrics/counts_all/20210502050341_ci_runners_group_type_active.yml b/config/metrics/counts_all/20210502050341_ci_runners_group_type_active.yml index ddbeeaf9ce1..bc5f69f80b7 100644 --- a/config/metrics/counts_all/20210502050341_ci_runners_group_type_active.yml +++ b/config/metrics/counts_all/20210502050341_ci_runners_group_type_active.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersGroupTypeActiveMetric distribution: - ce - ee diff --git a/config/metrics/counts_all/20210502050834_ci_runners_project_type_active.yml b/config/metrics/counts_all/20210502050834_ci_runners_project_type_active.yml index fe3a070d689..054e3f9e524 100644 --- a/config/metrics/counts_all/20210502050834_ci_runners_project_type_active.yml +++ b/config/metrics/counts_all/20210502050834_ci_runners_project_type_active.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersProjectTypeActiveMetric distribution: - ce - ee diff --git a/config/metrics/counts_all/20210502051651_ci_runners_instance_type_active_online.yml b/config/metrics/counts_all/20210502051651_ci_runners_instance_type_active_online.yml index 82cf9083fe2..653a4a3917a 100644 --- a/config/metrics/counts_all/20210502051651_ci_runners_instance_type_active_online.yml +++ b/config/metrics/counts_all/20210502051651_ci_runners_instance_type_active_online.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersInstanceTypeActiveOnlineMetric distribution: - ce - ee diff --git a/config/metrics/counts_all/20210502051922_ci_runners_group_type_active_online.yml b/config/metrics/counts_all/20210502051922_ci_runners_group_type_active_online.yml index 2f954dab596..53c2c2a8650 100644 --- a/config/metrics/counts_all/20210502051922_ci_runners_group_type_active_online.yml +++ b/config/metrics/counts_all/20210502051922_ci_runners_group_type_active_online.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersGroupTypeActiveOnlineMetric distribution: - ce - ee diff --git a/config/metrics/counts_all/20210502052036_ci_runners_project_type_active_online.yml b/config/metrics/counts_all/20210502052036_ci_runners_project_type_active_online.yml index de771a4a264..556492214f1 100644 --- a/config/metrics/counts_all/20210502052036_ci_runners_project_type_active_online.yml +++ b/config/metrics/counts_all/20210502052036_ci_runners_project_type_active_online.yml @@ -12,6 +12,7 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58197 time_frame: all data_source: database +instrumentation_class: CountCiRunnersProjectTypeActiveOnlineMetric distribution: - ce - ee diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 6edfd5add48..e257bddb900 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -1340,6 +1340,12 @@ Example: expect(metrics.merged_at).to be_like_time(time) ``` +Example for `be_within`: + +```ruby +expect(violation.reload.merged_at).to be_within(0.00001.seconds).of(merge_request.merged_at) +``` + #### `have_gitlab_http_status` Prefer `have_gitlab_http_status` over `have_http_status` and diff --git a/doc/development/value_stream_analytics.md b/doc/development/value_stream_analytics.md index 43db2187cdd..afbd1a76a1b 100644 --- a/doc/development/value_stream_analytics.md +++ b/doc/development/value_stream_analytics.md @@ -160,6 +160,7 @@ graph LR; IssueCreated --> IssueLastEdited; IssueCreated --> IssueLabelAdded; IssueCreated --> IssueLabelRemoved; + IssueCreated --> IssueFirstAssignedAt; MergeRequestCreated --> MergeRequestMerged; MergeRequestCreated --> MergeRequestClosed; MergeRequestCreated --> MergeRequestFirstDeployedToProduction; @@ -168,6 +169,13 @@ graph LR; MergeRequestCreated --> MergeRequestLastEdited; MergeRequestCreated --> MergeRequestLabelAdded; MergeRequestCreated --> MergeRequestLabelRemoved; + MergeRequestCreated --> MergeRequestFirstAssignedAt; + MergeRequestFirstAssignedAt --> MergeRequestClosed; + MergeRequestFirstAssignedAt --> MergeRequestLastBuildStarted; + MergeRequestFirstAssignedAt --> MergeRequestLastEdited; + MergeRequestFirstAssignedAt --> MergeRequestMerged; + MergeRequestFirstAssignedAt --> MergeRequestLabelAdded; + MergeRequestFirstAssignedAt --> MergeRequestLabelRemoved; MergeRequestLastBuildStarted --> MergeRequestLastBuildFinished; MergeRequestLastBuildStarted --> MergeRequestClosed; MergeRequestLastBuildStarted --> MergeRequestFirstDeployedToProduction; @@ -184,19 +192,30 @@ graph LR; IssueLabelAdded --> IssueLabelAdded; IssueLabelAdded --> IssueLabelRemoved; IssueLabelAdded --> IssueClosed; + IssueLabelAdded --> IssueFirstAssignedAt; IssueLabelRemoved --> IssueClosed; + IssueLabelRemoved --> IssueFirstAssignedAt; IssueFirstAddedToBoard --> IssueClosed; IssueFirstAddedToBoard --> IssueFirstAssociatedWithMilestone; IssueFirstAddedToBoard --> IssueFirstMentionedInCommit; IssueFirstAddedToBoard --> IssueLastEdited; IssueFirstAddedToBoard --> IssueLabelAdded; IssueFirstAddedToBoard --> IssueLabelRemoved; + IssueFirstAddedToBoard --> IssueFirstAssignedAt; + IssueFirstAssignedAt --> IssueClosed; + IssueFirstAssignedAt --> IssueFirstAddedToBoard; + IssueFirstAssignedAt --> IssueFirstAssociatedWithMilestone; + IssueFirstAssignedAt --> IssueFirstMentionedInCommit; + IssueFirstAssignedAt --> IssueLastEdited; + IssueFirstAssignedAt --> IssueLabelAdded; + IssueFirstAssignedAt --> IssueLabelRemoved; IssueFirstAssociatedWithMilestone --> IssueClosed; IssueFirstAssociatedWithMilestone --> IssueFirstAddedToBoard; IssueFirstAssociatedWithMilestone --> IssueFirstMentionedInCommit; IssueFirstAssociatedWithMilestone --> IssueLastEdited; IssueFirstAssociatedWithMilestone --> IssueLabelAdded; IssueFirstAssociatedWithMilestone --> IssueLabelRemoved; + IssueFirstAssociatedWithMilestone --> IssueFirstAssignedAt; IssueFirstMentionedInCommit --> IssueClosed; IssueFirstMentionedInCommit --> IssueFirstAssociatedWithMilestone; IssueFirstMentionedInCommit --> IssueFirstAddedToBoard; @@ -222,8 +241,10 @@ graph LR; MergeRequestLabelAdded --> MergeRequestLabelAdded; MergeRequestLabelAdded --> MergeRequestLabelRemoved; MergeRequestLabelAdded --> MergeRequestMerged; + MergeRequestLabelAdded --> MergeRequestFirstAssignedAt; MergeRequestLabelRemoved --> MergeRequestLabelAdded; MergeRequestLabelRemoved --> MergeRequestLabelRemoved; + MergeRequestLabelRemoved --> MergeRequestFirstAssignedAt; ``` ## Default stages diff --git a/doc/user/permissions.md b/doc/user/permissions.md index e4a3f60834f..418c01cd851 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -103,6 +103,8 @@ The following table lists project permissions available for each role: | [Issues](project/issues/index.md):<br>View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>View [related issues](project/issues/related_issues.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Set [weight](project/issues/issue_weight.md) | ✓ (15) | ✓ | ✓ | ✓ | ✓ | +| [Issues](project/issues/index.md):<br>Set metadata such as labels, milestones, or assignees when creating an issue | ✓ (15) | ✓ | ✓ | ✓ | ✓ | +| [Issues](project/issues/index.md):<br>Edit metadata such labels, milestones, or assignees for an existing issue | (15) | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Set [parent epic](group/epics/manage_epics.md#add-an-existing-issue-to-an-epic) | | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>View [confidential issues](project/issues/confidential_issues.md) | (2) | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Close / reopen (18) | | ✓ | ✓ | ✓ | ✓ | diff --git a/lib/gitlab/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb index 38f8eca3c3c..7138663811e 100644 --- a/lib/gitlab/config/loader/yaml.rb +++ b/lib/gitlab/config/loader/yaml.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# NOTE: DO NOT use this class for loading GitLab CI configuration files. +# Instead, use `Gitlab::Ci::Config::Yaml.load!`, which will properly handle +# CI configuration headers. + module Gitlab module Config module Loader diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric.rb new file mode 100644 index 00000000000..fbf4e0f904b --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersGroupTypeActiveMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner.group_type.active } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric.rb new file mode 100644 index 00000000000..acb6de53d14 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersGroupTypeActiveOnlineMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner.group_type.active.online } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric.rb new file mode 100644 index 00000000000..d9a785679d7 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersInstanceTypeActiveMetric < DatabaseMetric + operation :count + + relation do + ::Ci::Runner.instance_type.active + end + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric.rb new file mode 100644 index 00000000000..05a9c47c016 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersInstanceTypeActiveOnlineMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner.instance_type.active.online } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric.rb new file mode 100644 index 00000000000..8be4955e28d --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric.rb new file mode 100644 index 00000000000..e713e85b270 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersProjectTypeActiveMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner.project_type.active } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric.rb new file mode 100644 index 00000000000..91e7c6063b8 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountCiRunnersProjectTypeActiveOnlineMetric < DatabaseMetric + operation :count + + relation { ::Ci::Runner.project_type.active.online } + end + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index ad2a8130cca..8403c933076 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -145,7 +145,6 @@ module Gitlab merge_requests: count(MergeRequest), notes: count(Note) }.merge( - runners_usage, integrations_usage, user_preferences_usage, service_desk_counts @@ -156,18 +155,6 @@ module Gitlab end # rubocop: enable Metrics/AbcSize - def runners_usage - { - ci_runners: count(::Ci::Runner), - ci_runners_instance_type_active: count(::Ci::Runner.instance_type.active), - ci_runners_group_type_active: count(::Ci::Runner.group_type.active), - ci_runners_project_type_active: count(::Ci::Runner.project_type.active), - ci_runners_instance_type_active_online: count(::Ci::Runner.instance_type.active.online), - ci_runners_group_type_active_online: count(::Ci::Runner.group_type.active.online), - ci_runners_project_type_active_online: count(::Ci::Runner.project_type.active.online) - } - end - def system_usage_data_monthly { counts_monthly: { diff --git a/lib/gitlab/usage_data_metrics.rb b/lib/gitlab/usage_data_metrics.rb index 48f695d5db1..8948d621d3c 100644 --- a/lib/gitlab/usage_data_metrics.rb +++ b/lib/gitlab/usage_data_metrics.rb @@ -8,10 +8,6 @@ module Gitlab build_payload(:with_value) end - def suggested_names - build_payload(:with_suggested_name) - end - private def build_payload(method_symbol) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 04594cc02c3..4dd83d4e416 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13216,6 +13216,9 @@ msgstr "" msgid "CycleAnalyticsEvent|Issue first added to a board" msgstr "" +msgid "CycleAnalyticsEvent|Issue first assigned" +msgstr "" + msgid "CycleAnalyticsEvent|Issue first associated with a milestone" msgstr "" @@ -13240,6 +13243,9 @@ msgstr "" msgid "CycleAnalyticsEvent|Merge request created" msgstr "" +msgid "CycleAnalyticsEvent|Merge request first assigned" +msgstr "" + msgid "CycleAnalyticsEvent|Merge request first commit time" msgstr "" @@ -16245,12 +16251,6 @@ msgstr "" msgid "Editing" msgstr "" -msgid "Editing markdown" -msgstr "" - -msgid "Editing rich text" -msgstr "" - msgid "Edits" msgstr "" @@ -27038,9 +27038,6 @@ msgstr "" msgid "Mark to do as done" msgstr "" -msgid "Markdown" -msgstr "" - msgid "Markdown Help" msgstr "" @@ -38327,9 +38324,6 @@ msgstr "" msgid "Revoked personal access token %{personal_access_token_name}!" msgstr "" -msgid "Rich text" -msgstr "" - msgid "RightSidebar|Copy email address" msgstr "" @@ -38730,6 +38724,9 @@ msgstr "" msgid "Runners|Maintenance note" msgstr "" +msgid "Runners|Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up." +msgstr "" + msgid "Runners|Manually verify that the runner is available to pick up jobs." msgstr "" @@ -39106,6 +39103,9 @@ msgstr "" msgid "Runners|This registration process is only supported in GitLab Runner 15.10 or later" msgstr "" +msgid "Runners|This runner has not run any jobs" +msgstr "" + msgid "Runners|This runner has not run any jobs." msgstr "" @@ -42243,19 +42243,19 @@ msgstr "" msgid "Snippets|Delete file" msgstr "" -msgid "Snippets|Description (optional)" +msgid "Snippets|Describe what your snippet does or how to use it…" msgstr "" -msgid "Snippets|Error with Akismet. Please check the logs for more info." +msgid "Snippets|Description (optional)" msgstr "" -msgid "Snippets|Files" +msgid "Snippets|Error with Akismet. Please check the logs for more info." msgstr "" -msgid "Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby" +msgid "Snippets|File name (e.g. test.rb)" msgstr "" -msgid "Snippets|Optionally add a description about what your snippet does or how to use it…" +msgid "Snippets|Files" msgstr "" msgid "Snippets|Snippets can't contain empty files. Ensure all files have content, or delete them." @@ -43865,6 +43865,12 @@ msgstr "" msgid "Switch to GitLab Next" msgstr "" +msgid "Switch to Markdown" +msgstr "" + +msgid "Switch to rich text" +msgstr "" + msgid "Switch to the source to copy the file contents" msgstr "" @@ -49084,9 +49090,6 @@ msgstr "" msgid "View all projects" msgstr "" -msgid "View and edit markdown, with the option to preview the formatted output." -msgstr "" - msgid "View blame" msgstr "" @@ -49216,9 +49219,6 @@ msgstr "" msgid "View the documentation" msgstr "" -msgid "View the formatted output in real-time as you edit." -msgstr "" - msgid "View the latest successful deployment to this environment" msgstr "" diff --git a/package.json b/package.json index ec986934347..c5bc3767779 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "clipboard": "^2.0.8", "compression-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^6.4.1", - "core-js": "^3.29.1", + "core-js": "^3.30.1", "cron-validator": "^1.1.1", "cronstrue": "^1.122.0", "cropper": "^2.3.0", @@ -207,7 +207,7 @@ "vue-loader": "15.10.1", "vue-observe-visibility": "^1.0.0", "vue-resize": "^1.0.1", - "vue-router": "3.4.9", + "vue-router": "3.6.5", "vue-router-vue3": "npm:vue-router@4.1.6", "vue-template-compiler": "2.7.14", "vue-virtual-scroll-list": "^1.4.7", diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb index b1d83a6e2d0..05d59acd8e8 100644 --- a/qa/qa/page/component/snippet.rb +++ b/qa/qa/page/component/snippet.rb @@ -156,8 +156,14 @@ module QA end end - def has_embed_dropdown? - has_element?(:snippet_embed_dropdown) + RSpec::Matchers.define :have_embed_dropdown do + match do |page| + page.has_element?(:snippet_embed_dropdown) + end + + match_when_negated do |page| + page.has_no_element?(:snippet_embed_dropdown) + end end def click_edit_button diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index 9143a25d9ab..335790c5b27 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -19,7 +19,7 @@ module QA element :markdown_editor_form_field end - base.view 'app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue' do + base.view 'app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue' do element :editing_mode_switcher end @@ -59,9 +59,6 @@ module QA def use_new_editor click_element(:editing_mode_switcher) - within_element(:editing_mode_switcher) do - find('button', text: 'Rich text').click - end wait_until(reload: false) do has_element?(:content_editor_container) diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb index 98c1f9baf12..82b7379b67c 100644 --- a/spec/features/abuse_report_spec.rb +++ b/spec/features/abuse_report_spec.rb @@ -115,7 +115,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do before do visit project_issue_path(project, issue) - click_button 'More actions' + find('.more-actions-toggle button').click end it_behaves_like 'reports the user with an abuse category' diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index a70a1e2e70b..376e1e6063f 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -25,17 +25,17 @@ RSpec.describe 'Group milestones', feature_category: :subgroups do description.native.send_keys('') - click_button('Preview') + click_button("Preview") preview = find('.js-md-preview') expect(preview).to have_content('Nothing to preview.') - click_button('Write') + click_button("Continue editing") description.native.send_keys(':+1: Nice') - click_button('Preview') + click_button("Preview") expect(preview).to have_css('gl-emoji') expect(find('#milestone_description', visible: false)).not_to be_visible diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb index 52464c6be8b..887bc7d0c87 100644 --- a/spec/features/issuables/markdown_references/jira_spec.rb +++ b/spec/features/issuables/markdown_references/jira_spec.rb @@ -29,7 +29,7 @@ RSpec.describe "Jira", :js, feature_category: :team_planning do end it "creates a link to the referenced issue on the preview" do - find(".js-md-preview-button").click + click_button("Preview") wait_for_requests diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb index 3b1716230cd..56c395091d9 100644 --- a/spec/features/issues/user_creates_issue_spec.rb +++ b/spec/features/issues/user_creates_issue_spec.rb @@ -61,22 +61,22 @@ RSpec.describe "User creates issue", feature_category: :team_planning do textarea = first(".gfm-form textarea") page.within(form) do - click_link("Preview") + click_button("Preview") preview = find(".js-vue-md-preview") # this element is findable only when the "Preview" link is clicked. expect(preview).to have_content("Nothing to preview.") - click_link("Write") + click_button("Continue editing") fill_in("Description", with: "Bug fixed :smile:") - click_link("Preview") + click_button("Preview") expect(preview).to have_css("gl-emoji") expect(textarea).not_to be_visible - click_link("Write") + click_button("Continue editing") fill_in("Description", with: "/confidential") - click_link("Preview") + click_button("Preview") expect(form).to have_content('Makes this issue confidential.') end diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index 4ef58918a2b..c1cf8fada26 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -39,9 +39,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin click_button("Preview") end - expect(form).to have_button("Write") - - click_button("Write") + click_button("Continue editing") fill_in("Description", with: "/confidential") click_button("Preview") @@ -121,8 +119,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin expect(issuable_form).to have_selector(markdown_field_focused_selector) page.within issuable_form do - click_on _('Editing markdown') - click_on _('Rich text') + click_button("Switch to rich text") end expect(issuable_form).not_to have_selector(content_editor_focused_selector) @@ -134,8 +131,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin expect(issuable_form).to have_selector(content_editor_focused_selector) page.within issuable_form do - click_on _('Editing rich text') - click_on _('Markdown') + click_button("Switch to Markdown") end expect(issuable_form).not_to have_selector(markdown_field_focused_selector) diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index ddbcb04fa80..6e98812753a 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -52,6 +52,8 @@ RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_re find('.js-note-delete').click + wait_for_requests + page.within('.modal') do click_button('Delete comment', match: :first) end @@ -66,6 +68,8 @@ RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_re find('.js-note-edit').click + wait_for_requests + # make sure comment form is in view execute_script("window.scrollBy(0, 200)") diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 9ab53a00903..35e2fa2f89c 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -248,7 +248,7 @@ RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_wo page.within('.diff-file:nth-of-type(1) .discussion .note') do find('.more-actions').click - find('.more-actions .dropdown-menu li', match: :first) + find('.more-actions li', match: :first) find('.js-note-delete').click end diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index a74a8b1cd5a..f13c68a60ee 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -103,7 +103,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js, feature_category: : should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) accept_gl_confirm(button_text: 'Delete comment') do - first('button.more-actions-toggle').click + first('.more-actions-toggle button').click first('.js-note-delete').click end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index f167ab8fe8a..03b01ef4b7a 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -62,7 +62,7 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_ before do page.within('.js-main-target-form') do fill_in 'note[note]', with: 'This is awesome!' - find('.js-md-preview-button').click + click_button("Preview") click_button 'Comment' end end @@ -138,7 +138,7 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_ it 'hides the toolbar buttons when previewing a note' do wait_for_requests - find('.js-md-preview-button').click + click_button("Preview") page.within('.js-main-target-form') do expect(page).not_to have_css('.md-header-toolbar') end diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb index 6118f59df3c..1a9d40ae926 100644 --- a/spec/features/merge_request/user_views_open_merge_request_spec.rb +++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb @@ -59,7 +59,7 @@ RSpec.describe 'User views an open merge request', feature_category: :code_revie fill_in(:merge_request_description, with: '') page.within('.js-vue-markdown-field') do - click_link('Preview') + click_button("Preview") expect(find('.js-vue-md-preview')).to have_content('Nothing to preview.') end @@ -69,12 +69,12 @@ RSpec.describe 'User views an open merge request', feature_category: :code_revie fill_in(:merge_request_description, with: ':+1: Nice') page.within('.js-vue-markdown-field') do - click_link('Preview') + click_button("Preview") expect(find('.js-vue-md-preview')).to have_css('gl-emoji') end - expect(find('.js-vue-markdown-field')).to have_css('.js-vue-md-preview').and have_link('Write') + expect(find('.js-vue-markdown-field')).to have_css('.js-md-preview-button') expect(find('#merge_request_description', visible: false)).not_to be_visible end end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 2b6b09ccc10..6e335871ed1 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -142,7 +142,7 @@ RSpec.describe 'Editing file blob', :js, feature_category: :projects do it 'renders content with CommonMark' do visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) fill_editor(content: '1. one\\n - sublist\\n') - click_link 'Preview' + click_on "Preview" wait_for_requests # the above generates two separate lists (not embedded) in CommonMark diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb index 91b838116e9..b0cb57f158d 100644 --- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb +++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb @@ -36,7 +36,7 @@ RSpec.describe "User adds a comment on a commit", :js, feature_category: :source expect(page).not_to have_css(".js-note-text") # Check on the `Write` tab - click_button("Write") + click_button("Continue editing") expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji}") @@ -107,7 +107,7 @@ RSpec.describe "User adds a comment on a commit", :js, feature_category: :source # Test UI elements, then submit. page.within("form[data-line-code='#{sample_commit.line_code}']") do expect(find(".js-note-text", visible: false).text).to eq("") - expect(page).to have_css('.js-md-write-button') + expect(page).to have_css('.js-md-preview') click_button("Comment") end diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb index e23eb1cada8..e265756f930 100644 --- a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb +++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb @@ -30,7 +30,7 @@ RSpec.describe "User deletes comments on a commit", :js, feature_category: :sour note.hover find(".more-actions").click - find(".more-actions .dropdown-menu li", match: :first) + find(".more-actions li", match: :first) find(".js-note-delete").click end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb index c4019b4d123..709914434e7 100644 --- a/spec/features/projects/commit/user_comments_on_commit_spec.rb +++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb @@ -38,7 +38,7 @@ RSpec.describe "User comments on commit", :js, feature_category: :source_code_ma expect(page).not_to have_css(".js-note-text") # Check on `Write` tab - click_button("Write") + click_button("Continue editing") expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji_code}") @@ -109,7 +109,7 @@ RSpec.describe "User comments on commit", :js, feature_category: :source_code_ma note.hover find(".more-actions").click - find(".more-actions .dropdown-menu li", match: :first) + find(".more-actions li", match: :first) find(".js-note-delete").click end diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb index c282067f3ad..ffc319c8453 100644 --- a/spec/features/projects/releases/user_creates_release_spec.rb +++ b/spec/features/projects/releases/user_creates_release_spec.rb @@ -108,7 +108,7 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive fill_release_notes('**some** _markdown_ [content](https://example.com)') - click_on 'Preview' + click_button("Preview") wait_for_all_requests end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index 5aac27a71e4..5c1ee729346 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -81,6 +81,7 @@ RSpec.describe 'Comments on personal snippets', :js, feature_category: :source_c it 'previews a note' do fill_in 'note[note]', with: 'This is **awesome**!' + find('.js-md-preview-button').click page.within('.new-note .md-preview-holder') do diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js index 4ceed885e6e..7306e6eef17 100644 --- a/spec/frontend/api/projects_api_spec.js +++ b/spec/frontend/api/projects_api_spec.js @@ -146,4 +146,29 @@ describe('~/api/projects_api.js', () => { }); }); }); + + describe('getProjectShareLocations', () => { + it('requests share locations for a project', async () => { + const expectedUrl = `/api/v7/projects/1/share_locations`; + const params = { search: 'foo' }; + + const response = [ + { + id: 27, + web_url: 'http://127.0.0.1:3000/groups/Commit451', + name: 'Commit451', + avatar_url: null, + full_name: 'Commit451', + full_path: 'Commit451', + }, + ]; + + mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response); + + await expect(projectsApi.getProjectShareLocations(projectId, params)).resolves.toMatchObject({ + data: response, + }); + expect(mock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE }); + }); + }); }); diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap index a5690844053..1733c4d4bb4 100644 --- a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap +++ b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap @@ -10,7 +10,7 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = ` <gl-form-input-stub class="form-control js-snippet-file-name" name="snippet_file_name" - placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby" + placeholder="File name (e.g. test.rb)" type="text" value="foo.md" /> diff --git a/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js new file mode 100644 index 00000000000..e64308f49d1 --- /dev/null +++ b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js @@ -0,0 +1,37 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState } from '@gitlab/ui'; +import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue'; + +const DEFAULT_PROPS = { + emptyTitle: 'This runner has not run any jobs', + emptyDescription: + 'Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up.', +}; + +describe('RunnerJobsEmptyStateComponent', () => { + let wrapper; + + const mountComponent = () => { + wrapper = shallowMount(RunnerJobsEmptyState, { + provide: { + emptyStateImage: 'emptyStateImage', + }, + }); + }; + + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + beforeEach(() => { + mountComponent(); + }); + + describe('empty', () => { + it('should show an empty state if it is empty', () => { + const emptyState = findEmptyState(); + + expect(emptyState.props('svgPath')).toBe('emptyStateImage'); + expect(emptyState.props('title')).toBe(DEFAULT_PROPS.emptyTitle); + expect(emptyState.text()).toContain(DEFAULT_PROPS.emptyDescription); + }); + }); +}); diff --git a/spec/frontend/ci/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js index 367b9ce395d..179b37cfa21 100644 --- a/spec/frontend/ci/runner/components/runner_jobs_spec.js +++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js @@ -8,8 +8,9 @@ import { createAlert } from '~/alert'; import RunnerJobs from '~/ci/runner/components/runner_jobs.vue'; import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue'; import RunnerPagination from '~/ci/runner/components/runner_pagination.vue'; +import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue'; import { captureException } from '~/ci/runner/sentry_utils'; -import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants'; +import { RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants'; import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql'; @@ -31,7 +32,7 @@ describe('RunnerJobs', () => { const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader); const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable); const findRunnerPagination = () => wrapper.findComponent(RunnerPagination); - + const findEmptyState = () => wrapper.findComponent(RunnerJobsEmptyState); const createComponent = ({ mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerJobs, { apolloProvider: createMockApollo([[runnerJobsQuery, mockRunnerJobsQuery]]), @@ -127,8 +128,8 @@ describe('RunnerJobs', () => { await waitForPromises(); }); - it('Shows a "None" label', () => { - expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND); + it('should render empty state', () => { + expect(findEmptyState().exists()).toBe(true); }); }); diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js index 196005c9882..d47a95f5c07 100644 --- a/spec/frontend/ci/runner/mock_data.js +++ b/spec/frontend/ci/runner/mock_data.js @@ -1,9 +1,9 @@ // Fixtures generated by: spec/frontend/fixtures/runner.rb // List queries +import allRunnersWithCreatorData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.with_creator.json'; import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json'; import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json'; -import allRunnersWithCreatorData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.with_creator.json'; import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json'; import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json'; import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json'; diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap index b8e6bcbc3c4..a328f79e4e7 100644 --- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap +++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = ` -"<b-button-stub size=\\"md\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\"> +"<b-button-stub size=\\"sm\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-mr-3 gl-button btn-default-tertiary btn-icon\\"> <!----> <gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub> <!----> diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index b642ac9c46b..8bbd79a61af 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -2,7 +2,6 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { EditorContent, Editor } from '@tiptap/vue-2'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue'; import ContentEditor from '~/content_editor/components/content_editor.vue'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue'; @@ -44,7 +43,6 @@ describe('ContentEditor', () => { ContentEditorAlert, GlLink, GlSprintf, - EditorModeDropdown, }, }); }; @@ -107,12 +105,6 @@ describe('ContentEditor', () => { expect(findEditorElement().text()).not.toContain('For quick actions, type /'); }); - it('renders an editor mode dropdown', () => { - createWrapper(); - - expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true); - }); - describe('when setting initial content', () => { it('displays loading indicator', async () => { createWrapper(); diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js index 5d2a9e493e5..2fc7e5e2e1b 100644 --- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js +++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js @@ -6,6 +6,7 @@ import { TOOLBAR_CONTROL_TRACKING_ACTION, CONTENT_EDITOR_TRACKING_LABEL, } from '~/content_editor/constants'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; describe('content_editor/components/formatting_toolbar', () => { let wrapper; @@ -16,6 +17,7 @@ describe('content_editor/components/formatting_toolbar', () => { stubs: { GlTabs, GlTab, + EditorModeSwitcher, }, }); }; @@ -64,4 +66,10 @@ describe('content_editor/components/formatting_toolbar', () => { }); }); }); + + it('renders an editor mode dropdown', () => { + buildWrapper(); + + expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true); + }); }); diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js index 71ffbd3f93c..0d56280d630 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js @@ -3,6 +3,7 @@ import { NodeViewWrapper } from '@tiptap/vue-2'; import { selectedRect as getSelectedRect } from '@tiptap/pm/tables'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils'; @@ -20,6 +21,13 @@ describe('content/components/wrappers/table_cell_base', () => { node, ...propsData, }, + stubs: { + GlDropdown: stubComponent(GlDropdown, { + methods: { + hide: jest.fn(), + }, + }), + }, }); }; @@ -38,14 +46,6 @@ describe('content/components/wrappers/table_cell_base', () => { jest.spyOn($cursor, 'node').mockReturnValue(node); }; - const mockDropdownHide = () => { - /* - * TODO: Replace this method with using the scoped hide function - * provided by BootstrapVue https://bootstrap-vue.org/docs/components/dropdown. - * GitLab UI is not exposing it in the default scope - */ - findDropdown().vm.hide = jest.fn(); - }; beforeEach(() => { node = {}; @@ -96,8 +96,6 @@ describe('content/components/wrappers/table_cell_base', () => { createWrapper(); await nextTick(); - - mockDropdownHide(); }); it.each` diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js index f23bca54b55..02a8e38dc2a 100644 --- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js +++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js @@ -1,5 +1,6 @@ -import { GlToggle } from '@gitlab/ui'; +import { GlIcon, GlToggle } from '@gitlab/ui'; import { nextTick } from 'vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { trimText } from 'helpers/text_helper'; import { mockTracking } from 'helpers/tracking_helper'; @@ -46,6 +47,13 @@ const getDefaultProps = () => ({ }, ], }, + { + id: 2, + iid: 2, + active: true, + name: 'flag without description', + description: '', + }, ], }); @@ -61,6 +69,9 @@ describe('Feature flag table', () => { csrfToken: 'fakeToken', }, ...opts, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, }); }; @@ -105,10 +116,6 @@ describe('Feature flag table', () => { it('Should render a feature flag column', () => { expect(wrapper.find('.js-feature-flag-title').exists()).toBe(true); expect(trimText(wrapper.find('.feature-flag-name').text())).toEqual('flag name'); - - expect(trimText(wrapper.find('.feature-flag-description').text())).toEqual( - 'flag description', - ); }); it('should render an environments specs label', () => { @@ -125,6 +132,37 @@ describe('Feature flag table', () => { }); }); + describe.each(getDefaultProps().featureFlags)('description tooltip', (featureFlag) => { + beforeEach(() => { + createWrapper(props); + }); + + const haveInfoIcon = Boolean(featureFlag.description); + + it(`${haveInfoIcon ? 'displays' : "doesn't display"} an information icon`, () => { + expect( + wrapper + .findByTestId(featureFlag.id) + .find('.feature-flag-description') + .findComponent(GlIcon) + .exists(), + ).toBe(haveInfoIcon); + }); + + if (haveInfoIcon) { + it('includes a tooltip', () => { + const icon = wrapper + .findByTestId(featureFlag.id) + .find('.feature-flag-description') + .findComponent(GlIcon); + const tooltip = getBinding(icon.element, 'gl-tooltip'); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(featureFlag.description); + }); + } + }); + describe('when active and with an update toggle', () => { let toggle; let spy; diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js index a1ca9a69926..e7011f896b6 100644 --- a/spec/frontend/invite_members/components/group_select_spec.js +++ b/spec/frontend/invite_members/components/group_select_spec.js @@ -2,28 +2,29 @@ import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; import * as groupsApi from '~/api/groups_api'; +import * as projectsApi from '~/api/projects_api'; import GroupSelect from '~/invite_members/components/group_select.vue'; const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' }; const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' }; const allGroups = [group1, group2]; -const createComponent = (props = {}) => { - return mount(GroupSelect, { - propsData: { - invalidGroups: [], - ...props, - }, - }); -}; - describe('GroupSelect', () => { let wrapper; + const createComponent = (props = {}) => { + wrapper = mount(GroupSelect, { + propsData: { + invalidGroups: [], + sourceId: '1', + isProject: false, + ...props, + }, + }); + }; + beforeEach(() => { jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups); - - wrapper = createComponent(); }); const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); @@ -35,48 +36,93 @@ describe('GroupSelect', () => { .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text); it('renders GlSearchBoxByType with default attributes', () => { + createComponent(); + expect(findSearchBoxByType().exists()).toBe(true); expect(findSearchBoxByType().vm.$attrs).toMatchObject({ placeholder: 'Search groups', }); }); - describe('when user types in the search input', () => { - let resolveApiRequest; + describe('when `isProject` prop is `false`', () => { + describe('when user types in the search input', () => { + let resolveApiRequest; - beforeEach(() => { - jest.spyOn(groupsApi, 'getGroups').mockImplementation( - () => - new Promise((resolve) => { - resolveApiRequest = resolve; - }), - ); + beforeEach(() => { + jest.spyOn(groupsApi, 'getGroups').mockImplementation( + () => + new Promise((resolve) => { + resolveApiRequest = resolve; + }), + ); - findSearchBoxByType().vm.$emit('input', group1.name); - }); + createComponent(); - it('calls the API', () => { - resolveApiRequest({ data: allGroups }); + findSearchBoxByType().vm.$emit('input', group1.name); + }); + + it('calls the API', () => { + resolveApiRequest(allGroups); - expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, { - exclude_internal: true, - active: true, - order_by: 'similarity', + expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, { + exclude_internal: true, + active: true, + order_by: 'similarity', + }); + }); + + it('displays loading icon while waiting for API call to resolve', async () => { + expect(findSearchBoxByType().props('isLoading')).toBe(true); + + resolveApiRequest(allGroups); + await waitForPromises(); + + expect(findSearchBoxByType().props('isLoading')).toBe(false); }); }); + }); - it('displays loading icon while waiting for API call to resolve', async () => { - expect(findSearchBoxByType().props('isLoading')).toBe(true); + describe('when `isProject` prop is `true`', () => { + describe('when user types in the search input', () => { + let resolveApiRequest; - resolveApiRequest({ data: allGroups }); - await waitForPromises(); + beforeEach(() => { + jest.spyOn(projectsApi, 'getProjectShareLocations').mockImplementation( + () => + new Promise((resolve) => { + resolveApiRequest = resolve; + }), + ); + + createComponent({ isProject: true }); + + findSearchBoxByType().vm.$emit('input', group1.name); + }); + + it('calls the API', () => { + resolveApiRequest({ data: allGroups }); + + expect(projectsApi.getProjectShareLocations).toHaveBeenCalledWith('1', { + search: group1.name, + }); + }); - expect(findSearchBoxByType().props('isLoading')).toBe(false); + it('displays loading icon while waiting for API call to resolve', async () => { + expect(findSearchBoxByType().props('isLoading')).toBe(true); + + resolveApiRequest({ data: allGroups }); + await waitForPromises(); + + expect(findSearchBoxByType().props('isLoading')).toBe(false); + }); }); }); describe('avatar label', () => { - it('includes the correct attributes with name and avatar_url', () => { + it('includes the correct attributes with name and avatar_url', async () => { + createComponent(); + await waitForPromises(); + expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({ src: group1.avatar_url, 'entity-id': `${group1.id}`, @@ -87,7 +133,7 @@ describe('GroupSelect', () => { describe('when filtering out the group from results', () => { beforeEach(() => { - wrapper = createComponent({ invalidGroups: [group1.id] }); + createComponent({ invalidGroups: [group1.id] }); }); it('does not find an invalid group', () => { @@ -101,7 +147,10 @@ describe('GroupSelect', () => { }); describe('when group is selected from the dropdown', () => { - beforeEach(() => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + findAvatarByLabel(group1.full_name).trigger('click'); }); diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js index 4f082145562..21e215764e5 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -242,4 +242,16 @@ describe('InviteGroupsModal', () => { }); }); }); + + it('renders `GroupSelect` component and passes correct props', () => { + createComponent(); + + expect(findGroupSelect().props()).toEqual({ + groupsFilter: 'all', + sourceId: '1', + parentGroupId: null, + invalidGroups: [], + isProject: false, + }); + }); }); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index fb64551c76b..70f25afc5ba 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -264,13 +264,13 @@ describe('issue_comment_form component', () => { it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } }); - expect(wrapper.text()).not.toContain('Rich text'); + expect(wrapper.text()).not.toContain('Switch to rich text'); }); it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } }); - expect(wrapper.text()).toContain('Rich text'); + expect(wrapper.text()).toContain('Switch to rich text'); }); describe('textarea', () => { diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 63286927d53..879bada4aee 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -1,9 +1,10 @@ -import { mount, createWrapper } from '@vue/test-utils'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import noteActions from '~/notes/components/note_actions.vue'; import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants'; import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue'; @@ -19,6 +20,8 @@ describe('noteActions', () => { let actions; let axiosMock; + const mockCloseDropdown = jest.fn(); + const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx); const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim(); const findTimelineButton = () => wrapper.findComponent(TimelineEventButton); @@ -45,6 +48,14 @@ describe('noteActions', () => { store, propsData, computed, + stubs: { + GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, { + methods: { + close: mockCloseDropdown, + }, + }), + GlDisclosureDropdownItem, + }, }); }; @@ -144,17 +155,6 @@ describe('noteActions', () => { expect(wrapper.find('.js-note-delete').exists()).toBe(true); }); - it('closes tooltip when dropdown opens', async () => { - wrapper.find('.more-actions-toggle').trigger('click'); - - const rootWrapper = createWrapper(wrapper.vm.$root); - - await nextTick(); - const emitted = Object.keys(rootWrapper.emitted()); - - expect(emitted).toEqual([BV_HIDE_TOOLTIP]); - }); - it('should not be possible to assign or unassign the comment author in a merge request', () => { const assignUserButton = wrapper.find('[data-testid="assign-user"]'); expect(assignUserButton.exists()).toBe(false); @@ -175,6 +175,11 @@ describe('noteActions', () => { const { resolveButton } = wrapper.vm.$refs; expect(resolveButton.$el.getAttribute('title')).toBe(`Resolved by ${complexUnescapedName}`); }); + + it('closes the dropdown', () => { + findReportAbuseButton().vm.$emit('action'); + expect(mockCloseDropdown).toHaveBeenCalled(); + }); }); }); @@ -401,13 +406,13 @@ describe('noteActions', () => { }); it('opens the drawer when report abuse button is clicked', async () => { - await findReportAbuseButton().trigger('click'); + await findReportAbuseButton().vm.$emit('action'); expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true); }); it('closes the drawer', async () => { - await findReportAbuseButton().trigger('click'); + await findReportAbuseButton().vm.$emit('action'); findAbuseCategorySelector().vm.$emit('close-drawer'); await nextTick(); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 9423af4f058..b5b33607282 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -66,13 +66,13 @@ describe('issue_note_form component', () => { it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { createComponentWrapper({}, { contentEditorOnIssues: false }); - expect(wrapper.text()).not.toContain('Rich text'); + expect(wrapper.text()).not.toContain('Switch to rich text'); }); it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { createComponentWrapper({}, { contentEditorOnIssues: true }); - expect(wrapper.text()).toContain('Rich text'); + expect(wrapper.text()).toContain('Switch to rich text'); }); describe('conflicts editing', () => { diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js index 52615ac3c65..e72de11d921 100644 --- a/spec/frontend/shortcuts_spec.js +++ b/spec/frontend/shortcuts_spec.js @@ -1,11 +1,10 @@ import $ from 'jquery'; -import htmlSnippetsShow from 'test_fixtures/snippets/show.html'; import { flatten } from 'lodash'; +import htmlSnippetsShow from 'test_fixtures/snippets/show.html'; import { Mousetrap } from '~/lib/mousetrap'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/shortcuts'; - -jest.mock('mousetrap/plugins/pause/mousetrap-pause', () => {}); +import MarkdownPreview from '~/behaviors/preview_markdown'; describe('Shortcuts', () => { const createEvent = (type, target) => @@ -21,6 +20,9 @@ describe('Shortcuts', () => { beforeEach(() => { setHTMLFixture(htmlSnippetsShow); + new Shortcuts(); // eslint-disable-line no-new + new MarkdownPreview(); // eslint-disable-line no-new + jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus'); jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus'); jest.spyOn(document.querySelector('#search'), 'focus'); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index 7eb0468c5be..c8d972b19a3 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -19,7 +19,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = <gl-form-input-stub class="form-control" data-qa-selector="description_placeholder" - placeholder="Optionally add a description about what your snippet does or how to use it…" + placeholder="Describe what your snippet does or how to use it…" /> </div> @@ -90,7 +90,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = </div> <div - class="js-vue-md-preview md md-preview-holder" + class="js-vue-md-preview md md-preview-holder gl-px-5" style="display: none;" /> diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js index 1f236616e77..d2984254dee 100644 --- a/spec/frontend/super_sidebar/utils_spec.js +++ b/spec/frontend/super_sidebar/utils_spec.js @@ -13,8 +13,8 @@ describe('Super sidebar utils spec', () => { describe('getTopFrequentItems', () => { const maxItems = 3; - it('returns empty array if no items provided', () => { - const result = getTopFrequentItems(); + it.each([undefined, null])('returns empty array if `items` is %s', (items) => { + const result = getTopFrequentItems(items); expect(result.length).toBe(0); }); diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap index 2189d6ac3cc..6f98a74a82f 100644 --- a/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap +++ b/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap @@ -2,18 +2,8 @@ exports[`Form Footer Actions renders content properly 1`] = ` <footer - class="form-actions d-flex justify-content-between" + class="gl-mt-5 footer-block" > - <div> - Bar - </div> - - <div> - Foo - </div> - - <div> - Abrakadabra - </div> + Bar Foo Abrakadabra </footer> `; diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js deleted file mode 100644 index fd8493e0911..00000000000 --- a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue'; - -describe('vue_shared/component/markdown/editor_mode_dropdown', () => { - let wrapper; - - const createComponent = ({ value, size } = {}) => { - wrapper = shallowMount(EditorModeDropdown, { - propsData: { - value, - size, - }, - }); - }; - - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItem = (text) => - wrapper - .findAllComponents(GlDropdownItem) - .filter((item) => item.text().startsWith(text)) - .at(0); - - describe.each` - modeText | value | dropdownText | otherMode - ${'Rich text'} | ${'richText'} | ${'Editing rich text'} | ${'Markdown'} - ${'Markdown'} | ${'markdown'} | ${'Editing markdown'} | ${'Rich text'} - `('$modeText', ({ modeText, value, dropdownText, otherMode }) => { - beforeEach(() => { - createComponent({ value }); - }); - - it('shows correct dropdown label', () => { - expect(findDropdown().props('text')).toEqual(dropdownText); - }); - - it('checks correct checked dropdown item', () => { - expect(findDropdownItem(modeText).props().isChecked).toBe(true); - expect(findDropdownItem(otherMode).props().isChecked).toBe(false); - }); - - it('emits event on click', () => { - findDropdownItem(modeText).vm.$emit('click'); - - expect(wrapper.emitted().input).toEqual([[value]]); - }); - }); - - it('passes size to dropdown', () => { - createComponent({ size: 'small', value: 'markdown' }); - - expect(findDropdown().props('size')).toEqual('small'); - }); -}); diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js new file mode 100644 index 00000000000..693353ed604 --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js @@ -0,0 +1,37 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; + +describe('vue_shared/component/markdown/editor_mode_switcher', () => { + let wrapper; + + const createComponent = ({ value } = {}) => { + wrapper = shallowMount(EditorModeSwitcher, { + propsData: { + value, + }, + }); + }; + + const findSwitcherButton = () => wrapper.findComponent(GlButton); + + describe.each` + modeText | value | buttonText + ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'} + ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'} + `('when $modeText', ({ modeText, value, buttonText }) => { + beforeEach(() => { + createComponent({ value }); + }); + + it('shows correct button label', () => { + expect(findSwitcherButton().text()).toEqual(buttonText); + }); + + it('emits event on click', () => { + findSwitcherButton(modeText).vm.$emit('click'); + + expect(wrapper.emitted().input).toEqual([[]]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 68ce07f86b9..b29f0d58d77 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -18,12 +18,6 @@ const textareaValue = 'testing\n123'; const uploadsPath = 'test/uploads'; const restrictedToolBarItems = ['quote']; -function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) { - expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite); - expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite); - expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : ''); -} - describe('Markdown field component', () => { let axiosMock; let subject; @@ -92,8 +86,7 @@ describe('Markdown field component', () => { }); } - const getPreviewLink = () => subject.findByTestId('preview-tab'); - const getWriteLink = () => subject.findByTestId('write-tab'); + const getPreviewToggle = () => subject.findByTestId('preview-toggle'); const getMarkdownButton = () => subject.find('.js-md'); const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]'); const getVideo = () => subject.find('video'); @@ -109,8 +102,7 @@ describe('Markdown field component', () => { <p>markdown preview</p> <video src="${FIXTURES_PATH}/static/mock-video.mp4"></video> `; - let previewLink; - let writeLink; + let previewToggle; let dropzoneSpy; beforeEach(() => { @@ -140,8 +132,8 @@ describe('Markdown field component', () => { .onPost(markdownPreviewPath) .reply(HTTP_STATUS_OK, { references: { users: [], commands: 'test command' } }); - previewLink = getPreviewLink(); - previewLink.vm.$emit('click', { target: {} }); + previewToggle = getPreviewToggle(); + previewToggle.vm.$emit('click', true); await axios.waitFor(markdownPreviewPath); const referencedCommands = subject.find('[data-testid="referenced-commands"]'); @@ -155,26 +147,29 @@ describe('Markdown field component', () => { axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { body: previewHTML }); }); - it('sets preview link as active', async () => { - previewLink = getPreviewLink(); - previewLink.vm.$emit('click', { target: {} }); + it('sets preview toggle as active', async () => { + previewToggle = getPreviewToggle(); + + expect(previewToggle.text()).toBe('Preview'); + + previewToggle.vm.$emit('click', true); await nextTick(); - expect(previewLink.element.children[0].classList.contains('active')).toBe(true); + expect(previewToggle.text()).toBe('Continue editing'); }); it('shows preview loading text', async () => { - previewLink = getPreviewLink(); - previewLink.vm.$emit('click', { target: {} }); + previewToggle = getPreviewToggle(); + previewToggle.vm.$emit('click', true); await nextTick(); expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…'); }); it('renders markdown preview and GFM', async () => { - previewLink = getPreviewLink(); + previewToggle = getPreviewToggle(); - previewLink.vm.$emit('click', { target: {} }); + previewToggle.vm.$emit('click', true); await axios.waitFor(markdownPreviewPath); expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); @@ -182,8 +177,8 @@ describe('Markdown field component', () => { }); it('calls video.pause() on comment input when isSubmitting is changed to true', async () => { - previewLink = getPreviewLink(); - previewLink.vm.$emit('click', { target: {} }); + previewToggle = getPreviewToggle(); + previewToggle.vm.$emit('click', true); await axios.waitFor(markdownPreviewPath); const video = getVideo(); @@ -195,34 +190,27 @@ describe('Markdown field component', () => { expect(callPause).toHaveBeenCalled(); }); - it('clicking already active write or preview link does nothing', async () => { - writeLink = getWriteLink(); - previewLink = getPreviewLink(); - - writeLink.vm.$emit('click', { target: {} }); - await nextTick(); - - assertMarkdownTabs(true, writeLink, previewLink, subject); - writeLink.vm.$emit('click', { target: {} }); - await nextTick(); + it('switches between preview/write on toggle', async () => { + previewToggle = getPreviewToggle(); - assertMarkdownTabs(true, writeLink, previewLink, subject); - previewLink.vm.$emit('click', { target: {} }); + previewToggle.vm.$emit('click', true); await nextTick(); + expect(subject.find('.md-preview-holder').element.style.display).toBe(''); // visible - assertMarkdownTabs(false, writeLink, previewLink, subject); - previewLink.vm.$emit('click', { target: {} }); + previewToggle.vm.$emit('click', false); await nextTick(); - - assertMarkdownTabs(false, writeLink, previewLink, subject); + expect(subject.find('.md-preview-holder').element.style.display).toBe('none'); }); - it('passes correct props to MarkdownToolbar', () => { + it('passes correct props to MarkdownHeader and MarkdownToolbar', () => { expect(findMarkdownToolbar().props()).toEqual({ canAttachFile: true, markdownDocsPath, quickActionsDocsPath: '', showCommentToolBar: true, + }); + + expect(findMarkdownHeader().props()).toMatchObject({ showContentEditorSwitcher: false, }); }); @@ -380,13 +368,13 @@ describe('Markdown field component', () => { it('defaults to false', () => { createSubject(); - expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false); + expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false); }); it('passes showContentEditorSwitcher', () => { createSubject({ showContentEditorSwitcher: true }); - expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true); + expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 68f05e5119d..48fe5452e74 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -1,10 +1,11 @@ import $ from 'jquery'; import { nextTick } from 'vue'; -import { GlTabs } from '@gitlab/ui'; +import { GlToggle } from '@gitlab/ui'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; describe('Markdown field header component', () => { let wrapper; @@ -15,12 +16,11 @@ describe('Markdown field header component', () => { previewMarkdown: false, ...props, }, - stubs: { GlTabs }, + stubs: { GlToggle }, }); }; - const findWriteTab = () => wrapper.findByTestId('write-tab'); - const findPreviewTab = () => wrapper.findByTestId('preview-tab'); + const findPreviewToggle = () => wrapper.findByTestId('preview-toggle'); const findToolbar = () => wrapper.findByTestId('md-header-toolbar'); const findToolbarButtons = () => wrapper.findAllComponents(ToolbarButton); const findToolbarButtonByProp = (prop, value) => @@ -87,16 +87,14 @@ describe('Markdown field header component', () => { }); }); - it('activates `write` tab when previewMarkdown is false', () => { - expect(findWriteTab().attributes('active')).toBe('true'); - expect(findPreviewTab().attributes('active')).toBeUndefined(); + it('hides markdown preview when previewMarkdown is false', () => { + expect(findPreviewToggle().text()).toBe('Preview'); }); - it('activates `preview` tab when previewMarkdown is true', () => { + it('shows markdown preview when previewMarkdown is true', () => { createWrapper({ previewMarkdown: true }); - expect(findWriteTab().attributes('active')).toBeUndefined(); - expect(findPreviewTab().attributes('active')).toBe('true'); + expect(findPreviewToggle().text()).toBe('Continue editing'); }); it('hides toolbar in preview mode', () => { @@ -105,17 +103,16 @@ describe('Markdown field header component', () => { expect(findToolbar().classes().includes('gl-display-none!')).toBe(true); }); - it('emits toggle markdown event when clicking preview tab', async () => { - const eventData = { target: {} }; - findPreviewTab().vm.$emit('click', eventData); + it('emits toggle markdown event when clicking preview toggle', async () => { + findPreviewToggle().vm.$emit('click', true); await nextTick(); - expect(wrapper.emitted('preview-markdown').length).toEqual(1); + expect(wrapper.emitted('showPreview').length).toEqual(1); - findWriteTab().vm.$emit('click', eventData); + findPreviewToggle().vm.$emit('click', false); await nextTick(); - expect(wrapper.emitted('write-markdown').length).toEqual(1); + expect(wrapper.emitted('showPreview').length).toEqual(2); }); it('does not emit toggle markdown event when triggered from another form', () => { @@ -125,15 +122,8 @@ describe('Markdown field header component', () => { ), ]); - expect(wrapper.emitted('preview-markdown')).toBeUndefined(); - expect(wrapper.emitted('write-markdown')).toBeUndefined(); - }); - - it('blurs preview link after click', () => { - const target = { blur: jest.fn() }; - findPreviewTab().vm.$emit('click', { target }); - - expect(target.blur).toHaveBeenCalled(); + expect(wrapper.emitted('showPreview')).toBeUndefined(); + expect(wrapper.emitted('hidePreview')).toBeUndefined(); }); it('renders markdown table template', () => { @@ -166,12 +156,12 @@ describe('Markdown field header component', () => { expect(wrapper.find('.js-suggestion-btn').exists()).toBe(false); }); - it('hides preview tab when previewMarkdown property is false', () => { + it('hides markdown preview when previewMarkdown property is false', () => { createWrapper({ enablePreview: false, }); - expect(wrapper.findByTestId('preview-tab').exists()).toBe(false); + expect(wrapper.findByTestId('preview-toggle').exists()).toBe(false); }); describe('restricted tool bar items', () => { @@ -215,4 +205,18 @@ describe('Markdown field header component', () => { }); }); }); + + describe('with content editor switcher', () => { + beforeEach(() => { + createWrapper({ + showContentEditorSwitcher: true, + }); + }); + + it('re-emits event from switcher', () => { + wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText'); + + expect(wrapper.emitted('enableContentEditor')).toEqual([[]]); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index 63a689088c7..dec2327db0f 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -122,13 +122,13 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it('enables content editor switcher when contentEditorEnabled prop is true', () => { buildWrapper({ propsData: { enableContentEditor: true } }); - expect(findMarkdownField().text()).toContain('Rich text'); + expect(findMarkdownField().text()).toContain('Switch to rich text'); }); it('hides content editor switcher when contentEditorEnabled prop is false', () => { buildWrapper({ propsData: { enableContentEditor: false } }); - expect(findMarkdownField().text()).not.toContain('Rich text'); + expect(findMarkdownField().text()).not.toContain('Switch to rich text'); }); it('passes down any additional props to markdown field component', () => { diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index fea14f80496..2489421b697 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js @@ -1,6 +1,5 @@ import { mount } from '@vue/test-utils'; import Toolbar from '~/vue_shared/components/markdown/toolbar.vue'; -import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue'; describe('toolbar', () => { let wrapper; @@ -44,18 +43,4 @@ describe('toolbar', () => { expect(wrapper.find('.comment-toolbar').exists()).toBe(true); }); }); - - describe('with content editor switcher', () => { - beforeEach(() => { - createMountedWrapper({ - showContentEditorSwitcher: true, - }); - }); - - it('re-emits event from switcher', () => { - wrapper.findComponent(EditorModeDropdown).vm.$emit('input', 'richText'); - - expect(wrapper.emitted('enableContentEditor')).toEqual([[]]); - }); - }); }); diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb new file mode 100644 index 00000000000..33605783671 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersGroupTypeActiveMetric, feature_category: :runner do + let_it_be(:group) { create(:group) } + let(:expected_value) { 1 } + + before do + create(:ci_runner, + :group, + groups: [group] + ) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb new file mode 100644 index 00000000000..24d6ea6f1e9 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersGroupTypeActiveOnlineMetric, feature_category: :runner do + let(:group) { create(:group) } + let(:expected_value) { 1 } + + before do + create(:ci_runner, + :group, + groups: [group], + contacted_at: 1.second.ago + ) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric_spec.rb new file mode 100644 index 00000000000..ae4829cceef --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_metric_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersInstanceTypeActiveMetric, feature_category: :runner do + let(:expected_value) { 1 } + + before do + create(:ci_runner) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric_spec.rb new file mode 100644 index 00000000000..b1b9a5a6cea --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_instance_type_active_online_metric_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersInstanceTypeActiveOnlineMetric, feature_category: :runner do + let(:expected_value) { 1 } + + before do + create(:ci_runner, contacted_at: 1.second.ago) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric_spec.rb new file mode 100644 index 00000000000..6a3a8e6dd58 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_metric_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersMetric, feature_category: :runner do + let(:expected_value) { 1 } + + before do + create(:ci_runner) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb new file mode 100644 index 00000000000..eeb699c1377 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersProjectTypeActiveMetric, feature_category: :runner do + let(:project) { build(:project) } + let(:expected_value) { 1 } + + before do + create(:ci_runner, + :project, + projects: [project] + ) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb new file mode 100644 index 00000000000..c3ed752ae04 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersProjectTypeActiveOnlineMetric, feature_category: :runner do + let(:project) { build(:project) } + let(:expected_value) { 1 } + + before do + create(:ci_runner, + :project, + projects: [project], + contacted_at: 1.second.ago + ) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } +end diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb index 6391b003096..1f52819fd9e 100644 --- a/spec/lib/gitlab/usage_data_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_metrics_spec.rb @@ -85,16 +85,4 @@ RSpec.describe Gitlab::UsageDataMetrics, :with_license, feature_category: :servi end end end - - describe '.suggested_names' do - subject { described_class.suggested_names } - - let(:suggested_names) do - ::Gitlab::Usage::Metric.all.map(&:with_suggested_name).reduce({}, :deep_merge) - end - - it 'includes Service Ping suggested names' do - expect(subject).to match_array(suggested_names) - end - end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 7bf25af598c..e0640f10a03 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -665,29 +665,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic end end - describe '.runners_usage' do - before do - project = build(:project) - create_list(:ci_runner, 2, :instance_type, :online) - create(:ci_runner, :group, :online) - create(:ci_runner, :group, :inactive) - create_list(:ci_runner, 3, :project_type, :online, projects: [project]) - end - - subject { described_class.runners_usage } - - it 'gathers runner usage counts correctly' do - expect(subject[:ci_runners]).to eq(7) - expect(subject[:ci_runners_instance_type_active]).to eq(2) - expect(subject[:ci_runners_group_type_active]).to eq(1) - expect(subject[:ci_runners_project_type_active]).to eq(3) - - expect(subject[:ci_runners_instance_type_active_online]).to eq(2) - expect(subject[:ci_runners_group_type_active_online]).to eq(1) - expect(subject[:ci_runners_project_type_active_online]).to eq(3) - end - end - describe '.license_usage_data' do subject { described_class.license_usage_data } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index db981c832ac..60583bc351d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5321,13 +5321,13 @@ RSpec.describe User, feature_category: :user_profile do end describe '#source_groups_of_two_factor_authentication_requirement' do - let_it_be(:group_not_requiring_2FA) { create :group } + let_it_be(:group_not_requiring_2fa) { create :group } let(:user) { create :user } before do group.add_member(user, GroupMember::OWNER) - group_not_requiring_2FA.add_member(user, GroupMember::OWNER) + group_not_requiring_2fa.add_member(user, GroupMember::OWNER) end context 'when user is direct member of group requiring 2FA' do diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb index 1bbc05cc05a..f19af0c9af8 100644 --- a/spec/support/helpers/content_editor_helpers.rb +++ b/spec/support/helpers/content_editor_helpers.rb @@ -2,8 +2,7 @@ module ContentEditorHelpers def switch_to_content_editor - click_button _('Editing markdown') - click_button _('Rich text') + click_button("Switch to rich text") end def type_in_content_editor(keys) diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb index 78774b515df..7973d541f9c 100644 --- a/spec/support/helpers/features/notes_helpers.rb +++ b/spec/support/helpers/features/notes_helpers.rb @@ -41,7 +41,7 @@ module Features wait_for_requests filled_text.send_keys(:escape) - click_on('Preview') + click_button("Preview") yield if block_given? end diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb index fa2705a64fa..40f1f6fe6f3 100644 --- a/spec/support/helpers/note_interaction_helpers.rb +++ b/spec/support/helpers/note_interaction_helpers.rb @@ -7,6 +7,6 @@ module NoteInteractionHelpers note_element = find_by_scrolling("#note_#{note.id}") note_element.find('.more-actions-toggle').click - note_element.find('.more-actions .dropdown-menu li', match: :first) + note_element.find('.more-actions li', match: :first) end end diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index beee663fbc6..24249438faf 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -7,7 +7,6 @@ module UsageDataHelpers ci_external_pipelines ci_pipeline_config_auto_devops ci_pipeline_config_repository - ci_runners ci_triggers ci_pipeline_schedules auto_devops_enabled diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index d2dfb468485..2bcbd5e5190 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -104,8 +104,8 @@ RSpec.shared_examples 'an editable merge request' do fill_in 'merge_request_description', with: long_description height = get_textarea_height - find('.js-md-preview-button').click - find('.js-md-write-button').click + click_button("Preview") + click_button("Continue editing") new_height = get_textarea_height expect(height).to eq(new_height) diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb index 45ad4d5cf71..133da230bed 100644 --- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb +++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb @@ -48,6 +48,6 @@ RSpec.shared_examples 'reportable note' do |type| restore_window_size dropdown.find('.more-actions-toggle').click - dropdown.find('.dropdown-menu li', match: :first) + dropdown.find('.more-actions li', match: :first) end end diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb index 7a3b94ad81d..6451c531aec 100644 --- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb @@ -62,7 +62,7 @@ RSpec.shared_examples 'wiki file attachments' do attach_with_dropzone(true) wait_for_requests - find('.js-md-preview-button').click + click_button("Preview") file_path = page.find('input[name="files[]"]', visible: :hidden).value link = page.find('a.no-attachment-icon')['href'] img_link = page.find('a.no-attachment-icon img')['src'] diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb index 3e285bb8ad7..ca68df9a89b 100644 --- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -78,7 +78,7 @@ RSpec.shared_examples 'User previews wiki changes' do it_behaves_like 'relative links' do before do - click_on 'Preview' + click_button("Preview") end let(:element) { preview } @@ -88,7 +88,7 @@ RSpec.shared_examples 'User previews wiki changes' do # using two `\n` ensures we're sublist to it's own line due # to list auto-continue fill_in :wiki_content, with: "1. one\n\n - sublist\n" - click_on "Preview" + click_button("Preview") # the above generates two separate lists (not embedded) in CommonMark expect(preview).to have_content("sublist") @@ -102,7 +102,7 @@ RSpec.shared_examples 'User previews wiki changes' do [[also_do_not_linkify]] ``` HEREDOC - click_on "Preview" + click_button("Preview") expect(preview).to have_content("do_not_linkify") expect(preview).to have_content('[[do_not_linkify]]') diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index bce889b454d..5740adb3f0e 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -68,3 +68,64 @@ RSpec.shared_examples_for 'LEFT JOIN-able value stream analytics event' do end end end + +RSpec.shared_examples_for 'value stream analytics first assignment event methods' do + let_it_be(:model1) { create(model_factory) } # rubocop: disable Rails/SaveBang + let_it_be(:model2) { create(model_factory) } # rubocop: disable Rails/SaveBang + + let_it_be(:assignment_event1) do + create(event_factory, action: :add, created_at: 3.years.ago, model_factory => model1) + end + + let_it_be(:assignment_event2) do + create(event_factory, action: :add, created_at: 2.years.ago, model_factory => model1) + end + + let_it_be(:unassignment_event1) do + create(event_factory, action: :remove, created_at: 1.year.ago, model_factory => model1) + end + + let(:query) { model1.class.where(id: [model1.id, model2.id]) } + let(:event) { described_class.new({}) } + + describe '#apply_query_customization' do + subject(:records) { event.apply_query_customization(query).pluck(:id, *event.column_list).to_a } + + it 'looks up the first assignment event timestamp' do + expect(records).to match_array([[model1.id, be_within(1.second).of(assignment_event1.created_at)]]) + end + end + + describe '#apply_negated_query_customization' do + subject(:records) { event.apply_negated_query_customization(query).pluck(:id).to_a } + + it 'returns records where the event has not happened yet' do + expect(records).to eq([model2.id]) + end + end + + describe '#include_in' do + subject(:records) { event.include_in(query).pluck(:id, *event.column_list).to_a } + + it 'returns both records' do + expect(records).to match_array([ + [model1.id, be_within(1.second).of(assignment_event1.created_at)], + [model2.id, nil] + ]) + end + + context 'when invoked multiple times' do + subject(:records) do + scope = event.include_in(query) + event.include_in(scope).pluck(:id, *event.column_list).to_a + end + + it 'returns both records' do + expect(records).to match_array([ + [model1.id, be_within(1.second).of(assignment_event1.created_at)], + [model2.id, nil] + ]) + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 1a0b2612033..b3a8deae813 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4192,10 +4192,10 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== -core-js@^3.29.1, core-js@^3.6.5: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.1.tgz#40ff3b41588b091aaed19ca1aa5cb111803fa9a6" - integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw== +core-js@^3.29.1, core-js@^3.30.1, core-js@^3.6.5: + version "3.30.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba" + integrity sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ== core-util-is@~1.0.0: version "1.0.3" @@ -12677,10 +12677,10 @@ vue-resize@^1.0.1: dependencies: "@vue/devtools-api" "^6.4.5" -vue-router@3.4.9: - version "3.4.9" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.9.tgz#c016f42030ae2932f14e4748b39a1d9a0e250e66" - integrity sha512-CGAKWN44RqXW06oC+u4mPgHLQQi2t6vLD/JbGRDAXm0YpMv0bgpKuU5bBd7AvMgfTz9kXVRIWKHqRwGEb8xFkA== +vue-router@3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8" + integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ== vue-runtime-helpers@^1.1.2: version "1.1.2" |